/*
* ModeShape (http://www.modeshape.org)
*
* Licensed 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.modeshape.common.util;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import org.modeshape.common.annotation.Category;
import org.modeshape.common.annotation.Description;
import org.modeshape.common.annotation.Immutable;
import org.modeshape.common.annotation.Label;
import org.modeshape.common.annotation.ReadOnly;
import org.modeshape.common.i18n.I18n;
import org.modeshape.common.text.Inflector;
/**
* Utility class for working reflectively with objects.
*/
@Immutable
public class Reflection {
/**
* Build the list of classes that correspond to the list of argument objects.
*
* @param arguments the list of argument objects.
* @return the list of Class instances that correspond to the list of argument objects; the resulting list will contain a null
* element for each null argument.
*/
public static Class<?>[] buildArgumentClasses( Object... arguments ) {
if (arguments == null || arguments.length == 0) return EMPTY_CLASS_ARRAY;
Class<?>[] result = new Class<?>[arguments.length];
int i = 0;
for (Object argument : arguments) {
if (argument != null) {
result[i] = argument.getClass();
} else {
result[i] = null;
}
}
return result;
}
/**
* Build the list of classes that correspond to the list of argument objects.
*
* @param arguments the list of argument objects.
* @return the list of Class instances that correspond to the list of argument objects; the resulting list will contain a null
* element for each null argument.
*/
public static List<Class<?>> buildArgumentClassList( Object... arguments ) {
if (arguments == null || arguments.length == 0) return Collections.emptyList();
List<Class<?>> result = new ArrayList<Class<?>>(arguments.length);
for (Object argument : arguments) {
if (argument != null) {
result.add(argument.getClass());
} else {
result.add(null);
}
}
return result;
}
/**
* Convert any argument classes to primitives.
*
* @param arguments the list of argument classes.
* @return the list of Class instances in which any classes that could be represented by primitives (e.g., Boolean) were
* replaced with the primitive classes (e.g., Boolean.TYPE).
*/
public static List<Class<?>> convertArgumentClassesToPrimitives( Class<?>... arguments ) {
if (arguments == null || arguments.length == 0) return Collections.emptyList();
List<Class<?>> result = new ArrayList<Class<?>>(arguments.length);
for (Class<?> clazz : arguments) {
if (clazz == Boolean.class) clazz = Boolean.TYPE;
else if (clazz == Character.class) clazz = Character.TYPE;
else if (clazz == Byte.class) clazz = Byte.TYPE;
else if (clazz == Short.class) clazz = Short.TYPE;
else if (clazz == Integer.class) clazz = Integer.TYPE;
else if (clazz == Long.class) clazz = Long.TYPE;
else if (clazz == Float.class) clazz = Float.TYPE;
else if (clazz == Double.class) clazz = Double.TYPE;
else if (clazz == Void.class) clazz = Void.TYPE;
result.add(clazz);
}
return result;
}
/**
* Returns the name of the class. The result will be the fully-qualified class name, or the readable form for arrays and
* primitive types.
*
* @param clazz the class for which the class name is to be returned.
* @return the readable name of the class.
*/
public static String getClassName( final Class<?> clazz ) {
final String fullName = clazz.getName();
final int fullNameLength = fullName.length();
// Check for array ('[') or the class/interface marker ('L') ...
int numArrayDimensions = 0;
while (numArrayDimensions < fullNameLength) {
final char c = fullName.charAt(numArrayDimensions);
if (c != '[') {
String name = null;
// Not an array, so it must be one of the other markers ...
switch (c) {
case 'L': {
name = fullName.subSequence(numArrayDimensions + 1, fullNameLength).toString();
break;
}
case 'B': {
name = "byte";
break;
}
case 'C': {
name = "char";
break;
}
case 'D': {
name = "double";
break;
}
case 'F': {
name = "float";
break;
}
case 'I': {
name = "int";
break;
}
case 'J': {
name = "long";
break;
}
case 'S': {
name = "short";
break;
}
case 'Z': {
name = "boolean";
break;
}
case 'V': {
name = "void";
break;
}
default: {
name = fullName.subSequence(numArrayDimensions, fullNameLength).toString();
}
}
if (numArrayDimensions == 0) {
// No array markers, so just return the name ...
return name;
}
// Otherwise, add the array markers and the name ...
if (numArrayDimensions < BRACKETS_PAIR.length) {
name = name + BRACKETS_PAIR[numArrayDimensions];
} else {
for (int i = 0; i < numArrayDimensions; i++) {
name = name + BRACKETS_PAIR[1];
}
}
return name;
}
++numArrayDimensions;
}
return fullName;
}
/**
* Sets the value of a field of an object instance via reflection
*
* @param instance to inspect
* @param fieldName name of field to set
* @param value the value to set
*/
public static void setValue(Object instance, String fieldName, Object value) {
try {
Field f = findFieldRecursively(instance.getClass(), fieldName);
if (f == null)
throw new NoSuchMethodException("Cannot find field " + fieldName + " on " + instance.getClass() + " or superclasses");
f.setAccessible(true);
f.set(instance, value);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* Retrieves the field with the given name from a class
*
* @param fieldName the field to retrieve
* @param objectClass the class from which to retrieve the field
* @return either a {@link Field} instance or {@code null} if no such field exists.
*/
public static Field getField(String fieldName, Class<?> objectClass) {
try {
return objectClass.getDeclaredField(fieldName);
} catch (NoSuchFieldException e) {
if (!objectClass.equals(Object.class)) {
return getField(fieldName, objectClass.getSuperclass());
} else {
return null;
}
}
}
/**
* Searches for a method with a given name in a class.
*
* @param type a {@link Class} instance; never null
* @param methodName the name of the method to search for; never null
* @return a {@link Method} instance if the method is found
*/
public static Method findMethod(Class<?> type, String methodName) {
try {
return type.getDeclaredMethod(methodName);
} catch (NoSuchMethodException e) {
if (type.equals(Object.class) || type.isInterface()) {
throw new RuntimeException(e);
}
return findMethod(type.getSuperclass(), methodName);
}
}
/**
* Searches for a given field recursively under a particular class
*
* @param c a {@link Class} instance, never null
* @param fieldName the name of the field, never null
* @return a {@link Field} instance if the field is located anywhere in the hierarchy or {@code null} if no such field exists
*/
public static Field findFieldRecursively(Class<?> c, String fieldName) {
Field f = null;
try {
f = c.getDeclaredField(fieldName);
} catch (NoSuchFieldException e) {
if (!c.equals(Object.class)) f = findFieldRecursively(c.getSuperclass(), fieldName);
}
return f;
}
/**
* Instantiates a class based on the class name provided. Instantiation is attempted via an appropriate, static
* factory method named <tt>getInstance()</tt> first, and failing the existence of an appropriate factory, falls
* back to an empty constructor.
* <p />
*
* @param classname class to instantiate
* @return an instance of classname
*/
@SuppressWarnings( "unchecked" )
public static <T> T getInstance(String classname, ClassLoader cl) {
if (classname == null) throw new IllegalArgumentException("Cannot load null class!");
Class<T> clazz = null;
try {
clazz = (Class<T>)Class.forName(classname, true, cl);
// first look for a getInstance() constructor
T instance = null;
try {
Method factoryMethod = getFactoryMethod(clazz);
if (factoryMethod != null) instance = (T) factoryMethod.invoke(null);
}
catch (Exception e) {
// no factory method or factory method failed. Try a constructor.
instance = null;
}
if (instance == null) {
instance = clazz.newInstance();
}
return instance;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private static Method getFactoryMethod(Class<?> c) {
for (Method m : c.getMethods()) {
if (m.getName().equals("getInstance") && m.getParameterTypes().length == 0 && Modifier.isStatic(m.getModifiers()))
return m;
}
return null;
}
/**
* Invokes a method using reflection, in an accessible manner (by using {@link Method#setAccessible(boolean)}
*
* @param instance instance on which to execute the method
* @param method method to execute
* @param parameters parameters
*/
public static Object invokeAccessibly(Object instance, Method method, Object[] parameters) {
try {
method.setAccessible(true);
return method.invoke(instance, parameters);
} catch (Exception e) {
throw new RuntimeException("Unable to invoke method " + method + " on object of type " + (instance == null ?
"null" :
instance.getClass().getSimpleName()) +
(parameters != null ? " with parameters " + Arrays.asList(parameters) : ""), e);
}
}
private static final Class<?>[] EMPTY_CLASS_ARRAY = new Class[] {};
private static final String[] BRACKETS_PAIR = new String[] {"", "[]", "[][]", "[][][]", "[][][][]", "[][][][][]"};
private final Class<?> targetClass;
private Map<String, LinkedList<Method>> methodMap = null; // used for the brute-force method finder
/**
* Construct a Reflection instance that cache's some information about the target class. The target class is the Class object
* upon which the methods will be found.
*
* @param targetClass the target class
* @throws IllegalArgumentException if the target class is null
*/
public Reflection( Class<?> targetClass ) {
CheckArg.isNotNull(targetClass, "targetClass");
this.targetClass = targetClass;
}
/**
* Return the class that is the target for the reflection methods.
*
* @return the target class
*/
public Class<?> getTargetClass() {
return this.targetClass;
}
/**
* Find the method on the target class that matches the supplied method name.
*
* @param methodName the name of the method that is to be found.
* @param caseSensitive true if the method name supplied should match case-sensitively, or false if case does not matter
* @return the Method objects that have a matching name, or an empty array if there are no methods that have a matching name.
*/
public Method[] findMethods( String methodName,
boolean caseSensitive ) {
Pattern pattern = caseSensitive ? Pattern.compile(methodName) : Pattern.compile(methodName, Pattern.CASE_INSENSITIVE);
return findMethods(pattern);
}
/**
* Find the methods on the target class that matches the supplied method name.
*
* @param methodNamePattern the regular expression pattern for the name of the method that is to be found.
* @return the Method objects that have a matching name, or an empty array if there are no methods that have a matching name.
*/
public Method[] findMethods( Pattern methodNamePattern ) {
final Method[] allMethods = this.targetClass.getMethods();
final List<Method> result = new ArrayList<Method>();
for (int i = 0; i < allMethods.length; i++) {
final Method m = allMethods[i];
if (methodNamePattern.matcher(m.getName()).matches()) {
result.add(m);
}
}
return result.toArray(new Method[result.size()]);
}
/**
* Find the getter methods on the target class that begin with "get" or "is", that have no parameters, and that return
* something other than void. This method skips the {@link Object#getClass()} method.
*
* @return the Method objects for the getters; never null but possibly empty
*/
public Method[] findGetterMethods() {
final Method[] allMethods = this.targetClass.getMethods();
final List<Method> result = new ArrayList<Method>();
for (int i = 0; i < allMethods.length; i++) {
final Method m = allMethods[i];
int numParams = m.getParameterTypes().length;
if (numParams != 0) continue;
String name = m.getName();
if ("getClass()".equals(name)) continue;
if (m.getReturnType() == Void.TYPE) continue;
if (name.startsWith("get") || name.startsWith("is") || name.startsWith("are")) {
result.add(m);
}
}
return result.toArray(new Method[result.size()]);
}
/**
* Find the property names with getter methods on the target class. This method returns the property names for the methods
* returned by {@link #findGetterMethods()}.
*
* @return the Java Bean property names for the getters; never null but possibly empty
*/
public String[] findGetterPropertyNames() {
final Method[] getters = findGetterMethods();
final List<String> result = new ArrayList<String>();
for (int i = 0; i < getters.length; i++) {
final Method m = getters[i];
String name = m.getName();
String propertyName = null;
if (name.startsWith("get") && name.length() > 3) {
propertyName = name.substring(3);
} else if (name.startsWith("is") && name.length() > 2) {
propertyName = name.substring(2);
} else if (name.startsWith("are") && name.length() > 3) {
propertyName = name.substring(3);
}
if (propertyName != null) {
propertyName = INFLECTOR.camelCase(INFLECTOR.underscore(propertyName), false);
result.add(propertyName);
}
}
return result.toArray(new String[result.size()]);
}
/**
* Find the method on the target class that matches the supplied method name.
*
* @param methodName the name of the method that is to be found.
* @param caseSensitive true if the method name supplied should match case-sensitively, or false if case does not matter
* @return the first Method object found that has a matching name, or null if there are no methods that have a matching name.
*/
public Method findFirstMethod( String methodName,
boolean caseSensitive ) {
Pattern pattern = caseSensitive ? Pattern.compile(methodName) : Pattern.compile(methodName, Pattern.CASE_INSENSITIVE);
return findFirstMethod(pattern);
}
/**
* Find the method on the target class that matches the supplied method name.
*
* @param methodNamePattern the regular expression pattern for the name of the method that is to be found.
* @return the first Method object found that has a matching name, or null if there are no methods that have a matching name.
*/
public Method findFirstMethod( Pattern methodNamePattern ) {
final Method[] allMethods = this.targetClass.getMethods();
for (int i = 0; i < allMethods.length; i++) {
final Method m = allMethods[i];
if (methodNamePattern.matcher(m.getName()).matches()) {
return m;
}
}
return null;
}
/**
* Finds the methods on the target class that match the supplied method name.
*
* @param methodName the name of the method that is to be found.
* @param caseSensitive true if the method name supplied should match case-sensitively, or false if case does not matter
* @return the Method objects that have a matching name, or empty if there are no methods that have a matching name.
*/
public Iterable<Method> findAllMethods( String methodName,
boolean caseSensitive ) {
Pattern pattern = caseSensitive ? Pattern.compile(methodName) : Pattern.compile(methodName, Pattern.CASE_INSENSITIVE);
return findAllMethods(pattern);
}
/**
* Finds the methods on the target class that match the supplied method name.
*
* @param methodNamePattern the regular expression pattern for the name of the method that is to be found.
* @return the Method objects that have a matching name, or empty if there are no methods that have a matching name.
*/
public Iterable<Method> findAllMethods( Pattern methodNamePattern ) {
LinkedList<Method> methods = new LinkedList<Method>();
final Method[] allMethods = this.targetClass.getMethods();
for (int i = 0; i < allMethods.length; i++) {
final Method m = allMethods[i];
if (methodNamePattern.matcher(m.getName()).matches()) {
methods.add(m);
}
}
return methods;
}
/**
* Find and execute the best method on the target class that matches the signature specified with one of the specified names
* and the list of arguments. If no such method is found, a NoSuchMethodException is thrown.
* <P>
* This method is unable to find methods with signatures that include both primitive arguments <i>and</i> arguments that are
* instances of <code>Number</code> or its subclasses.
* </p>
*
* @param methodNames the names of the methods that are to be invoked, in the order they are to be tried
* @param target the object on which the method is to be invoked
* @param arguments the array of Object instances that correspond to the arguments passed to the method.
* @return the Method object that references the method that satisfies the requirements, or null if no satisfactory method
* could be found.
* @throws NoSuchMethodException if a matching method is not found.
* @throws SecurityException if access to the information is denied.
* @throws InvocationTargetException
* @throws IllegalAccessException
* @throws IllegalArgumentException
*/
public Object invokeBestMethodOnTarget( String[] methodNames,
final Object target,
final Object... arguments )
throws NoSuchMethodException, SecurityException, IllegalArgumentException, IllegalAccessException,
InvocationTargetException {
Class<?>[] argumentClasses = buildArgumentClasses(arguments);
int remaining = methodNames.length;
Object result = null;
for (String methodName : methodNames) {
--remaining;
try {
final Method method = findBestMethodWithSignature(methodName, argumentClasses);
result = AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() {
@Override
public Object run() throws Exception {
return method.invoke(target, arguments);
}
});
break;
} catch (PrivilegedActionException pae) {
// pae will be wrapping one of IllegalAccessException, IllegalArgumentException, InvocationTargetException
if (pae.getException() instanceof IllegalAccessException) {
throw (IllegalAccessException)pae.getException();
}
if (pae.getException() instanceof IllegalArgumentException) {
throw (IllegalArgumentException)pae.getException();
}
if (pae.getException() instanceof InvocationTargetException) {
throw (InvocationTargetException)pae.getException();
}
} catch (NoSuchMethodException e) {
if (remaining == 0) throw e;
}
}
return result;
}
/**
* Find and execute the best setter method on the target class for the supplied property name and the supplied list of
* arguments. If no such method is found, a NoSuchMethodException is thrown.
* <P>
* This method is unable to find methods with signatures that include both primitive arguments <i>and</i> arguments that are
* instances of <code>Number</code> or its subclasses.
* </p>
*
* @param javaPropertyName the name of the property whose setter is to be invoked, in the order they are to be tried
* @param target the object on which the method is to be invoked
* @param argument the new value for the property
* @return the result of the setter method, which is typically null (void)
* @throws NoSuchMethodException if a matching method is not found.
* @throws SecurityException if access to the information is denied.
* @throws InvocationTargetException
* @throws IllegalAccessException
* @throws IllegalArgumentException
*/
public Object invokeSetterMethodOnTarget( String javaPropertyName,
Object target,
Object argument )
throws NoSuchMethodException, SecurityException, IllegalArgumentException, IllegalAccessException,
InvocationTargetException {
String[] methodNamesArray = findMethodNames("set" + javaPropertyName);
try {
return invokeBestMethodOnTarget(methodNamesArray, target, argument);
} catch (NoSuchMethodException e) {
// If the argument is an Object[], see if it works with an array of whatever type the actual value is ...
if (argument instanceof Object[]) {
Object[] arrayArg = (Object[])argument;
for (Object arrayValue : arrayArg) {
if (arrayValue == null) continue;
Class<?> arrayValueType = arrayValue.getClass();
// Create an array of this type ...
Object typedArray = Array.newInstance(arrayValueType, arrayArg.length);
Object[] newArray = (Object[])typedArray;
for (int i = 0; i != arrayArg.length; ++i) {
newArray[i] = arrayArg[i];
}
// Try to execute again ...
try {
return invokeBestMethodOnTarget(methodNamesArray, target, typedArray);
} catch (NoSuchMethodException e2) {
// Throw the original exception ...
throw e;
}
}
}
throw e;
}
}
/**
* Find and execute the getter method on the target class for the supplied property name. If no such method is found, a
* NoSuchMethodException is thrown.
*
* @param javaPropertyName the name of the property whose getter is to be invoked, in the order they are to be tried
* @param target the object on which the method is to be invoked
* @return the property value (the result of the getter method call)
* @throws NoSuchMethodException if a matching method is not found.
* @throws SecurityException if access to the information is denied.
* @throws InvocationTargetException
* @throws IllegalAccessException
* @throws IllegalArgumentException
*/
public Object invokeGetterMethodOnTarget( String javaPropertyName,
Object target )
throws NoSuchMethodException, SecurityException, IllegalArgumentException, IllegalAccessException,
InvocationTargetException {
String[] methodNamesArray = findMethodNames("get" + javaPropertyName);
if (methodNamesArray.length <= 0) {
// Try 'is' getter ...
methodNamesArray = findMethodNames("is" + javaPropertyName);
}
if (methodNamesArray.length <= 0) {
// Try 'are' getter ...
methodNamesArray = findMethodNames("are" + javaPropertyName);
}
return invokeBestMethodOnTarget(methodNamesArray, target);
}
protected String[] findMethodNames( String methodName ) {
Method[] methods = findMethods(methodName, false);
Set<String> methodNames = new HashSet<String>();
for (Method method : methods) {
String actualMethodName = method.getName();
methodNames.add(actualMethodName);
}
return methodNames.toArray(new String[methodNames.size()]);
}
/**
* Find the best method on the target class that matches the signature specified with the specified name and the list of
* arguments. This method first attempts to find the method with the specified arguments; if no such method is found, a
* NoSuchMethodException is thrown.
* <P>
* This method is unable to find methods with signatures that include both primitive arguments <i>and</i> arguments that are
* instances of <code>Number</code> or its subclasses.
*
* @param methodName the name of the method that is to be invoked.
* @param arguments the array of Object instances that correspond to the arguments passed to the method.
* @return the Method object that references the method that satisfies the requirements, or null if no satisfactory method
* could be found.
* @throws NoSuchMethodException if a matching method is not found.
* @throws SecurityException if access to the information is denied.
*/
public Method findBestMethodOnTarget( String methodName,
Object... arguments ) throws NoSuchMethodException, SecurityException {
Class<?>[] argumentClasses = buildArgumentClasses(arguments);
return findBestMethodWithSignature(methodName, argumentClasses);
}
/**
* Find the best method on the target class that matches the signature specified with the specified name and the list of
* argument classes. This method first attempts to find the method with the specified argument classes; if no such method is
* found, a NoSuchMethodException is thrown.
*
* @param methodName the name of the method that is to be invoked.
* @param argumentsClasses the list of Class instances that correspond to the classes for each argument passed to the method.
* @return the Method object that references the method that satisfies the requirements, or null if no satisfactory method
* could be found.
* @throws NoSuchMethodException if a matching method is not found.
* @throws SecurityException if access to the information is denied.
*/
public Method findBestMethodWithSignature( String methodName,
Class<?>... argumentsClasses ) throws NoSuchMethodException, SecurityException {
return findBestMethodWithSignature(methodName, true, argumentsClasses);
}
/**
* Find the best method on the target class that matches the signature specified with the specified name and the list of
* argument classes. This method first attempts to find the method with the specified argument classes; if no such method is
* found, a NoSuchMethodException is thrown.
*
* @param methodName the name of the method that is to be invoked.
* @param caseSensitive true if the method name supplied should match case-sensitively, or false if case does not matter
* @param argumentsClasses the list of Class instances that correspond to the classes for each argument passed to the method.
* @return the Method object that references the method that satisfies the requirements, or null if no satisfactory method
* could be found.
* @throws NoSuchMethodException if a matching method is not found.
* @throws SecurityException if access to the information is denied.
*/
public Method findBestMethodWithSignature( String methodName,
boolean caseSensitive,
Class<?>... argumentsClasses ) throws NoSuchMethodException, SecurityException {
// Attempt to find the method
Method result;
// -------------------------------------------------------------------------------
// First try to find the method with EXACTLY the argument classes as specified ...
// -------------------------------------------------------------------------------
Class<?>[] classArgs = null;
try {
classArgs = argumentsClasses != null ? argumentsClasses : new Class[] {};
result = this.targetClass.getMethod(methodName, classArgs); // this may throw an exception if not found
return result;
} catch (NoSuchMethodException e) {
// No method found, so continue ...
}
// ---------------------------------------------------------------------------------------------
// Then try to find a method with the argument classes converted to a primitive, if possible ...
// ---------------------------------------------------------------------------------------------
List<Class<?>> argumentsClassList = convertArgumentClassesToPrimitives(argumentsClasses);
try {
classArgs = argumentsClassList.toArray(new Class[argumentsClassList.size()]);
result = this.targetClass.getMethod(methodName, classArgs); // this may throw an exception if not found
return result;
} catch (NoSuchMethodException e) {
// No method found, so continue ...
}
// ---------------------------------------------------------------------------------------------
// Still haven't found anything. So far, the "getMethod" logic only finds methods that EXACTLY
// match the argument classes (i.e., not methods declared with superclasses or interfaces of
// the arguments). There is no canned algorithm in Java to do this, so we have to brute-force it.
// The following algorithm will find the first method that matches by doing "instanceof", so it
// may not be the best method. Since there is some overhead to this algorithm, the first time
// caches some information in class members.
// ---------------------------------------------------------------------------------------------
Method method;
LinkedList<Method> methodsWithSameName;
if (this.methodMap == null) {
// This is idempotent, so no need to lock or synchronize ...
this.methodMap = new HashMap<String, LinkedList<Method>>();
Method[] methods = this.targetClass.getMethods();
for (int i = 0; i != methods.length; ++i) {
method = methods[i];
methodsWithSameName = this.methodMap.get(method.getName());
if (methodsWithSameName == null) {
methodsWithSameName = new LinkedList<Method>();
this.methodMap.put(method.getName(), methodsWithSameName);
}
methodsWithSameName.addFirst(method); // add lower methods first
}
}
// ------------------------------------------------------------------------
// Find the set of methods with the same name (do this twice, once with the
// original methods and once with the primitives) ...
// ------------------------------------------------------------------------
// List argClass = argumentsClasses;
for (int j = 0; j != 2; ++j) {
if (caseSensitive) {
methodsWithSameName = this.methodMap.get(methodName);
} else {
methodsWithSameName = new LinkedList<Method>();
Pattern pattern = Pattern.compile(methodName, Pattern.CASE_INSENSITIVE);
for (Map.Entry<String, LinkedList<Method>> entry : this.methodMap.entrySet()) {
// entry.getKey() is the method name
if (pattern.matcher(entry.getKey()).matches()) {
methodsWithSameName.addAll(entry.getValue());
}
}
}
if (methodsWithSameName == null) {
throw new NoSuchMethodException(methodName);
}
Iterator<Method> iter = methodsWithSameName.iterator();
Class<?>[] args;
Class<?> clazz;
boolean allMatch;
while (iter.hasNext()) {
method = iter.next();
args = method.getParameterTypes();
if (args.length == argumentsClassList.size()) {
allMatch = true; // assume they all match
for (int i = 0; i < args.length; ++i) {
clazz = argumentsClassList.get(i);
if (clazz != null) {
Class<?> argClass = args[i];
if (argClass.isAssignableFrom(clazz)) {
// It's a match
} else if (argClass.isArray() && clazz.isArray()
&& argClass.getComponentType().isAssignableFrom(clazz.getComponentType())) {
// They're both arrays, and they're castable, so we're good ...
} else {
allMatch = false; // found one that doesn't match
i = args.length; // force completion
}
} else {
// a null is assignable for everything except a primitive
if (args[i].isPrimitive()) {
allMatch = false; // found one that doesn't match
i = args.length; // force completion
}
}
}
if (allMatch) {
return method;
}
}
}
}
throw new NoSuchMethodException(methodName);
}
/**
* Get the representation of the named property (with the supplied labe, category, description, and allowed values) on the
* target object.
* <p>
* If the label is not provided, this method looks for the {@link Label} annotation on the property's field and sets the label
* to the annotation's literal value, or if the {@link Label#i18n()} class is referenced, the localized value of the
* referenced {@link I18n}.
* </p>
* If the description is not provided, this method looks for the {@link Description} annotation on the property's field and
* sets the label to the annotation's literal value, or if the {@link Description#i18n()} class is referenced, the localized
* value of the referenced {@link I18n}. </p>
* <p>
* And if the category is not provided, this method looks for the {@link Category} annotation on the property's field and sets
* the label to the annotation's literal value, or if the {@link Category#i18n()} class is referenced, the localized value of
* the referenced {@link I18n}.
* </p>
*
* @param target the target on which the setter is to be called; may not be null
* @param propertyName the name of the Java object property; may not be null
* @param label the new label for the property; may be null
* @param category the category for this property; may be null
* @param description the description for the property; may be null if this is not known
* @param allowedValues the of allowed values, or null or empty if the values are not constrained
* @return the representation of the Java property; never null
* @throws NoSuchMethodException if a matching method is not found.
* @throws SecurityException if access to the information is denied.
* @throws IllegalAccessException if the setter method could not be accessed
* @throws InvocationTargetException if there was an error invoking the setter method on the target
* @throws IllegalArgumentException if 'target' is null, or if 'propertyName' is null or empty
*/
public Property getProperty( Object target,
String propertyName,
String label,
String category,
String description,
Object... allowedValues )
throws SecurityException, IllegalArgumentException, NoSuchMethodException, IllegalAccessException,
InvocationTargetException {
CheckArg.isNotNull(target, "target");
CheckArg.isNotEmpty(propertyName, "propertyName");
Method[] setters = findMethods("set" + propertyName, false);
boolean readOnly = setters.length < 1;
Class<?> type = Object.class;
Method[] getters = findMethods("get" + propertyName, false);
if (getters.length == 0) {
getters = findMethods("is" + propertyName, false);
}
if (getters.length == 0) {
getters = findMethods("are" + propertyName, false);
}
if (getters.length > 0) {
type = getters[0].getReturnType();
}
boolean inferred = true;
Field field = null;
try {
// Find the corresponding field ...
field = getField(targetClass, propertyName);
} catch (NoSuchFieldException e) {
// Nothing to do here
}
if (description == null) {
Description desc = getAnnotation(Description.class, field, getters, setters);
if (desc != null) {
description = localizedString(desc.i18n(), desc.value());
inferred = false;
}
}
if (label == null) {
Label labelAnnotation = getAnnotation(Label.class, field, getters, setters);
if (labelAnnotation != null) {
label = localizedString(labelAnnotation.i18n(), labelAnnotation.value());
inferred = false;
}
}
if (category == null) {
Category cat = getAnnotation(Category.class, field, getters, setters);
if (cat != null) {
category = localizedString(cat.i18n(), cat.value());
inferred = false;
}
}
if (!readOnly) {
ReadOnly readOnlyAnnotation = getAnnotation(ReadOnly.class, field, getters, setters);
if (readOnlyAnnotation != null) {
readOnly = true;
inferred = false;
}
}
Property property = new Property(propertyName, label, description, category, readOnly, type, allowedValues);
property.setInferred(inferred);
return property;
}
/**
* Get a Field intance for a given class and property. Iterate over super classes of a class when a <@link
* NoSuchFieldException> occurs until no more super classes are found then re-throw the <@link NoSuchFieldException>.
*
* @param targetClass
* @param propertyName
* @return Field
* @throws NoSuchFieldException
*/
protected Field getField( Class<?> targetClass,
String propertyName ) throws NoSuchFieldException {
Field field = null;
try {
field = targetClass.getDeclaredField(Inflector.getInstance().lowerCamelCase(propertyName));
} catch (NoSuchFieldException e) {
Class<?> clazz = targetClass.getSuperclass();
if (clazz != null) {
field = getField(clazz, propertyName);
} else {
throw e;
}
}
return field;
}
protected static <AnnotationType extends Annotation> AnnotationType getAnnotation( Class<AnnotationType> annotationType,
Field field,
Method[] getters,
Method[] setters ) {
AnnotationType annotation = null;
if (field != null) {
annotation = field.getAnnotation(annotationType);
}
if (annotation == null && getters != null) {
for (Method getter : getters) {
annotation = getter.getAnnotation(annotationType);
if (annotation != null) break;
}
}
if (annotation == null && setters != null) {
for (Method setter : setters) {
annotation = setter.getAnnotation(annotationType);
if (annotation != null) break;
}
}
return annotation;
}
protected static String localizedString( Class<?> i18nClass,
String id ) {
if (i18nClass != null && !Object.class.equals(i18nClass) && id != null) {
try {
// Look up the I18n field ...
Field i18nMsg = i18nClass.getDeclaredField(id);
I18n msg = (I18n)i18nMsg.get(null);
if (msg != null) {
return msg.text();
}
} catch (SecurityException err) {
// ignore
} catch (NoSuchFieldException err) {
// ignore
} catch (IllegalArgumentException err) {
// ignore
} catch (IllegalAccessException err) {
// ignore
}
}
return id;
}
/**
* Get the representation of the named property (with the supplied description) on the target object.
*
* @param target the target on which the setter is to be called; may not be null
* @param propertyName the name of the Java object property; may not be null
* @param description the description for the property; may be null if this is not known
* @return the representation of the Java property; never null
* @throws NoSuchMethodException if a matching method is not found.
* @throws SecurityException if access to the information is denied.
* @throws IllegalAccessException if the setter method could not be accessed
* @throws InvocationTargetException if there was an error invoking the setter method on the target
* @throws IllegalArgumentException if 'target' is null, or if 'propertyName' is null or empty
*/
public Property getProperty( Object target,
String propertyName,
String description )
throws SecurityException, IllegalArgumentException, NoSuchMethodException, IllegalAccessException,
InvocationTargetException {
CheckArg.isNotNull(target, "target");
CheckArg.isNotEmpty(propertyName, "propertyName");
return getProperty(target, propertyName, null, null, description);
}
/**
* Get the representation of the named property on the target object.
*
* @param target the target on which the setter is to be called; may not be null
* @param propertyName the name of the Java object property; may not be null
* @return the representation of the Java property; never null
* @throws NoSuchMethodException if a matching method is not found.
* @throws SecurityException if access to the information is denied.
* @throws IllegalAccessException if the setter method could not be accessed
* @throws InvocationTargetException if there was an error invoking the setter method on the target
* @throws IllegalArgumentException if 'target' is null, or if 'propertyName' is null or empty
*/
public Property getProperty( Object target,
String propertyName )
throws SecurityException, IllegalArgumentException, NoSuchMethodException, IllegalAccessException,
InvocationTargetException {
CheckArg.isNotNull(target, "target");
CheckArg.isNotEmpty(propertyName, "propertyName");
return getProperty(target, propertyName, null, null, null);
}
/**
* Get representations for all of the Java properties on the supplied object.
*
* @param target the target on which the setter is to be called; may not be null
* @return the list of all properties; never null
* @throws NoSuchMethodException if a matching method is not found.
* @throws SecurityException if access to the information is denied.
* @throws IllegalAccessException if the setter method could not be accessed
* @throws InvocationTargetException if there was an error invoking the setter method on the target
* @throws IllegalArgumentException if 'target' is null, or if 'propertyName' is null or empty
*/
public List<Property> getAllPropertiesOn( Object target )
throws SecurityException, IllegalArgumentException, NoSuchMethodException, IllegalAccessException,
InvocationTargetException {
String[] propertyNames = findGetterPropertyNames();
List<Property> results = new ArrayList<Property>(propertyNames.length);
for (String propertyName : propertyNames) {
if ("class".equals(propertyName)) continue;
Property prop = getProperty(target, propertyName);
results.add(prop);
}
Collections.sort(results);
return results;
}
/**
* Get representations for all of the Java properties on the supplied object.
*
* @param target the target on which the setter is to be called; may not be null
* @return the map of all properties keyed by their name; never null
* @throws NoSuchMethodException if a matching method is not found.
* @throws SecurityException if access to the information is denied.
* @throws IllegalAccessException if the setter method could not be accessed
* @throws InvocationTargetException if there was an error invoking the setter method on the target
* @throws IllegalArgumentException if 'target' is null, or if 'propertyName' is null or empty
*/
public Map<String, Property> getAllPropertiesByNameOn( Object target )
throws SecurityException, IllegalArgumentException, NoSuchMethodException, IllegalAccessException,
InvocationTargetException {
String[] propertyNames = findGetterPropertyNames();
Map<String, Property> results = new HashMap<String, Property>();
for (String propertyName : propertyNames) {
if ("class".equals(propertyName)) continue;
Property prop = getProperty(target, propertyName);
results.put(prop.getName(), prop);
}
return results;
}
/**
* Set the property on the supplied target object to the specified value.
*
* @param target the target on which the setter is to be called; may not be null
* @param property the property that is to be set on the target
* @param value the new value for the property
* @throws NoSuchMethodException if a matching method is not found.
* @throws SecurityException if access to the information is denied.
* @throws IllegalAccessException if the setter method could not be accessed
* @throws InvocationTargetException if there was an error invoking the setter method on the target
* @throws IllegalArgumentException if 'target' is null, 'property' is null, or 'property.getName()' is null
*/
public void setProperty( Object target,
Property property,
Object value )
throws SecurityException, IllegalArgumentException, NoSuchMethodException, IllegalAccessException,
InvocationTargetException {
CheckArg.isNotNull(target, "target");
CheckArg.isNotNull(property, "property");
CheckArg.isNotNull(property.getName(), "property.getName()");
invokeSetterMethodOnTarget(property.getName(), target, value);
}
/**
* Get current value for the property on the supplied target object.
*
* @param target the target on which the setter is to be called; may not be null
* @param property the property that is to be set on the target
* @return the current value for the property; may be null
* @throws NoSuchMethodException if a matching method is not found.
* @throws SecurityException if access to the information is denied.
* @throws IllegalAccessException if the setter method could not be accessed
* @throws InvocationTargetException if there was an error invoking the setter method on the target
* @throws IllegalArgumentException if 'target' is null, 'property' is null, or 'property.getName()' is null
*/
public Object getProperty( Object target,
Property property )
throws SecurityException, IllegalArgumentException, NoSuchMethodException, IllegalAccessException,
InvocationTargetException {
CheckArg.isNotNull(target, "target");
CheckArg.isNotNull(property, "property");
CheckArg.isNotNull(property.getName(), "property.getName()");
return invokeGetterMethodOnTarget(property.getName(), target);
}
/**
* Get current value represented as a string for the property on the supplied target object.
*
* @param target the target on which the setter is to be called; may not be null
* @param property the property that is to be set on the target
* @return the current value for the property; may be null
* @throws NoSuchMethodException if a matching method is not found.
* @throws SecurityException if access to the information is denied.
* @throws IllegalAccessException if the setter method could not be accessed
* @throws InvocationTargetException if there was an error invoking the setter method on the target
* @throws IllegalArgumentException if 'target' is null, 'property' is null, or 'property.getName()' is null
*/
public String getPropertyAsString( Object target,
Property property )
throws SecurityException, IllegalArgumentException, NoSuchMethodException, IllegalAccessException,
InvocationTargetException {
Object value = getProperty(target, property);
StringBuilder sb = new StringBuilder();
writeObjectAsString(value, sb, false);
return sb.toString();
}
protected void writeObjectAsString( Object obj,
StringBuilder sb,
boolean wrapWithBrackets ) {
if (obj == null) {
sb.append("null");
return;
}
if (obj.getClass().isArray()) {
Object[] array = (Object[])obj;
boolean first = true;
if (wrapWithBrackets) sb.append("[");
for (Object value : array) {
if (first) first = false;
else sb.append(", ");
writeObjectAsString(value, sb, true);
}
if (wrapWithBrackets) sb.append("]");
return;
}
sb.append(obj);
}
protected static final Inflector INFLECTOR = Inflector.getInstance();
/**
* A representation of a property on a Java object.
*/
public static class Property implements Comparable<Property>, Serializable {
private static final long serialVersionUID = 1L;
private String name;
private String label;
private String description;
private Object value;
private Collection<?> allowedValues;
private Class<?> type;
private boolean readOnly;
private String category;
private boolean inferred;
/**
* Create a new object property that has no fields initialized.
*/
public Property() {
}
/**
* Create a new object property with the supplied parameters set.
*
* @param name the property name; may be null
* @param label the human-readable property label; may be null
* @param description the description for this property; may be null
* @param readOnly true if the property is read-only, or false otherwise
*/
public Property( String name,
String label,
String description,
boolean readOnly ) {
this(name, label, description, null, readOnly, null);
}
/**
* Create a new object property with the supplied parameters set.
*
* @param name the property name; may be null
* @param label the human-readable property label; may be null
* @param description the description for this property; may be null
* @param category the category for this property; may be null
* @param readOnly true if the property is read-only, or false otherwise
* @param type the value class; may be null
* @param allowedValues the array of allowed values, or null or empty if the values are not constrained
*/
public Property( String name,
String label,
String description,
String category,
boolean readOnly,
Class<?> type,
Object... allowedValues ) {
setName(name);
if (label != null) setLabel(label);
if (description != null) setDescription(description);
setCategory(category);
setReadOnly(readOnly);
setType(type);
setAllowedValues(allowedValues);
}
/**
* Get the property name in camel case. The getter method is simply "get" followed by the name of the property (with the
* first character of the property converted to uppercase). The setter method is "set" (or "is" for boolean properties)
* followed by the name of the property (with the first character of the property converted to uppercase).
*
* @return the property name; never null, but possibly empty
*/
public String getName() {
return name != null ? name : "";
}
/**
* Set the property name in camel case. The getter method is simply "get" followed by the name of the property (with the
* first character of the property converted to uppercase). The setter method is "set" (or "is" for boolean properties)
* followed by the name of the property (with the first character of the property converted to uppercase).
*
* @param name the nwe property name; may be null
*/
public void setName( String name ) {
this.name = name;
if (this.label == null) setLabel(null);
}
/**
* Get the human-readable label for the property. This is often just a {@link Inflector#humanize(String, String...)
* humanized} form of the {@link #getName() property name}.
*
* @return label the human-readable property label; never null, but possibly empty
*/
public String getLabel() {
return label != null ? label : "";
}
/**
* Set the human-readable label for the property. If null, this will be set to the
* {@link Inflector#humanize(String, String...) humanized} form of the {@link #getName() property name}.
*
* @param label the new label for the property; may be null
*/
public void setLabel( String label ) {
if (label == null && name != null) {
label = INFLECTOR.titleCase(INFLECTOR.humanize(INFLECTOR.underscore(name)));
}
this.label = label;
}
/**
* Get the description for this property.
*
* @return the description; never null, but possibly empty
*/
public String getDescription() {
return description != null ? description : "";
}
/**
* Set the description for this property.
*
* @param description the new description for this property; may be null
*/
public void setDescription( String description ) {
this.description = description;
}
/**
* Return whether this property is read-only.
*
* @return true if the property is read-only, or false otherwise
*/
public boolean isReadOnly() {
return readOnly;
}
/**
* Set whether this property is read-only.
*
* @param readOnly true if the property is read-only, or false otherwise
*/
public void setReadOnly( boolean readOnly ) {
this.readOnly = readOnly;
}
/**
* Get the name of the category in which this property belongs.
*
* @return the category name; never null, but possibly empty
*/
public String getCategory() {
return category != null ? category : "";
}
/**
* Set the name of the category in which this property belongs.
*
* @param category the category name; may be null
*/
public void setCategory( String category ) {
this.category = category;
}
/**
* Get the class to which the value must belong (excluding null values).
*
* @return the value class; never null, but may be {@link Object Object.class}
*/
public Class<?> getType() {
return type;
}
/**
* Set the class to which the value must belong (excluding null values).
*
* @param type the value class; may be null
*/
public void setType( Class<?> type ) {
this.type = type != null ? type : Object.class;
}
/**
* Determine if this is a boolean property (the {@link #getType() type} is a {@link Boolean} or boolean).
*
* @return true if this is a boolean property, or false otherwise
*/
public boolean isBooleanType() {
return Boolean.class.equals(type) || Boolean.TYPE.equals(type);
}
/**
* Determine if this is property's (the {@link #getType() type} is a primitive.
*
* @return true if this property's type is a primitive, or false otherwise
*/
public boolean isPrimitive() {
return type.isPrimitive();
}
/**
* Determine if this is property's (the {@link #getType() type} is an array.
*
* @return true if this property's type is an array, or false otherwise
*/
public boolean isArrayType() {
return type.isArray();
}
/**
* Get the allowed values for this property. If this is non-null and non-empty, the value must be one of these values.
*
* @return collection of allowed values, or the empty set if the values are not constrained
*/
public Collection<?> getAllowedValues() {
return allowedValues != null ? allowedValues : Collections.emptySet();
}
/**
* Set the allowed values for this property. If this is non-null and non-empty, the value is expected to be one of these
* values.
*
* @param allowedValues the collection of allowed values, or null or empty if the values are not constrained
*/
public void setAllowedValues( Collection<?> allowedValues ) {
this.allowedValues = allowedValues;
}
/**
* Set the allowed values for this property. If this is non-null and non-empty, the value is expected to be one of these
* values.
*
* @param allowedValues the array of allowed values, or null or empty if the values are not constrained
*/
public void setAllowedValues( Object... allowedValues ) {
if (allowedValues != null && allowedValues.length != 0) {
this.allowedValues = new ArrayList<Object>(Arrays.asList(allowedValues));
} else {
this.allowedValues = null;
}
}
/**
* Return whether this property was inferred purely by reflection, or whether annotations were used for its definition.
*
* @return true if it was inferred only by reflection, or false if at least one annotation was found and used
*/
public boolean isInferred() {
return inferred;
}
/**
* Set whether this property was inferred purely by reflection.
*
* @param inferred true if it was inferred only by reflection, or false if at least one annotation was found and used
*/
public void setInferred( boolean inferred ) {
this.inferred = inferred;
}
/**
* {@inheritDoc}
*
* @see java.lang.Comparable#compareTo(java.lang.Object)
*/
@Override
public int compareTo( Property that ) {
if (this == that) return 0;
if (that == null) return 1;
int diff = ObjectUtil.compareWithNulls(this.category, that.category);
if (diff != 0) return diff;
diff = ObjectUtil.compareWithNulls(this.label, that.label);
if (diff != 0) return diff;
diff = ObjectUtil.compareWithNulls(this.name, that.name);
if (diff != 0) return diff;
return 0;
}
/**
* {@inheritDoc}
*
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return HashCode.compute(this.category, this.name, this.label);
}
/**
* {@inheritDoc}
*
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals( Object obj ) {
if (obj == this) return true;
if (obj instanceof Property) {
Property that = (Property)obj;
if (!ObjectUtil.isEqualWithNulls(this.category, that.category)) return false;
if (!ObjectUtil.isEqualWithNulls(this.label, that.label)) return false;
if (!ObjectUtil.isEqualWithNulls(this.name, that.name)) return false;
if (!ObjectUtil.isEqualWithNulls(this.value, that.value)) return false;
if (!ObjectUtil.isEqualWithNulls(this.readOnly, that.readOnly)) return false;
return true;
}
return false;
}
/**
* {@inheritDoc}
*
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
if (name != null) sb.append(name).append(" = ");
sb.append(value);
sb.append(" ( ");
sb.append(readOnly ? "readonly " : "writable ");
if (category != null) sb.append("category=\"").append(category).append("\" ");
if (label != null) sb.append("label=\"").append(label).append("\" ");
if (description != null) sb.append("description=\"").append(description).append("\" ");
sb.append(")");
return sb.toString();
}
}
}