/*
* Copyright (c) 2012-2017, Inversoft Inc., All Rights Reserved
*
* 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.primeframework.mvc.util;
import java.beans.Introspector;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import org.primeframework.mvc.parameter.el.BeanExpressionException;
import org.primeframework.mvc.parameter.el.CollectionExpressionException;
import org.primeframework.mvc.parameter.el.ExpressionException;
import org.primeframework.mvc.parameter.el.ReadExpressionException;
import org.primeframework.mvc.parameter.el.UpdateExpressionException;
/**
* Provides support for reflection, bean properties and field access.
*
* @author Brian Pontarelli
*/
@SuppressWarnings("unchecked")
public class ReflectionUtils {
private static final Map<Class<?>, Map<String, Field>> fieldCache = new WeakHashMap<>();
private static final Map<Class<?>, Method[]> methods = new WeakHashMap<>();
private static final Map<Class<?>, Map<String, PropertyInfo>> propertyCache = new WeakHashMap<>();
private static final Map<String, MethodInformationExtractor> verifiers = new HashMap<>();
static {
verifiers.put("is", new GetMethodInformationExtractor());
verifiers.put("get", new GetMethodInformationExtractor());
verifiers.put("set", new SetMethodInformationExtractor());
}
/**
* Return true if any of the provided annotations are provided on the field.
*
* @param field The field
* @param annotations a list of annotations to look for
* @return true if any of the provided annotations are present.
*/
public static boolean areAnyAnnotationsPresent(Field field, List<Class<? extends Annotation>> annotations) {
for (Class<? extends Annotation> annotation : annotations) {
if (field.isAnnotationPresent(annotation)) {
return true;
}
}
return false;
}
/**
* Finds all of the fields that have the given annotation on the given Object.
*
* @param type The class to find fields from.
* @param annotation The annotation.
*/
public static List<Field> findAllFieldsWithAnnotation(Class<?> type, Class<? extends Annotation> annotation) {
Map<String, Field> fields = findFields(type);
List<Field> fieldList = new ArrayList<>();
for (Field field : fields.values()) {
if (field.isAnnotationPresent(annotation)) {
fieldList.add(field);
}
}
return fieldList;
}
/**
* Finds all of the fields that have the given annotation on the given Object.
*
* @param type The class to find fields from.
* @param annotations The annotations.
*/
public static List<Field> findAllFieldsWithAnnotations(Class<?> type, List<Class<? extends Annotation>> annotations) {
Map<String, Field> fields = findFields(type);
List<Field> fieldList = new ArrayList<>();
for (Field field : fields.values()) {
for (Class<? extends Annotation> annotation : annotations) {
if (field.isAnnotationPresent(annotation)) {
fieldList.add(field);
break;
}
}
}
return fieldList;
}
/**
* Pulls all of the fields and java bean properties from the given Class and returns the names.
*
* @param type The Class to pull the names from.
* @return The names of all the fields and java bean properties.
*/
public static Set<String> findAllMembers(Class<?> type) {
Map<String, Field> fields = findFields(type);
Map<String, PropertyInfo> map = findPropertyInfo(type);
// Favor properties by adding fields first
Set<String> names = new HashSet<>(fields.keySet());
names.addAll(map.keySet());
return names;
}
/**
* Locates all of the members (fields and JavaBean properties) that have the given annotation and returns the name of
* the member and the annotation itself.
*
* @param type The class to find the member annotations from.
* @param annotation The annotation type.
* @param <T> The annotation type.
* @return A map of members to annotations.
*/
public static <T extends Annotation> Map<String, T> findAllMembersWithAnnotation(Class<?> type, Class<T> annotation) {
Map<String, T> annotations = new HashMap<>();
List<Field> fields = findAllFieldsWithAnnotation(type, annotation);
for (Field field : fields) {
annotations.put(field.getName(), field.getAnnotation(annotation));
}
Map<String, PropertyInfo> properties = findPropertyInfo(type);
for (String property : properties.keySet()) {
Map<String, Method> methods = properties.get(property).getMethods();
for (Method method : methods.values()) {
if (method.isAnnotationPresent(annotation)) {
annotations.put(property, method.getAnnotation(annotation));
break;
}
}
}
return annotations;
}
/**
* Finds all of the methods that have the given annotation on the given Object.
*
* @param type The class to find methods from.
* @param annotation The annotation.
*/
public static List<Method> findAllMethodsWithAnnotation(Class<?> type, Class<? extends Annotation> annotation) {
Method[] methods = findMethods(type);
List<Method> methodList = new ArrayList<>();
for (Method method : methods) {
if (method.isAnnotationPresent(annotation)) {
methodList.add(method);
}
}
return methodList;
}
/**
* Loads or fetches from the cache a Map of {@link Field} objects keyed into the Map by the field name they correspond
* to.
*
* @param type The class to grab the fields from.
* @return The Map, which could be null if the class has no fields.
*/
public static Map<String, Field> findFields(Class<?> type) {
Map<String, Field> fieldMap;
synchronized (fieldCache) {
// Otherwise look for the property Map or create and store
fieldMap = fieldCache.get(type);
if (fieldMap != null) {
return fieldMap;
}
fieldMap = new HashMap<>();
Field[] fields = type.getFields();
for (Field field : fields) {
fieldMap.put(field.getName(), field);
}
fieldCache.put(type, Collections.unmodifiableMap(fieldMap));
}
return fieldMap;
}
/**
* Loads and caches the methods of the given Class in an order array. The order of this array is that methods defined
* in superclasses are in the array first, followed by methods in the given type. The deeper the superclass, the
* earlier the methods are in the array.
*
* @param type The class.
* @return The methods.
*/
public static Method[] findMethods(final Class<?> type) {
synchronized (methods) {
Method[] array = methods.get(type);
if (array == null) {
array = type.getMethods();
methods.put(type, array);
Arrays.sort(array, new Comparator<Method>() {
@Override
public int compare(Method method1, Method method2) {
int depth1 = depth(method1, type);
int depth2 = depth(method2, type);
if (depth1 == depth2) {
return method1.getName().compareTo(method2.getName());
}
return depth2 - depth1;
}
public int depth(Method method, Class<?> type) {
int depth = 0;
Class<?> declaringType = method.getDeclaringClass();
while (declaringType != type && type != null) {
type = type.getSuperclass();
depth++;
}
return depth;
}
});
}
return array;
}
}
/**
* Loads or fetches from the cache a Map of {@link PropertyInfo} objects keyed into the Map by the property name they
* correspond to.
*
* @param type The class to grab the property map from.
* @return The Map, which could be empty if the class has no properties.
*/
public static Map<String, PropertyInfo> findPropertyInfo(Class<?> type) {
Map<String, PropertyInfo> propMap;
synchronized (propertyCache) {
// Otherwise look for the property Map or create and store
propMap = propertyCache.get(type);
if (propMap != null) {
return propMap;
}
propMap = new HashMap<>();
Set<String> errors = new HashSet<>();
Method[] methods = findMethods(type);
for (Method method : methods) {
// Skip bridge methods (covariant or generics) because the non-bridge method is the one that should be correct
if (method.isBridge()) {
continue;
}
PropertyName name = getPropertyNames(method);
if (name == null) {
continue;
}
PropertyInfo info = propMap.get(name.getName());
boolean constructed = false;
if (info == null) {
info = new PropertyInfo();
info.setName(name.getName());
info.setDefiningClass(type);
constructed = true;
}
// Unify get and is
String prefix = name.getPrefix();
if (prefix.equals("is")) {
prefix = "get";
}
Method existingMethod = info.getMethods().get(prefix);
if (existingMethod != null) {
errors.add("Two or more [" + prefix + "] methods exist in the class [" + type + "] and Prime can't determine which to call");
continue;
}
MethodInformationExtractor verifier = verifiers.get(prefix);
if (verifier == null) {
continue;
}
info.getMethods().put(prefix, method);
info.setGenericType(verifier.determineGenericType(method));
info.setType(verifier.determineType(method));
info.setIndexed(verifier.isIndexed(method));
if (constructed) {
propMap.put(name.getName(), info);
}
}
// Check for property errors
for (PropertyInfo info : propMap.values()) {
Method read = info.getMethods().get("get");
Method write = info.getMethods().get("set");
if (read != null && isValidGetter(read)) {
if (info.isIndexed()) {
errors.add("Invalid property named [" + info.getName() + "]. It mixes indexed and normal JavaBean methods.");
}
} else if (read != null && isValidIndexedGetter(read)) {
if (!info.isIndexed() && write != null) {
errors.add("Invalid property named [" + info.getName() + "]. It mixes indexed and normal JavaBean methods.");
}
} else if (read != null) {
errors.add("Invalid getter method for property named [" + info.getName() + "]");
}
if (write != null && isValidSetter(write)) {
if (info.isIndexed()) {
errors.add("Invalid property named [" + info.getName() + "]. It mixes indexed and normal JavaBean methods.");
}
} else if (write != null && isValidIndexedSetter(write)) {
if (!info.isIndexed() && read != null) {
errors.add("Invalid property named [" + info.getName() + "]. It mixes indexed and normal JavaBean methods.");
}
} else if (write != null) {
errors.add("Invalid setter method for property named [" + info.getName() + "]");
}
if (read != null && write != null &&
((info.isIndexed() && read.getReturnType() != write.getParameterTypes()[1]) ||
(!info.isIndexed() && read.getReturnType() != write.getParameterTypes()[0]))) {
errors.add("Invalid getter/setter pair for JavaBean property named [" + info.getName() + "] in class [" +
write.getDeclaringClass() + "]. The return type and parameter types must be identical");
}
}
if (errors.size() > 0) {
throw new BeanExpressionException("Invalid JavaBean class [" + type + "]. Errors are: \n" + errors);
}
propertyCache.put(type, Collections.unmodifiableMap(propMap));
}
return propMap;
}
/**
* This handles fetching a field value.
*
* @param field The field to get.
* @param object The object to get he field from.
* @return The value of the field.
* @throws ExpressionException If any mishap occurred whilst Reflecting sire. All the exceptions that could be thrown
* whilst invoking will be wrapped inside the ReflectionException.
*/
public static Object getField(Field field, Object object) throws ExpressionException {
try {
// I think we have a winner
return field.get(object);
} catch (IllegalAccessException iae) {
throw new ReadExpressionException("Illegal access for field [" + field + "]", iae);
} catch (IllegalArgumentException iare) {
throw new ReadExpressionException("Illegal argument for field [" + field + "]", iare);
}
}
/**
* Determines the type of the given member (field or proprty).
*
* @param type The class.
* @param member The member name.
* @return The type.
*/
public static Class<?> getMemberType(Class<?> type, String member) {
Field field = findFields(type).get(member);
if (field != null) {
return field.getType();
}
PropertyInfo propertyInfo = findPropertyInfo(type).get(member);
if (propertyInfo != null) {
return propertyInfo.getType();
}
return null;
}
/**
* Invokes the given method on the given class and handles propagation of runtime exceptions.
*
* @param method The method to invoke.
* @param obj The object to invoke the methods on.
* @param params The parameters passed to the method.
* @return The return from the method invocation.
*/
public static <T> T invoke(Method method, Object obj, Object... params) {
try {
return (T) method.invoke(obj, params);
} catch (IllegalAccessException e) {
throw new ExpressionException("Unable to call method [" + method + "] because it isn't accessible", e);
} catch (IllegalArgumentException e) {
throw new ExpressionException("Unable to call method [" + method + "] because the incorrect parameters were passed to it", e);
} catch (InvocationTargetException e) {
Throwable target = e.getTargetException();
if (target instanceof RuntimeException) {
throw (RuntimeException) target;
}
if (target instanceof Error) {
throw (Error) target;
}
throw new ExpressionException("Unable to call method [" + method + "]", e);
}
}
/**
* Invokes all the given methods on the given object.
*
* @param obj The object to invoke the methods on.
* @param methods The methods to invoke.
*/
public static void invokeAll(Object obj, List<Method> methods) {
for (Method method : methods) {
try {
method.invoke(obj);
} catch (IllegalAccessException e) {
throw new ExpressionException("Unable to call method [" + method + "] because it isn't accessible", e);
} catch (InvocationTargetException e) {
Throwable target = e.getTargetException();
if (target instanceof RuntimeException) {
throw (RuntimeException) target;
}
throw new ExpressionException("Unable to call method [" + method + "]", e);
}
}
}
/**
* This handles invoking the getter method.
*
* @param method The method to invoke.
* @param object The object to invoke the method on.
* @return The return value of the method.
* @throws RuntimeException If the target of the InvocationTargetException is a RuntimeException, in which case, it is
* re-thrown.
* @throws Error If the target of the InvocationTargetException is an Error, in which case, it is
* re-thrown.
*/
public static Object invokeGetter(Method method, Object object) throws RuntimeException, Error {
return invoke(method, object);
}
/**
* This handles invoking the setter method and also will handle a single special case where the setter method takes a
* single object and the value is a collection with a single value.
*
* @param method The method to invoke.
* @param object The object to invoke the method on.
* @param value The value to set into the method.
* @throws RuntimeException If the target of the InvocationTargetException is a RuntimeException, in which case, it is
* re-thrown.
* @throws Error If the target of the InvocationTargetException is an Error, in which case, it is
* re-thrown.
*/
public static void invokeSetter(Method method, Object object, Object value) throws RuntimeException, Error {
Class[] types = method.getParameterTypes();
if (types.length != 1) {
throw new UpdateExpressionException("Invalid method [" + method + "] it should take a single parameter");
}
Class type = types[0];
if (!type.isInstance(value) && Collection.class.isInstance(value)) {
// Handle the Collection special case
Collection c = (Collection) value;
if (c.size() == 1) {
value = c.iterator().next();
} else {
throw new ExpressionException("Cannot set a Collection that contains multiple values into the method [" +
method + "] which is not a collection.");
}
}
invoke(method, object, value);
}
/**
* Check if the method is a proper java bean getter-property method. This means that it starts with get, has the form
* getFoo or getFOO, has no parameters and returns a non-void value.
*
* @param method The method to check.
* @return True if valid, false otherwise.
*/
public static boolean isValidGetter(Method method) {
return (method.getParameterTypes().length == 0 && method.getReturnType() != Void.TYPE);
}
/**
* Check if the method is a proper java bean indexed getter method. This means that it starts with get, has the form
* getFoo or getFOO, has one parameter, an indices, and returns a non-void value.
*
* @param method The method to check.
* @return True if valid, false otherwise.
*/
public static boolean isValidIndexedGetter(Method method) {
return (method.getParameterTypes().length == 1 && method.getReturnType() != Void.TYPE);
}
/**
* Check if the method is a proper java bean indexed setter method. This means that it starts with set, has the form
* setFoo or setFOO, takes a two parameters, an indices and a value.
*
* @param method The method to check.
* @return True if valid, false otherwise.
*/
public static boolean isValidIndexedSetter(Method method) {
return (method.getParameterTypes().length == 2);
}
/**
* Check if the method is a proper java bean setter-property method. This means that it starts with set, has the form
* setFoo or setFOO, takes a single parameter.
*
* @param method The method to check.
* @return True if valid, false otherwise.
*/
public static boolean isValidSetter(Method method) {
return (method.getParameterTypes().length == 1);
}
/**
* This handles setting a value on a field and also will handle a single special case where the setter method takes a
* single object and the value is a collection with a single value.
*
* @param field The field to set.
* @param object The object to set the field on.
* @param value The value to set into the field.
* @throws ExpressionException If any mishap occurred whilst Reflecting sire. All the exceptions that could be thrown
* whilst invoking will be wrapped inside the ReflectionException.
*/
public static void setField(Field field, Object object, Object value) throws ExpressionException {
Class type = field.getType();
if (!type.isInstance(value) && Collection.class.isInstance(value)) {
// Handle the Collection special case
Collection c = (Collection) value;
if (c.size() == 1) {
value = c.iterator().next();
} else {
throw new CollectionExpressionException("Cannot set a Collection that contains multiple values into the field [" +
field + "] which is not a collection.");
}
}
try {
// I think we have a winner
field.set(object, value);
} catch (IllegalAccessException iae) {
throw new UpdateExpressionException("Illegal access for field [" + field + "]", iae);
} catch (IllegalArgumentException iare) {
throw new UpdateExpressionException("Illegal argument for field [" + field + "]", iare);
}
}
/**
* Using the given Method, it returns the name of the java bean property and the prefix of the method.
* <p/>
* <h3>Examples:</h3>
* <p/>
* <pre>
* getFoo -> get, foo
* getX -> get, x
* getURL -> get, URL
* handleBar -> handle, bar
* </pre>
*
* @param method The method to translate.
* @return The property names or null if this was not a valid property Method.
*/
private static PropertyName getPropertyNames(Method method) {
String name = method.getName();
char[] ca = name.toCharArray();
int startIndex = -1;
for (int i = 0; i < ca.length; i++) {
char c = ca[i];
if (Character.isUpperCase(c) && i == 0) {
break;
} else if (Character.isUpperCase(c)) {
startIndex = i;
break;
}
}
if (startIndex == -1) {
return null;
}
String propertyName = Introspector.decapitalize(name.substring(startIndex));
String prefix = name.substring(0, startIndex);
return new PropertyName(prefix, propertyName);
}
/**
* This interface defines a mechanism to extract information from JavaBean properties.
*
* @author Brian Pontarelli
*/
public interface MethodInformationExtractor {
/**
* Determines the generic type of the property.
*
* @param method The method to pull the generic type from.
* @return The generic type.
*/
Type determineGenericType(Method method);
/**
* Determines the type of the method. For getters this is the return type. For setters this is the parameter.
*
* @param method The method.
*/
Class<?> determineType(Method method);
/**
* Whether or not this property is an indexed property.
*
* @param method The method to determine if it is indexed.
* @return True if indexed or false otherwise.
*/
boolean isIndexed(Method method);
}
/**
* This class extracts information about JavaBean standard getter methods. The forms of the methods are as follows:
* <p/>
* <h3>Indexed methods</h3>
* <p/>
* <h4>Retrieval</h4>
* <p/>
* <pre>
* public Object getFoo(int index)
* public boolean isFoo(int index)
* </pre>
* <p/>
* <h3>Normal methods</h3>
* <p/>
* <h4>Retrieval</h4>
* <p/>
* <pre>
* public Object getFoo()
* public boolean isFoo()
* </pre>
* <p/>
* All <b>is</b> methods must have a return type of boolean regardless of being indexed or not.
*
* @author Brian Pontarelli
*/
public static class GetMethodInformationExtractor implements MethodInformationExtractor {
/**
* @param method The method to get the generic type from.
* @return Returns the return type of the method.
*/
@Override
public Type determineGenericType(Method method) {
return method.getGenericReturnType();
}
@Override
public Class<?> determineType(Method method) {
return method.getReturnType();
}
@Override
public boolean isIndexed(Method method) {
return isValidIndexedGetter(method);
}
}
/**
* This class is a small helper class that is used to store the read and write methods of a bean property as well as a
* flag that determines if it is indexed.
*
* @author Brian Pontarelli
*/
public static class PropertyInfo {
private final Map<String, Method> methods = new HashMap<String, Method>();
private Class<?> definingClass;
private Type genericType;
private boolean indexed;
private String name;
private Class<?> type;
public Type getGenericType() {
return genericType;
}
public void setGenericType(Type genericType) {
this.genericType = genericType;
}
public Map<String, Method> getMethods() {
return methods;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Class<?> getType() {
return type;
}
public void setType(Class<?> type) {
this.type = type;
}
public boolean isIndexed() {
return indexed;
}
public void setIndexed(boolean indexed) {
this.indexed = indexed;
}
public void setDefiningClass(Class<?> definingClass) {
this.definingClass = definingClass;
}
public String toString() {
return "Property named [" + name + "] in class [" + definingClass + "]";
}
}
/**
* This class stores the information about JavaBean methods including the prefix and propertyName.
*
* @author Brian Pontarelli
*/
public static class PropertyName {
private final String name;
private final String prefix;
public PropertyName(String prefix, String name) {
this.prefix = prefix;
this.name = name;
}
public String getName() {
return name;
}
public String getPrefix() {
return prefix;
}
}
/**
* This class extracts information from JavaBean standard setter methods. The forms of the methods are as follows:
* <p/>
* <h3>Indexed methods</h3>
* <p/>
* <h4>Store</h4>
* <p/>
* <pre>
* public void setFoo(int index, Object obj)
* public void setBool(int index, boolean bool)
* </pre>
* <h3>Normal methods</h3>
* <p/>
* <h4>Storage</h4>
* <p/>
* <pre>
* public void setFoo(Object o)
* </pre>
*
* @author Brian Pontarelli
*/
public static class SetMethodInformationExtractor implements MethodInformationExtractor {
@Override
public Type determineGenericType(Method method) {
Type[] types = method.getGenericParameterTypes();
if (types.length == 1) {
return types[0];
}
return types[1];
}
@Override
public Class<?> determineType(Method method) {
return isIndexed(method) ? method.getParameterTypes()[1] : method.getParameterTypes()[0];
}
@Override
public boolean isIndexed(Method method) {
return isValidIndexedSetter(method);
}
}
}