MappedPropertyDescriptor.java
- /*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- package org.apache.commons.beanutils2;
- import java.beans.IntrospectionException;
- import java.beans.PropertyDescriptor;
- import java.lang.ref.Reference;
- import java.lang.ref.SoftReference;
- import java.lang.ref.WeakReference;
- import java.lang.reflect.Method;
- import java.lang.reflect.Modifier;
- /**
- * A MappedPropertyDescriptor describes one mapped property. Mapped properties are multivalued properties like indexed properties but that are accessed with a
- * String key instead of an index. Such property values are typically stored in a Map collection. For this class to work properly, a mapped value must have
- * getter and setter methods of the form
- * <p>
- * {@code get<strong>Property</strong>(String key)} and
- * <p>
- * {@code set<strong>Property</strong>(String key, Object value)},
- * <p>
- * where {@code <strong>Property</strong>} must be replaced by the name of the property.
- *
- * @see java.beans.PropertyDescriptor
- */
- public class MappedPropertyDescriptor extends PropertyDescriptor {
- /**
- * Holds a {@link Method} in a {@link SoftReference} so that it it doesn't prevent any ClassLoader being garbage collected, but tries to re-create the
- * method if the method reference has been released.
- *
- * See https://issues.apache.org/jira/browse/BEANUTILS-291
- */
- private static final class MappedMethodReference {
- private String className;
- private String methodName;
- private Reference<Method> methodRef;
- private Reference<Class<?>> classRef;
- private Reference<Class<?>> writeParamTypeRef0;
- private Reference<Class<?>> writeParamTypeRef1;
- private String[] writeParamClassNames;
- MappedMethodReference(final Method m) {
- if (m != null) {
- className = m.getDeclaringClass().getName();
- methodName = m.getName();
- methodRef = new SoftReference<>(m);
- classRef = new WeakReference<>(m.getDeclaringClass());
- final Class<?>[] types = m.getParameterTypes();
- if (types.length == 2) {
- writeParamTypeRef0 = new WeakReference<>(types[0]);
- writeParamTypeRef1 = new WeakReference<>(types[1]);
- writeParamClassNames = new String[2];
- writeParamClassNames[0] = types[0].getName();
- writeParamClassNames[1] = types[1].getName();
- }
- }
- }
- private Method get() {
- if (methodRef == null) {
- return null;
- }
- Method m = methodRef.get();
- if (m == null) {
- Class<?> clazz = classRef.get();
- if (clazz == null) {
- clazz = reLoadClass();
- if (clazz != null) {
- classRef = new WeakReference<>(clazz);
- }
- }
- if (clazz == null) {
- throw new RuntimeException("Method " + methodName + " for " + className + " could not be reconstructed - class reference has gone");
- }
- Class<?>[] paramTypes = null;
- if (writeParamClassNames != null) {
- paramTypes = new Class[2];
- paramTypes[0] = writeParamTypeRef0.get();
- if (paramTypes[0] == null) {
- paramTypes[0] = reLoadClass(writeParamClassNames[0]);
- if (paramTypes[0] != null) {
- writeParamTypeRef0 = new WeakReference<>(paramTypes[0]);
- }
- }
- paramTypes[1] = writeParamTypeRef1.get();
- if (paramTypes[1] == null) {
- paramTypes[1] = reLoadClass(writeParamClassNames[1]);
- if (paramTypes[1] != null) {
- writeParamTypeRef1 = new WeakReference<>(paramTypes[1]);
- }
- }
- } else {
- paramTypes = STRING_CLASS_PARAMETER;
- }
- try {
- m = clazz.getMethod(methodName, paramTypes);
- // Un-comment following line for testing
- // System.out.println("Recreated Method " + methodName + " for " + className);
- } catch (final NoSuchMethodException e) {
- throw new RuntimeException("Method " + methodName + " for " + className + " could not be reconstructed - method not found");
- }
- methodRef = new SoftReference<>(m);
- }
- return m;
- }
- /**
- * Try to re-load the class
- */
- private Class<?> reLoadClass() {
- return reLoadClass(className);
- }
- /**
- * Try to re-load the class
- */
- private Class<?> reLoadClass(final String name) {
- ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
- // Try the context class loader
- if (classLoader != null) {
- try {
- return classLoader.loadClass(name);
- } catch (final ClassNotFoundException e) {
- // ignore
- }
- }
- // Try this class's class loader
- classLoader = MappedPropertyDescriptor.class.getClassLoader();
- try {
- return classLoader.loadClass(name);
- } catch (final ClassNotFoundException e) {
- return null;
- }
- }
- }
- /**
- * The parameter types array for the reader method signature.
- */
- private static final Class<?>[] STRING_CLASS_PARAMETER = new Class[] { String.class };
- /**
- * Gets a capitalized version of the specified property name.
- *
- * @param s The property name
- */
- private static String capitalizePropertyName(final String s) {
- if (s.isEmpty()) {
- return s;
- }
- final char[] chars = s.toCharArray();
- chars[0] = Character.toUpperCase(chars[0]);
- return new String(chars);
- }
- /**
- * Find a method on a class with a specified parameter list.
- */
- private static Method getMethod(final Class<?> clazz, final String methodName, final Class<?>[] parameterTypes) throws IntrospectionException {
- if (methodName == null) {
- return null;
- }
- final Method method = MethodUtils.getMatchingAccessibleMethod(clazz, methodName, parameterTypes);
- if (method != null) {
- return method;
- }
- final int parameterCount = parameterTypes == null ? 0 : parameterTypes.length;
- // No Method found
- throw new IntrospectionException("No method \"" + methodName + "\" with " + parameterCount + " parameter(s) of matching types.");
- }
- /**
- * Find a method on a class with a specified number of parameters.
- */
- private static Method getMethod(final Class<?> clazz, final String methodName, final int parameterCount) throws IntrospectionException {
- if (methodName == null) {
- return null;
- }
- final Method method = internalGetMethod(clazz, methodName, parameterCount);
- if (method != null) {
- return method;
- }
- // No Method found
- throw new IntrospectionException("No method \"" + methodName + "\" with " + parameterCount + " parameter(s)");
- }
- /**
- * Find a method on a class with a specified number of parameters.
- */
- private static Method internalGetMethod(final Class<?> initial, final String methodName, final int parameterCount) {
- // For overridden methods we need to find the most derived version.
- // So we start with the given class and walk up the superclass chain.
- for (Class<?> clazz = initial; clazz != null; clazz = clazz.getSuperclass()) {
- final Method[] methods = clazz.getDeclaredMethods();
- for (final Method method : methods) {
- if (method == null) {
- continue;
- }
- // skip static methods.
- final int mods = method.getModifiers();
- if (!Modifier.isPublic(mods) || Modifier.isStatic(mods)) {
- continue;
- }
- if (method.getName().equals(methodName) && method.getParameterTypes().length == parameterCount) {
- return method;
- }
- }
- }
- // Now check any inherited interfaces. This is necessary both when
- // the argument class is itself an interface, and when the argument
- // class is an abstract class.
- final Class<?>[] interfaces = initial.getInterfaces();
- for (final Class<?> interface1 : interfaces) {
- final Method method = internalGetMethod(interface1, methodName, parameterCount);
- if (method != null) {
- return method;
- }
- }
- return null;
- }
- /**
- * The underlying data type of the property we are describing.
- */
- private Reference<Class<?>> mappedPropertyTypeRef;
- /**
- * The reader method for this property (if any).
- */
- private MappedMethodReference mappedReadMethodRef;
- /**
- * The writer method for this property (if any).
- */
- private MappedMethodReference mappedWriteMethodRef;
- /**
- * Constructs a MappedPropertyDescriptor for a property that follows the standard Java convention by having getFoo and setFoo accessor methods, with the
- * addition of a String parameter (the key). Thus if the argument name is "fred", it will assume that the writer method is "setFred" and the reader method
- * is "getFred". Note that the property name should start with a lower case character, which will be capitalized in the method names.
- *
- * @param propertyName The programmatic name of the property.
- * @param beanClass The Class object for the target bean. For example sun.beans.OurButton.class.
- * @throws IntrospectionException if an exception occurs during introspection.
- */
- public MappedPropertyDescriptor(final String propertyName, final Class<?> beanClass) throws IntrospectionException {
- super(propertyName, null, null);
- if (propertyName == null || propertyName.isEmpty()) {
- throw new IntrospectionException("bad property name: " + propertyName + " on class: " + beanClass.getClass().getName());
- }
- setName(propertyName);
- final String base = capitalizePropertyName(propertyName);
- // Look for mapped read method and matching write method
- Method mappedReadMethod = null;
- Method mappedWriteMethod = null;
- try {
- try {
- mappedReadMethod = getMethod(beanClass, "get" + base, STRING_CLASS_PARAMETER);
- } catch (final IntrospectionException e) {
- mappedReadMethod = getMethod(beanClass, "is" + base, STRING_CLASS_PARAMETER);
- }
- final Class<?>[] params = { String.class, mappedReadMethod.getReturnType() };
- mappedWriteMethod = getMethod(beanClass, "set" + base, params);
- } catch (final IntrospectionException e) {
- /*
- * Swallow IntrospectionException TODO: Why?
- */
- }
- // If there's no read method, then look for just a write method
- if (mappedReadMethod == null) {
- mappedWriteMethod = getMethod(beanClass, "set" + base, 2);
- }
- if (mappedReadMethod == null && mappedWriteMethod == null) {
- throw new IntrospectionException("Property '" + propertyName + "' not found on " + beanClass.getName());
- }
- mappedReadMethodRef = new MappedMethodReference(mappedReadMethod);
- mappedWriteMethodRef = new MappedMethodReference(mappedWriteMethod);
- findMappedPropertyType();
- }
- /**
- * This constructor takes the name of a mapped property, and method names for reading and writing the property.
- *
- * @param propertyName The programmatic name of the property.
- * @param beanClass The Class object for the target bean. For example sun.beans.OurButton.class.
- * @param mappedGetterName The name of the method used for reading one of the property values. May be null if the property is write-only.
- * @param mappedSetterName The name of the method used for writing one of the property values. May be null if the property is read-only.
- * @throws IntrospectionException if an exception occurs during introspection.
- */
- public MappedPropertyDescriptor(final String propertyName, final Class<?> beanClass, final String mappedGetterName, final String mappedSetterName)
- throws IntrospectionException {
- super(propertyName, null, null);
- if (propertyName == null || propertyName.isEmpty()) {
- throw new IntrospectionException("bad property name: " + propertyName);
- }
- setName(propertyName);
- // search the mapped get and set methods
- Method mappedReadMethod;
- Method mappedWriteMethod = null;
- mappedReadMethod = getMethod(beanClass, mappedGetterName, STRING_CLASS_PARAMETER);
- if (mappedReadMethod != null) {
- final Class<?>[] params = { String.class, mappedReadMethod.getReturnType() };
- mappedWriteMethod = getMethod(beanClass, mappedSetterName, params);
- } else {
- mappedWriteMethod = getMethod(beanClass, mappedSetterName, 2);
- }
- mappedReadMethodRef = new MappedMethodReference(mappedReadMethod);
- mappedWriteMethodRef = new MappedMethodReference(mappedWriteMethod);
- findMappedPropertyType();
- }
- /**
- * This constructor takes the name of a mapped property, and Method objects for reading and writing the property.
- *
- * @param propertyName The programmatic name of the property.
- * @param mappedGetter The method used for reading one of the property values. May be null if the property is write-only.
- * @param mappedSetter The method used for writing one the property values. May be null if the property is read-only.
- * @throws IntrospectionException if an exception occurs during introspection.
- */
- public MappedPropertyDescriptor(final String propertyName, final Method mappedGetter, final Method mappedSetter) throws IntrospectionException {
- super(propertyName, mappedGetter, mappedSetter);
- if (propertyName == null || propertyName.isEmpty()) {
- throw new IntrospectionException("bad property name: " + propertyName);
- }
- setName(propertyName);
- mappedReadMethodRef = new MappedMethodReference(mappedGetter);
- mappedWriteMethodRef = new MappedMethodReference(mappedSetter);
- findMappedPropertyType();
- }
- /**
- * Introspect our bean class to identify the corresponding getter and setter methods.
- */
- private void findMappedPropertyType() throws IntrospectionException {
- final Method mappedReadMethod = getMappedReadMethod();
- final Method mappedWriteMethod = getMappedWriteMethod();
- Class<?> mappedPropertyType = null;
- if (mappedReadMethod != null) {
- if (mappedReadMethod.getParameterTypes().length != 1) {
- throw new IntrospectionException("bad mapped read method arg count");
- }
- mappedPropertyType = mappedReadMethod.getReturnType();
- if (mappedPropertyType == Void.TYPE) {
- throw new IntrospectionException("mapped read method " + mappedReadMethod.getName() + " returns void");
- }
- }
- if (mappedWriteMethod != null) {
- final Class<?>[] params = mappedWriteMethod.getParameterTypes();
- if (params.length != 2) {
- throw new IntrospectionException("bad mapped write method arg count");
- }
- if (mappedPropertyType != null && mappedPropertyType != params[1]) {
- throw new IntrospectionException("type mismatch between mapped read and write methods");
- }
- mappedPropertyType = params[1];
- }
- mappedPropertyTypeRef = new SoftReference<>(mappedPropertyType);
- }
- /**
- * Gets the Class object for the property values.
- *
- * @return The Java type info for the property values. Note that the "Class" object may describe a built-in Java type such as "int". The result may be
- * "null" if this is a mapped property that does not support non-keyed access.
- * <p>
- * This is the type that will be returned by the mappedReadMethod.
- */
- public Class<?> getMappedPropertyType() {
- return mappedPropertyTypeRef.get();
- }
- /**
- * Gets the method that should be used to read one of the property value.
- *
- * @return The method that should be used to read the property value. May return null if the property can't be read.
- */
- public Method getMappedReadMethod() {
- return mappedReadMethodRef.get();
- }
- /**
- * Gets the method that should be used to write one of the property value.
- *
- * @return The method that should be used to write one of the property value. May return null if the property can't be written.
- */
- public Method getMappedWriteMethod() {
- return mappedWriteMethodRef.get();
- }
- /**
- * Sets the method that should be used to read one of the property value.
- *
- * @param mappedGetter The mapped getter method.
- * @throws IntrospectionException If an error occurs finding the mapped property
- */
- public void setMappedReadMethod(final Method mappedGetter) throws IntrospectionException {
- mappedReadMethodRef = new MappedMethodReference(mappedGetter);
- findMappedPropertyType();
- }
- /**
- * Sets the method that should be used to write the property value.
- *
- * @param mappedSetter The mapped setter method.
- * @throws IntrospectionException If an error occurs finding the mapped property
- */
- public void setMappedWriteMethod(final Method mappedSetter) throws IntrospectionException {
- mappedWriteMethodRef = new MappedMethodReference(mappedSetter);
- findMappedPropertyType();
- }
- }