package de.cronn.reflection.util; import java.beans.PropertyDescriptor; import java.lang.annotation.Annotation; import java.lang.reflect.AccessibleObject; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.Collection; import java.util.Collections; import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.objenesis.ObjenesisHelper; public final class PropertyUtils { private static final Map<Class<?>, PropertyDescriptorCache<?>> cache = new ConcurrentHashMap<>(); private PropertyUtils() { } @Nullable public static PropertyDescriptor getPropertyDescriptorByName(Object bean, String propertyName) { return getPropertyDescriptorByName(ClassUtils.getRealClass(bean), propertyName); } @Nullable public static PropertyDescriptor getPropertyDescriptorByName(Class<?> beanClass, String propertyName) { PropertyDescriptorCache<?> propertyDescriptorCache = getCache(beanClass); return propertyDescriptorCache.getDescriptorByName(propertyName); } @Nonnull public static PropertyDescriptor getPropertyDescriptorByNameOrThrow(Object bean, String propertyName) { Class<Object> beanClass = ClassUtils.getRealClass(bean); return getPropertyDescriptorByNameOrThrow(beanClass, propertyName); } @Nonnull public static PropertyDescriptor getPropertyDescriptorByNameOrThrow(Class<?> beanClass, String propertyName) { PropertyDescriptor propertyDescriptor = getPropertyDescriptorByName(beanClass, propertyName); Assert.notNull(propertyDescriptor, () -> String.format("Property '%s' not found for '%s'", propertyName, beanClass.getSimpleName())); return propertyDescriptor; } public static Collection<PropertyDescriptor> getPropertyDescriptors(Class<?> type) { PropertyDescriptorCache<?> propertyDescriptorCache = getCache(type); return propertyDescriptorCache.getDescriptors(); } public static Collection<PropertyDescriptor> getPropertyDescriptors(Object object) { return getPropertyDescriptors(ClassUtils.getRealClass(object)); } public static <A extends Annotation> Map<PropertyDescriptor, A> getPropertyDescriptorsWithAnnotation(Object object, Class<A> annotationClass) { Class<Object> objectClass = ClassUtils.getRealClass(object); return getPropertyDescriptorsWithAnnotation(objectClass, annotationClass); } public static <A extends Annotation> Map<PropertyDescriptor, A> getPropertyDescriptorsWithAnnotation(Class<?> type, Class<A> annotationClass) { PropertyDescriptorCache<?> propertyDescriptorCache = getCache(type); return propertyDescriptorCache.getDescriptorsForAnnotation(annotationClass); } @SuppressWarnings("unchecked") static <T> PropertyDescriptorCache<T> getCache(Class<T> type) { return (PropertyDescriptorCache<T>) cache.computeIfAbsent(type, PropertyDescriptorCache::compute); } public static <T> T copyNonDefaultValues(T source, T destination) { return copyNonDefaultValues(source, destination, Collections.emptySet()); } public static <T> T copyNonDefaultValues(T source, T destination, PropertyDescriptor... excludedProperties) { return copyNonDefaultValues(source, destination, Stream.of(excludedProperties).collect(Collectors.toSet())); } public static <T> T copyNonDefaultValues(T source, T destination, Collection<PropertyDescriptor> excludedProperties) { getPropertyDescriptors(source).stream() .filter(property -> !excludedProperties.contains(property)) .filter(PropertyUtils::isFullyAccessible) .filter(propertyDescriptor -> !hasDefaultValue(source, propertyDescriptor)) .forEach(propertyDescriptor -> copyValue(source, destination, propertyDescriptor)); return destination; } public static <T> Object copyValue(T source, T destination, PropertyDescriptor propertyDescriptor) { Object value = read(source, propertyDescriptor); write(destination, propertyDescriptor, value); return value; } public static <T> boolean hasDefaultValue(T bean, PropertyDescriptor propertyDescriptor) { Object value = read(bean, propertyDescriptor); Class<?> beanClass = ClassUtils.getRealClass(bean); return isDefaultValue(beanClass, propertyDescriptor, value); } public static <T> boolean hasSameValue(T a, T b, PropertyDescriptor propertyDescriptor) { Object valueFromA = read(a, propertyDescriptor); Object valueFromB = read(b, propertyDescriptor); return Objects.equals(valueFromA, valueFromB); } public static <T> boolean hasDifferentValue(T a, T b, PropertyDescriptor propertyDescriptor) { return !hasSameValue(a, b, propertyDescriptor); } public static <T> boolean isDefaultValue(Class<T> objectClass, TypedPropertyGetter<T, ?> propertyGetter, Object value) { return isDefaultValue(objectClass, getPropertyDescriptor(objectClass, propertyGetter), value); } public static <T> boolean isDefaultValue(Class<T> objectClass, PropertyDescriptor propertyDescriptor, Object value) { Object defaultValue = getDefaultValue(objectClass, propertyDescriptor); if (defaultValue instanceof Float && value instanceof Float) { return (float) defaultValue == (float) value; } else if (defaultValue instanceof Double && value instanceof Double) { return (double) defaultValue == (double) value; } else { return Objects.equals(value, defaultValue); } } public static <T> Object getDefaultValue(Class<T> objectClass, PropertyDescriptor propertyDescriptor) { return getCache(objectClass).getDefaultValue(propertyDescriptor); } public static void write(Object destination, String propertyName, Object value) { PropertyDescriptor propertyDescriptor = getPropertyDescriptorByNameOrThrow(destination, propertyName); write(destination, propertyDescriptor, value); } public static <T> void writeIfPropertyExists(Object destination, String propertyName, Supplier<T> valueSupplier) { PropertyDescriptor property = getPropertyDescriptorByName(destination, propertyName); if (property != null) { T value = valueSupplier.get(); write(destination, property, value); } } public static void write(Object destination, PropertyDescriptor propertyDescriptor, Object value) { write(destination, propertyDescriptor, value, false); } public static void write(Object destination, PropertyDescriptor propertyDescriptor, Object value, boolean force) { try { if (!isWritable(propertyDescriptor)) { if (force) { writeDirectly(destination, propertyDescriptor, value); } else { throw new IllegalArgumentException(propertyDescriptor.getName() + " is not writable"); } } else { Object[] args = new Object[] { value }; Method writeMethod = propertyDescriptor.getWriteMethod(); withAccessibleObject(writeMethod, method -> method.invoke(destination, args), force); } } catch (ReflectiveOperationException | RuntimeException e) { throw new ReflectionRuntimeException("Failed to write " + getQualifiedPropertyName(destination, propertyDescriptor), e); } } public static void writeDirectly(Object destination, PropertyDescriptor propertyDescriptor, Object value) { writeDirectly(destination, propertyDescriptor.getName(), value); } public static void writeDirectly(Object destination, String propertyName, Object value) { try { Field field = findField(destination.getClass(), propertyName); writeDirectly(destination, field, value); } catch (NoSuchFieldException e) { throw new ReflectionRuntimeException("Failed to write " + getQualifiedPropertyName(destination, propertyName), e); } } public static void writeDirectly(Object destination, Field field, Object value) { Assert.notNull(destination, () -> "Destination must not be null"); try { withAccessibleObject(field, f -> f.set(destination, value)); } catch (ReflectiveOperationException e) { throw new ReflectionRuntimeException("Failed to write " + getQualifiedPropertyName(destination, field), e); } } private static Field findField(Class<?> objectClass, String propertyName) throws NoSuchFieldException { try { return objectClass.getDeclaredField(propertyName); } catch (NoSuchFieldException e) { Class<?> superclass = objectClass.getSuperclass(); if (!superclass.equals(Object.class)) { return findField(superclass, propertyName); } throw e; } } public static <T> T readDirectly(Object object, PropertyDescriptor propertyDescriptor) { return readDirectly(object, propertyDescriptor.getName()); } public static <T> T readDirectly(Object object, String propertyName) { try { Field field = findField(object.getClass(), propertyName); return readDirectly(object, field); } catch (NoSuchFieldException e) { throw new ReflectionRuntimeException("Failed to read " + getQualifiedPropertyName(object, propertyName), e); } } public static <T> T readDirectly(Object object, Field field) { try { return withAccessibleObject(field, f -> { @SuppressWarnings("unchecked") T value = (T) field.get(object); return value; }, true); } catch (ReflectiveOperationException e) { throw new ReflectionRuntimeException("Failed to read " + getQualifiedPropertyName(object, field), e); } } public static <T> T read(Object source, PropertyDescriptor propertyDescriptor) { return read(source, propertyDescriptor, false); } public static <T> T read(Object source, PropertyDescriptor propertyDescriptor, boolean force) { final Object result; try { if (!isReadable(propertyDescriptor)) { if (force) { return readDirectly(source, propertyDescriptor); } else { throw new IllegalArgumentException(String.format("%s must be readable", propertyDescriptor.getName())); } } else { Method readMethod = propertyDescriptor.getReadMethod(); result = withAccessibleObject(readMethod, method -> readMethod.invoke(source), force); } } catch (ReflectiveOperationException | RuntimeException e) { throw new ReflectionRuntimeException("Failed to read " + getQualifiedPropertyName(source, propertyDescriptor), e); } @SuppressWarnings("unchecked") T castedResult = (T) result; return castedResult; } public static <T> T readIfPropertyExists(Object source, String propertyName) { PropertyDescriptor property = getPropertyDescriptorByName(source, propertyName); if (property != null) { return read(source, property); } else { return null; } } public static <T> T readProperty(Object entity, PropertyDescriptor propertyDescriptor, Class<T> expectedType) { Class<?> clazz = ClassUtils.getRealClass(entity); String propertyName = propertyDescriptor.getName(); Class<?> propertyType = propertyDescriptor.getPropertyType(); if (!expectedType.isAssignableFrom(propertyType)) { throw new IllegalArgumentException(String.format("%s.%s is of type %s but %s is expected", clazz, propertyName, propertyType, expectedType)); } return PropertyUtils.read(entity, propertyDescriptor); } @Nonnull public static <T> PropertyDescriptor getPropertyDescriptor(T bean, TypedPropertyGetter<T, ?> propertyGetter) { Class<T> beanClass = ClassUtils.getRealClass(bean); return getPropertyDescriptor(beanClass, propertyGetter); } @Nonnull public static <T> PropertyDescriptor getPropertyDescriptor(Class<T> beanClass, TypedPropertyGetter<T, ?> propertyGetter) { Method method = getMethod(beanClass, propertyGetter); PropertyDescriptor propertyDescriptor = getPropertyDescriptorByMethod(beanClass, method); Assert.notNull(propertyDescriptor, () -> String.format("Found no property for %s on %s", method, beanClass)); return propertyDescriptor; } @Nonnull public static <T> String getPropertyName(Class<T> beanClass, TypedPropertyGetter<T, ?> propertyGetter) { PropertyDescriptor propertyDescriptor = getPropertyDescriptor(beanClass, propertyGetter); return propertyDescriptor.getName(); } @Nonnull public static <T> String getPropertyName(T bean, TypedPropertyGetter<T, ?> propertyGetter) { Class<T> beanClass = ClassUtils.getRealClass(bean); return getPropertyName(beanClass, propertyGetter); } @Nullable public static <T> PropertyDescriptor getPropertyDescriptorByMethod(Class<T> beanClass, Method method) { PropertyDescriptorCache<?> propertyDescriptorCache = getCache(beanClass); return propertyDescriptorCache.getDescriptorByMethod(method); } @Nullable public static <T> PropertyDescriptor getPropertyDescriptorByField(Class<T> beanClass, Field field) { PropertyDescriptorCache<?> propertyDescriptorCache = getCache(beanClass); return propertyDescriptorCache.getDescriptorByField(field); } @Nonnull public static <T> Method getMethod(Class<T> beanClass, TypedPropertyGetter<T, ?> propertyGetter) { PropertyDescriptorCache<T> cache = getCache(beanClass); return cache.getMethod(propertyGetter); } public static <T> Method findMethodByGetter(Class<T> beanClass, TypedPropertyGetter<T, ?> propertyGetter) { MethodCaptor methodCaptor = new MethodCaptor(); T proxy = createProxy(beanClass, methodCaptor); propertyGetter.get(proxy); return methodCaptor.getCapturedMethod(); } private static <T> T createProxy(Class<T> beanClass, MethodCaptor methodCaptor) { Class<? extends T> proxyClass = getCache(beanClass).getMethodCapturingProxy(); try { T proxyInstance = ObjenesisHelper.newInstance(proxyClass); writeDirectly(proxyInstance, MethodCaptor.FIELD_NAME, methodCaptor); return proxyInstance; } catch (IllegalAccessError e) { throw new ReflectionRuntimeException("Failed to create proxy on " + beanClass, e); } } public static boolean hasAnnotationOfProperty(Class<?> entityType, PropertyDescriptor descriptor, Class<? extends Annotation> annotationClass) { return getAnnotationOfProperty(entityType, descriptor, annotationClass) != null; } public static <T, A extends Annotation> A getAnnotationOfProperty(Class<T> entityType, TypedPropertyGetter<T, ?> propertyGetter, Class<A> annotationClass) { PropertyDescriptor propertyDescriptor = getPropertyDescriptor(entityType, propertyGetter); return getAnnotationOfProperty(entityType, propertyDescriptor, annotationClass); } public static <A extends Annotation> A getAnnotationOfProperty(Object object, PropertyDescriptor descriptor, Class<A> annotationClass) { Class<Object> objectClass = ClassUtils.getRealClass(object); return getAnnotationOfProperty(objectClass, descriptor, annotationClass); } public static <A extends Annotation> A getAnnotationOfProperty(Class<?> entityType, PropertyDescriptor descriptor, Class<A> annotationClass) { PropertyDescriptorCache<?> cache = getCache(entityType); Map<PropertyDescriptor, A> descriptorsForAnnotation = cache.getDescriptorsForAnnotation(annotationClass); return descriptorsForAnnotation.get(descriptor); } public static boolean isFullyAccessible(PropertyDescriptor descriptor) { return isReadable(descriptor) && isWritable(descriptor); } public static boolean isWritable(PropertyDescriptor descriptor) { return descriptor.getWriteMethod() != null; } public static boolean isReadable(PropertyDescriptor descriptor) { return descriptor.getReadMethod() != null; } public static boolean isDeclaredInClass(PropertyDescriptor propertyDescriptor, Class<?> entityClass) { Method readMethod = propertyDescriptor.getReadMethod(); return readMethod != null && Objects.equals(readMethod.getDeclaringClass(), entityClass); } public static boolean hasProperty(Object bean, String propertyName) { return getPropertyDescriptorByName(bean, propertyName) != null; } public static boolean hasProperty(Class<?> beanClass, String propertyName) { return getPropertyDescriptorByName(beanClass, propertyName) != null; } public static Object getDefaultValueObject(Class<?> type) { if (type.isPrimitive()) { if (type.equals(byte.class)) { return Byte.valueOf((byte) 0); } else if (type.equals(char.class)) { return Character.valueOf('\0'); } else if (type.equals(short.class)) { return Short.valueOf((short) 0); } else if (type.equals(int.class)) { return Integer.valueOf(0); } else if (type.equals(long.class)) { return Long.valueOf(0L); } else if (type.equals(float.class)) { return Float.valueOf(0.0f); } else if (type.equals(double.class)) { return Double.valueOf(0.0); } else if (type.equals(boolean.class)) { return Boolean.valueOf(false); } else if (type.equals(void.class)) { return null; } else { throw new IllegalArgumentException("Unhandled primitive type: " + type); } } return null; } public static <T> String getQualifiedPropertyName(T bean, TypedPropertyGetter<T, ?> propertyGetter) { Class<T> beanClass = ClassUtils.getRealClass(bean); return getQualifiedPropertyName(beanClass, propertyGetter); } public static <T> String getQualifiedPropertyName(Class<T> type, TypedPropertyGetter<T, ?> propertyGetter) { PropertyDescriptor propertyDescriptor = getPropertyDescriptor(type, propertyGetter); return getQualifiedPropertyName(type, propertyDescriptor); } public static String getQualifiedPropertyName(Object bean, PropertyDescriptor propertyDescriptor) { return getQualifiedPropertyName(ClassUtils.getRealClass(bean), propertyDescriptor); } public static String getQualifiedPropertyName(Class<?> type, PropertyDescriptor propertyDescriptor) { return getQualifiedPropertyName(type, propertyDescriptor.getName()); } public static String getQualifiedPropertyName(Class<?> type, String name) { return type.getSimpleName() + "." + name; } static String getQualifiedPropertyName(Object bean, Field field) { return getQualifiedPropertyName(bean, field.getName()); } private static String getQualifiedPropertyName(Object bean, String name) { return getQualifiedPropertyName(ClassUtils.getRealClass(bean), name); } public static boolean isCollectionType(PropertyDescriptor propertyDescriptor) { return Collection.class.isAssignableFrom(propertyDescriptor.getPropertyType()); } public static boolean isNotCollectionType(PropertyDescriptor propertyDescriptor) { return !isCollectionType(propertyDescriptor); } private interface AccessibleObjectFunction<T extends AccessibleObject, R> { R access(T object) throws ReflectiveOperationException; } private interface AccessibleObjectConsumer<T extends AccessibleObject> { void access(T object) throws ReflectiveOperationException; } private static <T extends AccessibleObject> void withAccessibleObject(T accessibleObject, AccessibleObjectConsumer<T> accessibleObjectConsumer) throws ReflectiveOperationException { withAccessibleObject(accessibleObject, obj -> { accessibleObjectConsumer.access(obj); return null; }, true); } private static <T extends AccessibleObject, R> R withAccessibleObject(T accessibleObject, AccessibleObjectFunction<T, R> function, boolean force) throws ReflectiveOperationException { boolean accessible = accessibleObject.isAccessible(); try { if (force && !accessible) { accessibleObject.setAccessible(true); } return function.access(accessibleObject); } finally { if (force && !accessible) { accessibleObject.setAccessible(false); } } } static void clearCache() { cache.clear(); } }