package alien4cloud.utils;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.util.*;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.beans.BeansException;
import org.springframework.util.ReflectionUtils;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import alien4cloud.exception.InvalidArgumentException;
import lombok.extern.slf4j.Slf4j;
/**
* Utility class to help working with java reflection.
*/
@Slf4j
public final class ReflectionUtil {
private ReflectionUtil() {
}
/**
* Get the Class of the generic argument of a generic class based on it's implementation class.
*
* @param implementationClass The implementation class that actually is a sub-class of generic class.
* @param genericClass The generic class for which to get the argument type.
* @param index The index of the argument.
* @return null if the type cannot be found, the generic type when found.
*/
public static <T> Class<?> getGenericArgumentType(Class<? extends T> implementationClass, Class<T> genericClass, int index) {
if (implementationClass.isAssignableFrom(genericClass)) {
return null;
}
Type[] types = getGenericArgumentTypes(implementationClass, genericClass, implementationClass.getTypeParameters());
if (types != null) {
return (Class<?>) types[index];
}
return null;
}
private static <T> Type[] getGenericArgumentTypes(Class<? extends T> currentClass, Class<T> genericClass, Type... resolvedTypes) {
Map<String, Type> resolvedTypesByName = new HashMap<String, Type>();
for (int i = 0; i < resolvedTypes.length; i++) {
TypeVariable<?> typeVariable = (TypeVariable<?>) currentClass.getTypeParameters()[i];
resolvedTypesByName.put(typeVariable.getName(), resolvedTypes[i]);
}
Set<Type> directGenericInterfaces = getDirectGenericInterfaces(currentClass, genericClass);
Type[] types = null;
for (Type genericInterface : directGenericInterfaces) {
if (genericInterface instanceof Class) {
types = getGenericArgumentTypes((Class<? extends T>) genericInterface, genericClass);
}
if (types == null && genericInterface instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) genericInterface;
List<Type> nextResolvedTypes = new LinkedList<Type>();
for (Type t : parameterizedType.getActualTypeArguments()) {
if (t instanceof TypeVariable<?>) {
Type resolvedType = resolvedTypesByName.get(((TypeVariable<?>) t).getName());
nextResolvedTypes.add(resolvedType != null ? resolvedType : t);
} else {
nextResolvedTypes.add(t);
}
}
types = getGenericArgumentTypes((Class<? extends T>) parameterizedType.getRawType(), genericClass,
nextResolvedTypes.toArray(new Type[nextResolvedTypes.size()]));
}
if (types != null) {
return types;
}
}
return resolvedTypes;
}
/**
* Get all direct generic interfaces that can be assigned from the expected class.
*
* @param clazz The class from which to get generic.
* @param expectedClazz The class from which .
* @return a Set that contains all direct generic interfaces matching the expectedClazz from clazz.
*/
private static Set<Type> getDirectGenericInterfaces(Class<?> clazz, Class<?> expectedClazz) {
Set<Type> superTypes = Sets.newHashSet();
if (clazz.getGenericSuperclass() != null && isAssignableFrom(clazz.getGenericSuperclass(), expectedClazz)) {
superTypes.add(clazz.getGenericSuperclass());
}
for (Type genericInterface : clazz.getGenericInterfaces()) {
if (isAssignableFrom(genericInterface, expectedClazz)) {
superTypes.add(genericInterface);
}
}
return superTypes;
}
private static boolean isAssignableFrom(Type type, Class<?> clazz) {
if (type instanceof Class) {
return clazz.isAssignableFrom((Class<?>) type);
}
if (type instanceof ParameterizedType) {
Type rawType = ((ParameterizedType) type).getRawType();
return rawType instanceof Class && clazz.isAssignableFrom((Class<?>) rawType);
}
return false;
}
/**
* Try getting an annotation from a property, fall back to field with the same name if not found
*
* @param clazz the parent class
* @param annotationClass the annotation type to search
* @param property the property to search for annotation
* @param <T> generic annotation type
* @return annotation
*/
public static <T extends Annotation> T getAnnotation(Class<?> clazz, final Class<T> annotationClass, final PropertyDescriptor property) {
T annotationType = property.getReadMethod().getAnnotation(annotationClass);
if (annotationType == null) {
// Search on the setter
annotationType = property.getWriteMethod().getAnnotation(annotationClass);
}
try {
final Map<Class<?>, T> foundFields = Maps.newHashMap();
if (annotationType == null) {
// Search in the field declaration
ReflectionUtils.doWithFields(clazz, new ReflectionUtils.FieldCallback() {
@Override
public void doWith(final Field field) throws IllegalAccessException {
foundFields.put(field.getDeclaringClass(), field.getAnnotation(annotationClass));
}
}, new ReflectionUtils.FieldFilter() {
@Override
public boolean matches(final Field field) {
// no static fields please
return !Modifier.isStatic(field.getModifiers())
&& (field.getName().equals(property.getName()) || field.getName().equals(property.getReadMethod().getName()));
}
});
if (!foundFields.isEmpty()) {
annotationType = foundFields.values().iterator().next();
}
}
} catch (IllegalArgumentException e) {
if (log.isDebugEnabled()) {
log.debug("Reflection error", e);
}
}
return annotationType;
}
/**
* Get properties descriptors
*
* @param clazz the class to get descriptors from
* @return properties descriptors
*/
public static PropertyDescriptor[] getPropertyDescriptors(Class<?> clazz) {
try {
BeanInfo beanInfo = Introspector.getBeanInfo(clazz);
return beanInfo.getPropertyDescriptors();
} catch (IntrospectionException e) {
throw new InvalidArgumentException("Cannot get descriptors of class [" + clazz.getName() + "]", e);
}
}
/**
* Merge object from an object to another. Failsafe : resist to invalid property.
*
* @param from source of the update
* @param to target of the update
* @param ignoreNullValue indicate if we should merge null value
* @param ignores properties names that should be ignored
*/
public static void mergeObject(Object from, Object to, boolean ignoreNullValue, Set<String> ignores) {
try {
Map<String, Object> settablePropertiesMap = Maps.newHashMap();
PropertyDescriptor[] propertyDescriptors = getPropertyDescriptors(from.getClass());
for (PropertyDescriptor property : propertyDescriptors) {
if (property.getReadMethod() == null || property.getWriteMethod() == null) {
continue;
}
Object value = property.getReadMethod().invoke(from);
if ((value != null || !ignoreNullValue) && !ignores.contains(property.getName())) {
settablePropertiesMap.put(property.getName(), value);
}
}
for (Map.Entry<String, Object> settableProperty : settablePropertiesMap.entrySet()) {
// Set new values
String propertyName = settableProperty.getKey();
Object propertyValue = settableProperty.getValue();
setPropertyValue(to, propertyName, propertyValue);
}
} catch (IllegalAccessException | InvocationTargetException | BeansException e) {
throw new InvalidArgumentException("Cannot merge object", e);
}
}
/**
* Merge object from an object to another. Failsafe : resist to invalid property.
*
* @param from source of the update
* @param to target of the update
* @param ignores properties names that should be ignored
*/
public static void mergeObject(Object from, Object to, String... ignores) {
mergeObject(from, to, true, Sets.newHashSet(ignores));
}
/**
* Get property's value of an object
*
* @param object the object to get property from
* @param property the name of the property
* @return the value of the property
*/
public static Object getPropertyValue(Object object, String property) {
BeanWrapper wrapper = new BeanWrapperImpl(object);
return wrapper.getPropertyValue(property);
}
/**
* Set property's value of an object
*
* @param object the object to set property
* @param property the name of the property
* @param value new value to set
*/
public static void setPropertyValue(Object object, String property, Object value) {
BeanWrapper wrapper = new BeanWrapperImpl(object);
if (wrapper.isWritableProperty(property)) {
wrapper.setPropertyValue(property, value);
}
}
/**
* Recursive getDeclaredField that allows looking for parent types fields.
*
* @param fieldName The name of the field to get.
* @param clazz The class in which to search.
*/
public static Field getDeclaredField(Class<?> clazz, String fieldName) throws NoSuchFieldException {
try {
Field field = clazz.getDeclaredField(fieldName);
return field;
} catch (NoSuchFieldException e) {
if (Object.class.equals(clazz.getSuperclass())) {
throw e;
}
return getDeclaredField(clazz.getSuperclass(), fieldName);
}
}
/**
* Return true if the given class is a primitive type, a primitive wrapper or a String.
*
* @param clazz The class to check.
* @return True if the class is String, a primitive type or a primitive Wrapper type.
*/
public static boolean isPrimitiveOrWrapperOrString(Class<?> clazz) {
return clazz.isPrimitive() || clazz.equals(String.class) || clazz.equals(Boolean.class) || clazz.equals(Integer.class) || clazz.equals(Long.class)
|| clazz.equals(Float.class) || clazz.equals(Double.class) || clazz.equals(Character.class) || clazz.equals(Byte.class)
|| clazz.equals(Short.class);
}
}