/*
* Copyright 2017 OmniFaces
*
* 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.omnifaces.util;
import static java.beans.Introspector.getBeanInfo;
import static java.beans.PropertyEditorManager.findEditor;
import static java.lang.String.format;
import static java.util.logging.Level.FINE;
import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.beans.PropertyEditor;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Logger;
import javax.enterprise.inject.Typed;
/**
* Collection of utility methods for working with reflection.
*
* @since 2.0
* @author Arjan Tijms
*
*/
@Typed
public final class Reflection {
private static final Logger logger = Logger.getLogger(Reflection.class.getName());
private static final String ERROR_LOAD_CLASS = "Cannot load class '%s'.";
private static final String ERROR_CREATE_INSTANCE = "Cannot create instance of class '%s'.";
private static final String ERROR_ACCESS_FIELD = "Cannot access field '%s' of class '%s'.";
private static final String ERROR_INVOKE_METHOD = "Cannot invoke method '%s' of class '%s' with arguments %s.";
private Reflection() {
// Hide constructor.
}
/**
* Sets a collection of properties of a given object to the values associated with those properties.
* <p>
* In the map that represents these properties, each key represents the name of the property, with the value
* associated with that key being the value that is set for the property.
* <p>
* E.g. map entry key = foo, value = "bar", which "bar" an instance of String, will conceptually result in the
* following call: <code>object.setFoo("string");</code>
*
* <p>
* NOTE: This particular method assumes that there's a write method for each property in the map with the right
* type. No specific checking is done whether this is indeed the case.
*
* @param object
* the object on which properties will be set
* @param propertiesToSet
* the map containing properties and their values to be set on the object
*/
public static void setProperties(Object object, Map<String, Object> propertiesToSet) {
try {
Map<String, PropertyDescriptor> availableProperties = new HashMap<>();
for (PropertyDescriptor propertyDescriptor : getBeanInfo(object.getClass()).getPropertyDescriptors()) {
availableProperties.put(propertyDescriptor.getName(), propertyDescriptor);
}
for (Entry<String, Object> propertyToSet : propertiesToSet.entrySet()) {
availableProperties.get(propertyToSet.getKey()).getWriteMethod().invoke(object, propertyToSet.getValue());
}
} catch (IntrospectionException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
throw new IllegalStateException(e);
}
}
/**
* Sets a collection of properties of a given object to the (optionally coerced) values associated with those properties.
* <p>
* In the map that represents these properties, each key represents the name of the property, with the value
* associated with that key being the value that is set for the property.
* <p>
* E.g. map entry key = foo, value = "bar", which "bar" an instance of String, will conceptually result in the
* following call: <code>object.setFoo("string");</code>
*
* <p>
* NOTE 1: In case the value is a String, and the target type is not String, the standard property editor mechanism
* will be used to attempt a conversion.
*
* <p>
* Note 2: This method operates somewhat as the reverse of {@link Reflection#setProperties(Object, Map)}. Here only
* the available writable properties of the object are matched against the map with properties to set. Properties
* in the map for which there isn't a corresponding writable property on the object are ignored.
*
* <p>
* Following the above two notes, use this method when attempting to set properties on an object in a lenient best effort
* basis. Use {@link Reflection#setProperties(Object, Map)} when all properties need to be set with the exact type as the value
* appears in the map.
*
*
* @param object
* the object on which properties will be set
* @param propertiesToSet
* the map containing properties and their values to be set on the object
*/
public static void setPropertiesWithCoercion(Object object, Map<String, Object> propertiesToSet) {
try {
for (PropertyDescriptor property : getBeanInfo(object.getClass()).getPropertyDescriptors()) {
Method setter = property.getWriteMethod();
if (setter == null || !propertiesToSet.containsKey(property.getName())) {
continue;
}
Object value = propertiesToSet.get(property.getName());
if (value instanceof String && !property.getPropertyType().equals(String.class)) {
// Try to convert Strings to the type expected by the converter
PropertyEditor editor = findEditor(property.getPropertyType());
editor.setAsText((String) value);
value = editor.getValue();
}
property.getWriteMethod().invoke(object, value);
}
}
catch (Exception e) {
throw new IllegalStateException(e);
}
}
/**
* Finds a method based on the method name, amount of parameters and limited typing and returns <code>null</code>
* is none is found.
* <p>
* Note that this supports overloading, but a limited one. Given an actual parameter of type Long, this will select
* a method accepting Number when the choice is between Number and a non-compatible type like String. However,
* it will NOT select the best match if the choice is between Number and Long.
*
* @param base the object in which the method is to be found
* @param methodName name of the method to be found
* @param params the method parameters
* @return a method if one is found, null otherwise
*/
public static Method findMethod(Object base, String methodName, Object... params) {
List<Method> methods = new ArrayList<>();
for (Class<?> cls = base.getClass(); cls != null; cls = cls.getSuperclass()) {
for (Method method : cls.getDeclaredMethods()) {
if (method.getName().equals(methodName) && method.getParameterTypes().length == params.length) {
methods.add(method);
}
}
}
if (methods.size() == 1) {
return methods.get(0);
}
else {
return closestMatchingMethod(methods, params); // Overloaded methods were found. Try to find closest match.
}
}
private static Method closestMatchingMethod(List<Method> methods, Object... params) {
for (Method method : methods) {
Class<?>[] candidateParams = method.getParameterTypes();
boolean match = true;
for (int i = 0; i < params.length; i++) {
if (!candidateParams[i].isInstance(params[i])) {
match = false;
break;
}
}
// If all candidate parameters were expected and for none of them the actual parameter was NOT an instance, we have a match.
if (match) {
return method;
}
// Else, at least one parameter was not an instance. Go ahead a test then next methods.
}
return null;
}
/**
* Returns the class object associated with the given class name, using the context class loader and if
* that fails the defining class loader of the current class.
* @param <T> The expected class type.
* @param className Fully qualified class name of the class for which a class object needs to be created.
* @return The class object associated with the given class name.
* @throws IllegalStateException If the class cannot be loaded.
* @throws ClassCastException When <code>T</code> is of wrong type.
*/
@SuppressWarnings("unchecked")
public static <T> Class<T> toClass(String className) {
try {
return (Class<T>) (Class.forName(className, true, Thread.currentThread().getContextClassLoader()));
}
catch (Exception e) {
try {
return (Class<T>) Class.forName(className);
}
catch (Exception ignore) {
logger.log(FINE, "Ignoring thrown exception; previous exception will be rethrown instead.", ignore);
// Just continue to IllegalStateException on original ClassNotFoundException.
}
throw new IllegalStateException(format(ERROR_LOAD_CLASS, className), e);
}
}
/**
* Returns the class object associated with the given class name, using the context class loader and if
* that fails the defining class loader of the current class. If the class cannot be loaded, then return null
* instead of throwing illegal state exception.
* @param <T> The expected class type.
* @param className Fully qualified class name of the class for which a class object needs to be created.
* @return The class object associated with the given class name.
* @throws ClassCastException When <code>T</code> is of wrong type.
* @since 2.5
*/
public static <T> Class<T> toClassOrNull(String className) {
try {
return toClass(className);
}
catch (Exception ignore) {
logger.log(FINE, "Ignoring thrown exception; the sole intent is to return null instead.", ignore);
return null;
}
}
/**
* Finds a constructor based on the given parameter types and returns <code>null</code> is none is found.
* @param clazz The class object for which the constructor is to be found.
* @param parameterTypes The desired method parameter types.
* @return A constructor if one is found, null otherwise.
* @since 2.6
*/
public static <T> Constructor<T> findConstructor(Class<T> clazz, Class<?>... parameterTypes) {
try {
return clazz.getConstructor(parameterTypes);
}
catch (Exception ignore) {
logger.log(FINE, "Ignoring thrown exception; the sole intent is to return null instead.", ignore);
return null;
}
}
/**
* Returns a new instance of the given class name using the default constructor.
* @param <T> The expected return type.
* @param className Fully qualified class name of the class for which an instance needs to be created.
* @return A new instance of the given class name using the default constructor.
* @throws IllegalStateException If the class cannot be loaded.
* @throws ClassCastException When <code>T</code> is of wrong type.
*/
@SuppressWarnings("unchecked")
public static <T> T instance(String className) {
return (T) instance(toClass(className));
}
/**
* Returns a new instance of the given class object using the default constructor.
* @param <T> The generic object type.
* @param clazz The class object for which an instance needs to be created.
* @return A new instance of the given class object using the default constructor.
* @throws IllegalStateException If the class cannot be found, or cannot be instantiated, or when a security manager
* prevents this operation.
*/
public static <T> T instance(Class<T> clazz) {
try {
return clazz.newInstance();
}
catch (Exception e) {
throw new IllegalStateException(format(ERROR_CREATE_INSTANCE, clazz), e);
}
}
/**
* Returns the value of the field of the given instance on the given field name.
* @param <T> The expected return type.
* @param instance The instance to access the given field on.
* @param fieldName The name of the field to be accessed on the given instance.
* @return The value of the field of the given instance on the given field name.
* @throws ClassCastException When <code>T</code> is of wrong type.
* @throws IllegalStateException If the field cannot be accessed.
* @since 2.5
*/
@SuppressWarnings("unchecked")
public static <T> T accessField(Object instance, String fieldName) {
try {
Field field = instance.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
return (T) field.get(instance);
}
catch (Exception e) {
throw new IllegalStateException(format(ERROR_ACCESS_FIELD, fieldName, instance.getClass()), e);
}
}
/**
* Invoke a method of the given instance on the given method name with the given parameters and return the result.
* <p>
* Note: the current implementation assumes for simplicity that no one of the given parameters is null. If one of
* them is still null, a NullPointerException will be thrown.
* @param <T> The expected return type.
* @param instance The instance to invoke the given method on.
* @param methodName The name of the method to be invoked on the given instance.
* @param parameters The method parameters, if any.
* @return The result of the method invocation, if any.
* @throws NullPointerException When one of the given parameters is null.
* @throws IllegalStateException If the method cannot be invoked.
* @throws ClassCastException When <code>T</code> is of wrong type.
* @since 2.5
*/
@SuppressWarnings("unchecked")
public static <T> T invokeMethod(Object instance, String methodName, Object... parameters) {
try {
Method method = findMethod(instance, methodName, parameters);
method.setAccessible(true);
return (T) method.invoke(instance, parameters);
}
catch (Exception e) {
throw new IllegalStateException(
format(ERROR_INVOKE_METHOD, methodName, instance.getClass(), Arrays.toString(parameters)), e);
}
}
}