/* | |
* 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 { | |
/** | |
* 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; | |
/** | |
* The parameter types array for the reader method signature. | |
*/ | |
private static final Class<?>[] STRING_CLASS_PARAMETER = new Class[]{String.class}; | |
/** | |
* 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.length() == 0) { | |
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.length() == 0) { | |
throw new IntrospectionException("bad property name: " + | |
propertyName); | |
} | |
setName(propertyName); | |
// search the mapped get and set methods | |
Method mappedReadMethod = null; | |
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 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.length() == 0) { | |
throw new IntrospectionException("bad property name: " + | |
propertyName); | |
} | |
setName(propertyName); | |
mappedReadMethodRef = new MappedMethodReference(mappedGetter); | |
mappedWriteMethodRef = new MappedMethodReference(mappedSetter); | |
findMappedPropertyType(); | |
} | |
/** | |
* 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(); | |
} | |
/** | |
* 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(); | |
} | |
/** | |
* 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 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(); | |
} | |
/** | |
* Introspect our bean class to identify the corresponding getter | |
* and setter methods. | |
*/ | |
private void findMappedPropertyType() throws IntrospectionException { | |
try { | |
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); | |
} catch (final IntrospectionException ex) { | |
throw ex; | |
} | |
} | |
/** | |
* Return a capitalized version of the specified property name. | |
* | |
* @param s The property name | |
*/ | |
private static String capitalizePropertyName(final String s) { | |
if (s.length() == 0) { | |
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 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; | |
} | |
/** | |
* 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 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."); | |
} | |
/** | |
* 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 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; | |
} | |
} | |
} | |
} |