Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
io.evitadb.utils.ReflectionLookup Maven / Gradle / Ivy
/*
*
* _ _ ____ ____
* _____ _(_) |_ __ _| _ \| __ )
* / _ \ \ / / | __/ _` | | | | _ \
* | __/\ V /| | || (_| | |_| | |_) |
* \___| \_/ |_|\__\__,_|____/|____/
*
* Copyright (c) 2023
*
* Licensed under the Business Source License, Version 1.1 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://github.com/FgForrest/evitaDB/blob/main/LICENSE
*
* 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 io.evitadb.utils;
import io.evitadb.dataType.data.ReflectionCachingBehaviour;
import io.evitadb.dataType.map.WeakConcurrentMap;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.annotation.Repeatable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.RecordComponent;
import java.lang.reflect.Type;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static io.evitadb.utils.CollectionUtils.createLinkedHashMap;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptySet;
import static java.util.Collections.singleton;
import static java.util.Optional.empty;
import static java.util.Optional.of;
import static java.util.Optional.ofNullable;
/**
* This class provides access to the reflection information of the classes (i.e. annotated fields and methods).
* Information is cached in production instance to provide optimal performance lookups.
*
* @author Jan Novotný ([email protected] ), FG Forrest a.s. (c) 2021
*/
@Slf4j
@RequiredArgsConstructor
public class ReflectionLookup {
public final static ReflectionLookup NO_CACHE_INSTANCE = new ReflectionLookup(ReflectionCachingBehaviour.NO_CACHE);
private final WeakConcurrentMap, Map>> constructorCache = new WeakConcurrentMap<>();
private final WeakConcurrentMap, List> classCache = new WeakConcurrentMap<>();
private final WeakConcurrentMap, Map>> fieldCache = new WeakConcurrentMap<>();
private final WeakConcurrentMap, Map>> methodCache = new WeakConcurrentMap<>();
private final WeakConcurrentMap, Map> propertiesCache = new WeakConcurrentMap<>();
private final WeakConcurrentMap, List> gettersWithCorrespondingSetterOrConstructor = new WeakConcurrentMap<>();
private final WeakConcurrentMap, Set>> interfacesCache = new WeakConcurrentMap<>();
private final WeakConcurrentMap methodSamePackageAnnotation = new WeakConcurrentMap<>();
private final WeakConcurrentMap fieldSamePackageAnnotation = new WeakConcurrentMap<>();
private final WeakConcurrentMap, Map> extractorCache = new WeakConcurrentMap<>();
private final ReflectionCachingBehaviour cachingBehaviour;
/**
* Returns property name from getter method (i.e. method starting with get/is).
*/
@Nonnull
public static String getPropertyNameFromMethodName(@Nonnull String methodName) {
return getPropertyNameFromMethodNameIfPossible(methodName)
.orElseThrow(() -> new IllegalArgumentException("Method " + methodName + " must start with get or is in order to store localized label value!"));
}
/**
* Returns property name from getter method (i.e. method starting with get/is).
*/
@Nonnull
public static Optional getPropertyNameFromMethodNameIfPossible(@Nonnull String methodName) {
if (methodName.startsWith("get") && methodName.length() > 3 && Character.isUpperCase(methodName.charAt(3))) {
return of(StringUtils.uncapitalize(methodName.substring(3)));
} else if (methodName.startsWith("is") && methodName.length() > 2 && Character.isUpperCase(methodName.charAt(2))) {
return of(StringUtils.uncapitalize(methodName.substring(2)));
} else if (methodName.startsWith("set") && methodName.length() > 3 && Character.isUpperCase(methodName.charAt(3))) {
return of(StringUtils.uncapitalize(methodName.substring(3)));
} else {
return empty();
}
}
/**
* Returns true if method matches the getter format.
*/
public static boolean isGetter(@Nonnull String methodName) {
if (methodName.startsWith("get") && methodName.length() > 3 && Character.isUpperCase(methodName.charAt(3))) {
return true;
} else if (methodName.startsWith("is") && methodName.length() > 2 && Character.isUpperCase(methodName.charAt(2))) {
return true;
} else {
return false;
}
}
/**
* Returns true if method matches the setter format.
*/
public static boolean isSetter(@Nonnull String methodName) {
if (methodName.startsWith("set") && methodName.length() > 3 && Character.isUpperCase(methodName.charAt(3))) {
return true;
} else {
return false;
}
}
/**
* When passed type is an array, its component type is returned, otherwise the type is returned back without any
* additional logic attached.
*/
@Nonnull
public static Class getSimpleType(@Nonnull Class ofType) {
return ofType.isArray() ? ofType.getComponentType() : ofType;
}
/**
* Returns all interfaces declared by passed `aClass` or transitively by those interfaces.
*/
private static Collection> getAllDeclaredInterfaces(@Nonnull Class aClass) {
return Stream.of(
Arrays.stream(aClass.getInterfaces()),
Arrays.stream(aClass.getInterfaces()).flatMap(it -> getAllDeclaredInterfaces(it).stream())
)
.flatMap(it -> it)
.collect(Collectors.toList());
}
/**
* Return all interfaces that the given class implements as a Set,
* including ones implemented by superclasses.
* If the class itself is an interface, it gets returned as single interface.
*
* @param clazz the class to analyze for interfaces
* @return all interfaces that the given object implements
*/
@Nonnull
private static Set> getAllInterfacesForClassAsSet(@Nonnull Class clazz) {
Assert.notNull(clazz, "Class must not be null");
if (clazz.isInterface()) {
return singleton(clazz);
}
final Set> interfaces = new LinkedHashSet<>();
Class current = clazz;
while (current != null) {
final Class[] ifcs = current.getInterfaces();
for (Class ifc : ifcs) {
interfaces.addAll(getAllInterfacesForClassAsSet(ifc));
}
current = current.getSuperclass();
}
return interfaces;
}
private static void registerMethods(Map> cachedInformations, Set foundMethodAnnotations, Class tmpClass) {
for (Method method : tmpClass.getDeclaredMethods()) {
final Annotation[] someAnnotation = method.getAnnotations();
if (someAnnotation != null && someAnnotation.length > 0) {
method.setAccessible(true);
for (Annotation annotation : someAnnotation) {
final MethodAnnotationKey methodAnnotationKey = new MethodAnnotationKey(method.getName(), annotation);
if (!foundMethodAnnotations.contains(methodAnnotationKey)) {
List cachedAnnotations = cachedInformations.computeIfAbsent(method, k -> new LinkedList<>());
cachedAnnotations.add(annotation);
foundMethodAnnotations.add(methodAnnotationKey);
}
}
}
}
}
@Nullable
private static Class getRepeatableContainerAnnotation(Class annotationType) {
final Class containerAnnotation;
final Repeatable repeatable = annotationType.getAnnotation(Repeatable.class);
if (repeatable != null) {
containerAnnotation = repeatable.value();
} else {
containerAnnotation = null;
}
return containerAnnotation;
}
private static void addAnnotationIfMatches(Class annotationType, Class containerAnnotation, List fieldResult, Annotation annotation) {
if (annotationType.isInstance(annotation)) {
//noinspection unchecked
fieldResult.add((T) annotation);
}
if (ofNullable(containerAnnotation).map(it -> containerAnnotation.isInstance(annotation)).orElse(false)) {
final Annotation[] wrappedAnnotations;
try {
wrappedAnnotations = (Annotation[]) annotation.getClass().getMethod("value").invoke(annotation);
} catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
throw new IllegalArgumentException("Repeatable annotation unwind error: " + e.getMessage(), e);
}
for (Annotation wrappedAnnotation : wrappedAnnotations) {
//noinspection unchecked
fieldResult.add((T) wrappedAnnotation);
}
}
}
private static T getAnnotation(Class searchedAnnotationType, Class type, Deque> processedAnnotations) {
final Annotation[] annotations = type.getAnnotations();
for (Annotation annItem : annotations) {
if (searchedAnnotationType.isInstance(annItem)) {
//noinspection unchecked
return (T) annItem;
}
final Class annType = annItem.annotationType();
try {
processedAnnotations.push(annType);
final T innerAnnotation = annType.getAnnotation(searchedAnnotationType);
if (innerAnnotation == null) {
if (!processedAnnotations.contains(annType)) {
return getAnnotation(searchedAnnotationType, annType, processedAnnotations);
}
} else {
return innerAnnotation;
}
} finally {
processedAnnotations.pop();
}
}
return null;
}
private static List expand(Annotation[] annotation) {
if (ArrayUtils.isEmpty(annotation)) {
return emptyList();
} else {
final List allAnnotations = new LinkedList<>();
for (Annotation annItem : annotation) {
if (!annItem.annotationType().getName().startsWith("java.lang.annotation")) {
allAnnotations.add(annItem);
allAnnotations.addAll(expand(annItem.annotationType().getAnnotations()));
}
}
return allAnnotations;
}
}
private static Map mapGettersAndSetters(Class onClass) {
final Map result = new LinkedHashMap<>();
if (onClass.isRecord()) {
final RecordComponent[] recordComponents = onClass.getRecordComponents();
for (RecordComponent recordComponent : recordComponents) {
final String propertyName = recordComponent.getName();
final Field propertyField = mapField(onClass, propertyName);
registerPropertyDescriptor(result, recordComponent.getAccessor(), propertyName, false, propertyField);
}
} else {
final Method[] methods = onClass.getMethods();
for (Method method : methods) {
final String methodName = method.getName();
final String propertyName;
final boolean isGetter;
if (methodName.startsWith("get") && methodName.length() > 3 && Character.isUpperCase(methodName.charAt(3)) && !void.class.equals(method.getReturnType()) && method.getParameterCount() == 0) {
propertyName = StringUtils.uncapitalize(methodName.substring(3));
isGetter = true;
} else if (methodName.startsWith("is") && methodName.length() > 2 && Character.isUpperCase(methodName.charAt(2)) && !void.class.equals(method.getReturnType()) && method.getParameterCount() == 0) {
propertyName = StringUtils.uncapitalize(methodName.substring(2));
isGetter = true;
} else if (methodName.startsWith("set") && methodName.length() > 3 && Character.isUpperCase(methodName.charAt(3)) && void.class.equals(method.getReturnType()) && method.getParameterCount() == 1) {
propertyName = StringUtils.uncapitalize(methodName.substring(3));
isGetter = false;
} else {
continue;
}
final Field propertyField = mapField(onClass, propertyName);
registerPropertyDescriptor(result, method, propertyName, isGetter, propertyField);
}
}
return result;
}
private static Map> mapConstructors(Class onClass) {
final HashMap> mappedConstructors = new HashMap<>();
for (Constructor constructor : onClass.getConstructors()) {
try {
final Parameter[] parameters = constructor.getParameters();
Assert.isTrue(
parameters.length == constructor.getParameterTypes().length,
"Source file was not compiled with -parameters option. There are no names for constructor arguments!"
);
final LinkedHashSet arguments = new LinkedHashSet<>(parameters.length);
for (final Parameter parameter : parameters) {
arguments.add(
new ArgumentKey(
parameter.getName(),
parameter.getType()
)
);
}
mappedConstructors.put(
new ConstructorKey(onClass, arguments),
constructor
);
} catch (Exception ex) {
log.error(
"Constructor " + constructor.toGenericString() + " on class " + onClass +
" is unusable for reflection access due to: " + ex.getMessage()
);
}
}
return mappedConstructors;
}
/**
* Returns getter method for all setters and also getters that return value injected by constructor. If there are
* multiple constructors only "best" one is returned - i.e. the one that allows to inject biggest count of properties
* without setters. If there are multiple such constructors - none is used, because it represents ambiguous situation.
*/
@Nullable
private static List getGettersForSettersAndBestConstructor(Stream simplePropertiesWithSetter, Map propertiesWithoutSetter, List constructorsByBestFit, WeightedConstructorKey bestConstructor, long bestFitWeight) {
if (constructorsByBestFit.size() == 1) {
return returnMethodsForBestConstructor(
simplePropertiesWithSetter, propertiesWithoutSetter, bestConstructor
);
} else {
final Set collidingConstructors = new HashSet<>(constructorsByBestFit.size());
collidingConstructors.add(bestConstructor);
for (int i = 1; i < constructorsByBestFit.size(); i++) {
final WeightedConstructorKey weightedConstructorKey = constructorsByBestFit.get(i);
if (weightedConstructorKey.weight() == bestFitWeight) {
collidingConstructors.add(weightedConstructorKey);
} else {
return returnMethodsForBestConstructor(
simplePropertiesWithSetter, propertiesWithoutSetter, bestConstructor
);
}
}
log.error(
"Multiple constructors fit to read-only getters: " +
collidingConstructors
.stream()
.map(WeightedConstructorKey::toString)
.collect(Collectors.joining(", "))
);
}
return null;
}
/**
* Method returns all getter methods that match properties injected by constructor arguments along with all getter
* methods that have corresponding setter method.
*/
@Nonnull
private static List returnMethodsForBestConstructor(Stream simplePropertiesWithSetter, Map propertiesInjectedByConstructor, WeightedConstructorKey bestConstructor) {
final Set constructorArgs = bestConstructor.constructorKey().arguments()
.stream()
.map(ArgumentKey::getName)
.collect(Collectors.toSet());
final Stream propertiesWithConstructor = propertiesInjectedByConstructor
.entrySet()
.stream()
.filter(it -> constructorArgs.contains(it.getKey()))
.map(Entry::getValue);
return Stream.concat(
simplePropertiesWithSetter,
propertiesWithConstructor
).collect(Collectors.toList());
}
/**
* Finds field on passed class. If field is not found, it traverses through super classes to find it.
*/
private static Field mapField(@Nonnull Class onClass, @Nonnull String propertyName) {
try {
return onClass.getDeclaredField(propertyName);
} catch (NoSuchFieldException e) {
if (Object.class.equals(onClass) || onClass.getSuperclass() == null) {
return null;
} else {
return mapField(onClass.getSuperclass(), propertyName);
}
}
}
/**
* Goes through inheritance chain and looks up for annotations.
*/
private static void getFieldAnnotationsThroughSuperClasses(Map> annotations, Set foundFields, Class examinedClass) {
do {
for (Field field : examinedClass.getDeclaredFields()) {
final Annotation[] someAnnotation = field.getAnnotations();
if (someAnnotation != null && someAnnotation.length > 0 && !foundFields.contains(field.getName())) {
field.setAccessible(true);
annotations.put(field, expand(someAnnotation));
foundFields.add(field.getName());
}
}
examinedClass = examinedClass.getSuperclass();
} while (examinedClass != null && !Objects.equals(Object.class, examinedClass));
}
private static void processRepeatableAnnotations(Set annotations, Set> alreadyDetectedAnnotations, Set> addedAnnotationsInThisRound, Annotation annotation) {
final Class containerAnnotation = getRepeatableContainerAnnotation(annotation.annotationType());
if (!alreadyDetectedAnnotations.contains(annotation.annotationType()) && (containerAnnotation == null || !alreadyDetectedAnnotations.contains(containerAnnotation))) {
annotations.add(annotation);
addedAnnotationsInThisRound.add(annotation.annotationType());
ofNullable(containerAnnotation).ifPresent(addedAnnotationsInThisRound::add);
}
}
/**
* Registers property descriptor.
*/
private static void registerPropertyDescriptor(Map result, Method method, String propertyName, boolean isGetter, Field propertyField) {
final PropertyDescriptor existingTuple = result.get(propertyName);
if (existingTuple == null) {
result.put(
propertyName,
new PropertyDescriptor(
propertyField,
isGetter ? method : null,
isGetter ? null : method
)
);
} else {
result.put(
propertyName,
new PropertyDescriptor(
propertyField,
isGetter ? method : existingTuple.getter(),
isGetter ? existingTuple.setter() : method
)
);
}
}
/**
* Method finds default (non-arg) constructor of the target class.
*/
@Nonnull
public Constructor findDefaultConstructor(@Nonnull Class onClass) {
final Map> cachedConstructors = mapAndCacheConstructors(onClass);
final Constructor defaultConstructor = cachedConstructors.get(new ConstructorKey(onClass, emptySet()));
Assert.notNull(defaultConstructor, "No non-arg constructor found on class: " + onClass);
//noinspection unchecked
return (Constructor) defaultConstructor;
}
/**
* Method finds default (non-arg) constructor of the target class.
*/
@Nullable
public Constructor findAnyMatchingConstructor(@Nonnull Class onClass, Set propertyNames) {
final Map> cachedConstructors = mapAndCacheConstructors(onClass);
for (Entry> entry : cachedConstructors.entrySet()) {
final ConstructorKey constructorKey = entry.getKey();
if (propertyNames.containsAll(constructorKey.arguments())) {
//noinspection unchecked
return (Constructor) entry.getValue();
}
}
return null;
}
/**
* Method finds any constructor that match any combination of passed property names of the target class.
*/
@Nonnull
public Constructor findConstructor(@Nonnull Class onClass, Set propertyNames) {
final Map> cachedConstructors = mapAndCacheConstructors(onClass);
final Constructor argConstructor = cachedConstructors.get(new ConstructorKey(onClass, propertyNames));
Assert.isTrue(
argConstructor != null,
"Constructor that would inject properties " +
propertyNames
.stream()
.map(ArgumentKey::getName)
.collect(Collectors.joining(", ")) +
" not found on class: " + onClass
);
//noinspection unchecked
return (Constructor) argConstructor;
}
/**
* Returns appropriate field for passed propertyName.
*/
@Nullable
public Field findPropertyField(@Nonnull Class onClass, @Nonnull String propertyName) {
Map index = propertiesCache.get(onClass);
if (index == null) {
index = mapAndCacheGettersAndSetters(onClass);
}
final PropertyDescriptor getterSetterTuple = index.get(propertyName);
return getterSetterTuple == null ? null : getterSetterTuple.field();
}
/**
* Returns appropriate setter method for passed getter method.
*/
@Nullable
public Method findSetter(@Nonnull Class onClass, @Nonnull Method method) {
final String propertyName = getPropertyNameFromMethodName(method.getName());
return findSetter(onClass, propertyName);
}
/**
* Returns appropriate setter method for passed getter method.
*/
@Nullable
public Method findSetter(@Nonnull Class onClass, @Nonnull String propertyName) {
Map index = propertiesCache.get(onClass);
if (index == null) {
index = mapAndCacheGettersAndSetters(onClass);
}
final PropertyDescriptor getterSetterTuple = index.get(propertyName);
return getterSetterTuple == null ? null : getterSetterTuple.setter();
}
/**
* Returns all getters found on particular class or its superclasses. Ie. all getters that can be successfully called
* on class instance.
*/
@Nonnull
public Collection findAllGetters(@Nonnull Class onClass) {
Map index = propertiesCache.get(onClass);
if (index == null) {
index = mapAndCacheGettersAndSetters(onClass);
}
return index.values()
.stream()
.map(PropertyDescriptor::getter)
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
/**
* Returns all getters that have corresponding setter for them (i.e. property is read/write) found on particular
* class or its superclasses. Ie. returns all getters that can be successfully called on class instance and which
* value can be written back.
*/
@Nonnull
public Collection findAllGettersHavingCorrespondingSetter(@Nonnull Class onClass) {
Map index = propertiesCache.get(onClass);
if (index == null) {
index = mapAndCacheGettersAndSetters(onClass);
}
return index.values()
.stream()
.filter(it -> it.getter() != null && it.setter() != null)
.map(PropertyDescriptor::getter)
.collect(Collectors.toList());
}
/**
* Returns all getters that have corresponding setter for them (i.e. property is read/write) found on particular
* class or its superclasses. Ie. returns all getters that can be successfully called on class instance and which
* value can be written back.
*
* It also returns getters that do have not appropriate setter, but target property that is possible to pass as
* constructor argument of the class.
*/
@Nonnull
public Collection findAllGettersHavingCorrespondingSetterOrConstructorArgument(@Nonnull Class onClass) {
final List cachedGetters = gettersWithCorrespondingSetterOrConstructor.get(onClass);
if (cachedGetters == null) {
final List resolvedGetters = resolveGettersCorrespondingToSettersOrConstructorArgument(onClass);
if (cachingBehaviour == ReflectionCachingBehaviour.CACHE) {
gettersWithCorrespondingSetterOrConstructor.put(onClass, resolvedGetters);
}
return resolvedGetters;
} else {
return cachedGetters;
}
}
/**
* Returns all getters that have passed `annotationType` defined on them.
*/
public List findAllGettersHavingAnnotation(@Nonnull Class onClass, @Nonnull Class annotationType) {
return findAllGetters(onClass)
.stream()
.filter(it -> getAnnotationInstance(it, annotationType) != null)
.toList();
}
/**
* Returns all setters found on particular class or its superclasses. Ie. all setters that can be successfully called
* on class instance.
*/
@Nonnull
public Collection findAllSetters(@Nonnull Class onClass) {
Map index = propertiesCache.get(onClass);
if (index == null) {
index = mapAndCacheGettersAndSetters(onClass);
}
return index.values()
.stream()
.map(PropertyDescriptor::setter)
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
/**
* Returns all setters that have corresponding getter for them (i.e. property is read/write) found on particular
* class or its superclasses. Ie. returns all setters that can be successfully called on class instance and which
* value can be read back.
*/
@Nonnull
public Collection findAllSettersHavingCorrespondingSetter(@Nonnull Class onClass) {
Map index = propertiesCache.get(onClass);
if (index == null) {
index = mapAndCacheGettersAndSetters(onClass);
}
return index.values()
.stream()
.filter(it -> it.getter() != null && it.setter() != null)
.map(PropertyDescriptor::setter)
.collect(Collectors.toList());
}
/**
* Returns appropriate getter method for passed getter method.
*/
@Nullable
public Method findGetter(@Nonnull Class onClass, @Nonnull Method method) {
final String propertyName = getPropertyNameFromMethodName(method.getName());
return findGetter(onClass, propertyName);
}
/**
* Returns appropriate getter method for passed getter method.
*/
@Nullable
public Method findGetter(@Nonnull Class onClass, @Nonnull String propertyName) {
Map index = propertiesCache.get(onClass);
if (index == null) {
index = mapAndCacheGettersAndSetters(onClass);
}
final PropertyDescriptor getterSetterTuple = index.get(propertyName);
return getterSetterTuple == null ? null : getterSetterTuple.getter();
}
/**
* Returns all annotations from the class. Annotations are also looked up in superclass/implements hierarchy.
* Annotations on annotation are also taken into account.
*/
@Nonnull
public List getClassAnnotations(@Nonnull Class type) {
List cachedInformations = classCache.get(type);
if (cachedInformations == null) {
final Set informations = new LinkedHashSet<>(16);
final Set> alreadyDetectedAnnotations = new HashSet<>();
if (!Objects.equals(Object.class, type)) {
getClassAnnotationsThroughSuperClasses(informations, type, alreadyDetectedAnnotations);
}
cachedInformations = new ArrayList<>(informations);
if (cachingBehaviour == ReflectionCachingBehaviour.CACHE) {
classCache.put(type, cachedInformations);
}
}
return cachedInformations;
}
/**
* Returns closest annotation of certain type from the class. Annotations are also looked up in superclass/implements hierarchy.
* Annotations on annotation are also taken into account.
*/
@Nullable
public T getClassAnnotation(@Nonnull Class type, @Nonnull Class annotationType) {
final List result = getClassAnnotations(type, annotationType);
return result.isEmpty() ? null : result.get(0);
}
/**
* Returns the class the passed annotation belongs to.
*
* @param clazz class to start the search from
* @param annotationInstance annotation instance to find the origin class for
* @return the class the passed annotation belongs to
* @throws IllegalArgumentException if the annotation is not present on the class
*/
@Nonnull
public Class findOriginClass(@Nonnull Class clazz, @Nonnull Annotation annotationInstance) {
Class examinedClass = clazz;
do {
final Annotation[] someAnnotation = examinedClass.getAnnotations();
if (someAnnotation.length > 0) {
for (Annotation annotation : expand(someAnnotation)) {
if (annotation == annotationInstance) {
return examinedClass;
}
}
}
for (Class implementedInterface : examinedClass.getInterfaces()) {
final List interfaceAnnotation = getClassAnnotations(implementedInterface);
if (!interfaceAnnotation.isEmpty()) {
for (Annotation annotation : expand(interfaceAnnotation.toArray(new Annotation[0]))) {
if (annotation == annotationInstance) {
return implementedInterface;
}
}
}
}
examinedClass = examinedClass.getSuperclass();
} while (examinedClass != null && !Objects.equals(Object.class, examinedClass));
throw new IllegalArgumentException("Annotation " + annotationInstance + " is not present on class " + clazz + "!");
}
/**
* Returns all annotations of certain type from the class. Annotations are also looked up in superclass/implements hierarchy.
* Annotations on annotation are also taken into account.
*/
@Nonnull
public List getClassAnnotations(@Nonnull Class type, @Nonnull Class annotationType) {
final List cachedInformation = getClassAnnotations(type);
final List result = new ArrayList<>(cachedInformation.size());
final Class containerAnnotation = getRepeatableContainerAnnotation(annotationType);
for (Annotation annotation : cachedInformation) {
addAnnotationIfMatches(annotationType, containerAnnotation, result, annotation);
}
return result;
}
/**
* Returns true if method has at least one annotation in the same package as the passed annotation.
*/
public boolean hasAnnotationInSamePackage(@Nonnull Method method, @Nonnull Class annotation) {
return methodSamePackageAnnotation.computeIfAbsent(
new MethodAndPackage(method, annotation.getPackage()),
tuple -> Arrays.stream(tuple.method().getAnnotations())
.anyMatch(it -> Objects.equals(it.annotationType().getPackage(), tuple.annotationPackage()))
);
}
/**
* Returns true if field has at least one annotation in the same package as the passed annotation.
*/
public boolean hasAnnotationInSamePackage(@Nonnull Field field, @Nonnull Class annotation) {
return fieldSamePackageAnnotation.computeIfAbsent(
new FieldAndPackage(field, annotation.getPackage()),
tuple -> Arrays.stream(tuple.field().getAnnotations())
.anyMatch(it -> Objects.equals(it.annotationType().getPackage(), tuple.annotationPackage()))
);
}
/**
* Returns true if method has at least one annotation in the same package as the passed annotation.
*/
public boolean hasAnnotationForPropertyInSamePackage(@Nonnull Method method, @Nonnull Class annotation) {
if (hasAnnotationInSamePackage(method, annotation)) {
return true;
} else {
final Optional propertyName = getPropertyNameFromMethodNameIfPossible(method.getName());
if (propertyName.isPresent()) {
final Field propertyField = findPropertyField(method.getDeclaringClass(), propertyName.get());
if (propertyField != null && hasAnnotationInSamePackage(propertyField, annotation)) {
return true;
}
// try to find annotation on opposite method
if (isGetter(method.getName())) {
return ofNullable(findSetter(method.getDeclaringClass(), method))
.map(setter -> hasAnnotationForPropertyInSamePackageShallow(setter, annotation))
.orElse(false);
} else if (isSetter(method.getName())) {
return ofNullable(findGetter(method.getDeclaringClass(), method))
.map(getter -> hasAnnotationForPropertyInSamePackageShallow(getter, annotation))
.orElse(false);
} else {
return false;
}
}
return false;
}
}
/**
* Returns index of all fields with list of annotations used on them. Fields are also looked up in superclass
* hierarchy. Annotations on annotation are also taken into account.
*/
@Nonnull
public Map> getFields(@Nonnull Class type) {
Map> cachedInformations = fieldCache.get(type);
if (cachedInformations == null) {
cachedInformations = new LinkedHashMap<>(16);
final Set foundFields = new HashSet<>();
if (!Objects.equals(Object.class, type)) {
getFieldAnnotationsThroughSuperClasses(cachedInformations, foundFields, type);
}
if (cachingBehaviour == ReflectionCachingBehaviour.CACHE) {
fieldCache.put(type, cachedInformations);
}
}
return cachedInformations;
}
/**
* Returns index of all fields with certain type of annotation. Fields are also looked up in superclass hierarchy.
* Annotations on annotation are also taken into account.
*/
@Nonnull
public Map> getFields(@Nonnull Class type, @Nonnull Class annotationType) {
final Map> cachedInformations = getFields(type);
final Map> result = createLinkedHashMap(cachedInformations.size());
final Class containerAnnotation = getRepeatableContainerAnnotation(annotationType);
for (Entry> entry : cachedInformations.entrySet()) {
final List fieldResult = new LinkedList<>();
for (Annotation annotation : entry.getValue()) {
addAnnotationIfMatches(annotationType, containerAnnotation, fieldResult, annotation);
}
if (!fieldResult.isEmpty()) {
result.put(entry.getKey(), fieldResult);
}
}
return result;
}
/**
* Returns index of all methods with list of annotations used on them. Methods are also looked up in superclass
* and implements hierarchy. Annotations on annotation are also taken into account.
*/
@Nonnull
public Map> getMethods(@Nonnull Class type) {
Map> cachedInformations = methodCache.get(type);
if (cachedInformations == null) {
cachedInformations = new LinkedHashMap<>(16);
final Set foundMethodAnnotations = new HashSet<>();
Class tmpClass = type;
if (!Objects.equals(Object.class, tmpClass)) {
do {
registerMethods(cachedInformations, foundMethodAnnotations, tmpClass);
tmpClass = tmpClass.getSuperclass();
} while (tmpClass != null && !Objects.equals(Object.class, tmpClass));
}
final Set> interfaces = getAllImplementedInterfaces(type);
for (Class anInterface : interfaces) {
registerMethods(cachedInformations, foundMethodAnnotations, anInterface);
}
if (cachingBehaviour == ReflectionCachingBehaviour.CACHE) {
methodCache.put(type, cachedInformations);
}
}
return cachedInformations;
}
/**
* Returns index of all methods with certain type of annotation. Methods are also looked up in superclass / implements
* hierarchy. Annotations on annotation are also taken into account.
*/
@Nonnull
public Map> getMethods(@Nonnull Class type, @Nonnull Class annotationType) {
final Map> cachedInformation = getMethods(type);
final Map> result = createLinkedHashMap(cachedInformation.size());
final Class containerAnnotation = getRepeatableContainerAnnotation(annotationType);
for (Entry> entry : cachedInformation.entrySet()) {
final List methodResult = new LinkedList<>();
for (Annotation annotation : entry.getValue()) {
addAnnotationIfMatches(annotationType, containerAnnotation, methodResult, annotation);
}
if (!methodResult.isEmpty()) {
result.put(entry.getKey(), methodResult);
}
}
return result;
}
/**
* Returns annotation of certain type on certain field.
* Annotations on annotation are also taken into account.
* First matching annotation is returned.
*/
@Nullable
public T getAnnotationInstance(@Nonnull Field field, @Nonnull Class annotationType) {
final T annotation = field.getAnnotation(annotationType);
if (annotation == null) {
for (Annotation fieldAnnotation : field.getAnnotations()) {
final T result = getAnnotation(annotationType, fieldAnnotation.annotationType(), new LinkedList<>());
if (result != null) {
return result;
}
}
return null;
} else {
return annotation;
}
}
/**
* Returns annotation of certain type on certain method.
* Annotations on annotation are also taken into account.
* First matching annotation is returned.
*/
@Nullable
public T getAnnotationInstance(@Nonnull Method method, @Nonnull Class annotationType) {
final T annotation = method.getAnnotation(annotationType);
if (annotation == null) {
for (Annotation fieldAnnotation : method.getAnnotations()) {
final T result = getAnnotation(annotationType, fieldAnnotation.annotationType(), new LinkedList<>());
if (result != null) {
return result;
}
}
return null;
} else {
return annotation;
}
}
/**
* Returns annotation of certain type on certain method. If no annotation is found on the method, annotation is
* looked up on field of appropriate property name.
* Annotations on annotation are also taken into account.
* First matching annotation is returned.
*/
@Nullable
public T getAnnotationInstanceForProperty(@Nonnull Method method, @Nonnull Class annotationType) {
return ofNullable(getAnnotationInstance(method, annotationType))
.orElseGet(() -> {
final Optional propertyName = getPropertyNameFromMethodNameIfPossible(method.getName());
return propertyName
.map(
it -> ofNullable(getAnnotationInstanceForProperty(method.getDeclaringClass(), it, annotationType))
.orElseGet(() -> {
// try to find annotation on opposite method
if (isGetter(method.getName())) {
return ofNullable(findSetter(method.getDeclaringClass(), it))
.map(setter -> getAnnotationInstance(setter, annotationType))
.orElse(null);
} else if (isSetter(method.getName())) {
return ofNullable(findGetter(method.getDeclaringClass(), it))
.map(getter -> getAnnotationInstance(getter, annotationType))
.orElse(null);
} else {
return null;
}
})
)
.orElse(null);
});
}
/**
* Returns annotation of certain type on field of certain name in declaring class.
* Annotations on annotation are also taken into account.
* First matching annotation is returned.
*/
@Nullable
public T getAnnotationInstanceForProperty(
@Nonnull Class declaringClass,
@Nonnull String propertyName,
@Nonnull Class annotationType
) {
final Field propertyField = findPropertyField(declaringClass, propertyName);
if (propertyField == null) {
return null;
} else {
final Map> annotatedFields = getFields(declaringClass, annotationType);
final List annotations = annotatedFields.get(propertyField);
if (annotations == null || annotations.isEmpty()) {
return null;
} else {
return annotations.get(0);
}
}
}
/**
* Extracts specific class for generic one.
*/
@Nonnull
public Class extractGenericType(@Nonnull Type genericReturnType, int position) {
Assert.isTrue(genericReturnType instanceof ParameterizedType, "Cannot infer generic class from: " + genericReturnType);
final ParameterizedType parameterizedType = (ParameterizedType) genericReturnType;
final Type actualType = parameterizedType.getActualTypeArguments()[position];
Assert.isTrue(actualType instanceof Class, "Cannot infer generic class from: " + genericReturnType);
//noinspection unchecked
return (Class) actualType;
}
/**
* Returns set of all interfaces that are implemented by passed class.
*/
@Nonnull
public Set> getAllImplementedInterfaces(@Nonnull Class aClass) {
final Set> ifaces = interfacesCache.get(aClass);
if (ifaces == null) {
final Set> mainInterfaces = getAllInterfacesForClassAsSet(aClass);
final Set> resolvedIfaces = new HashSet<>(mainInterfaces);
mainInterfaces.forEach(it -> resolvedIfaces.addAll(getAllDeclaredInterfaces(it)));
if (cachingBehaviour == ReflectionCachingBehaviour.CACHE) {
interfacesCache.put(aClass, resolvedIfaces);
}
return resolvedIfaces;
}
return ifaces;
}
/**
* Applies logic in `extractor` lambda and caches result if caching is enabled for the combination of
* the `modelClass` and `cacheKey`.
*/
@Nullable
public T extractFromClass(@Nonnull Class modelClass, @Nonnull Object cacheKey, @Nonnull Function, T> extractor) {
if (cachingBehaviour == ReflectionCachingBehaviour.CACHE) {
//noinspection unchecked
return (T) extractorCache.computeIfAbsent(
modelClass, aClass -> new ConcurrentHashMap<>()
).computeIfAbsent(
cacheKey, key -> extractor.apply(modelClass)
);
} else {
return extractor.apply(modelClass);
}
}
/**
* Returns true if method has at least one annotation in the same package as the passed annotation.
* The shallow implementation avoids stack overflow when called from {@link #hasAnnotationForPropertyInSamePackage(Method, Class)}.
*/
private boolean hasAnnotationForPropertyInSamePackageShallow(@Nonnull Method method, @Nonnull Class annotation) {
if (hasAnnotationInSamePackage(method, annotation)) {
return true;
} else {
final Optional propertyName = getPropertyNameFromMethodNameIfPossible(method.getName());
if (propertyName.isPresent()) {
final Field propertyField = findPropertyField(method.getDeclaringClass(), propertyName.get());
if (propertyField != null && hasAnnotationInSamePackage(propertyField, annotation)) {
return true;
}
}
return false;
}
}
private Map mapAndCacheGettersAndSetters(@Nonnull Class onClass) {
final Map index = mapGettersAndSetters(onClass);
if (cachingBehaviour == ReflectionCachingBehaviour.CACHE) {
propertiesCache.put(onClass, index);
}
return index;
}
private Map> mapAndCacheConstructors(@Nonnull Class onClass) {
final Map> cachedResult = constructorCache.get(onClass);
if (cachedResult == null) {
final Map> index = mapConstructors(onClass);
if (cachingBehaviour == ReflectionCachingBehaviour.CACHE) {
constructorCache.put(onClass, index);
}
return index;
} else {
return cachedResult;
}
}
/**
* Computes list of all getter methods that match properties injected by constructor arguments along with all getter
* methods that have corresponding setter method.
*/
@Nonnull
private List resolveGettersCorrespondingToSettersOrConstructorArgument(@Nonnull Class onClass) {
final Map index = ofNullable(propertiesCache.get(onClass))
.orElseGet(() -> mapAndCacheGettersAndSetters(onClass));
final Stream simplePropertiesWithSetter = index.values()
.stream()
.filter(it -> it.getter() != null && it.setter() != null)
.map(PropertyDescriptor::getter);
final Map propertiesWithoutSetter = index.values()
.stream()
.filter(it -> it.getter() != null && it.setter() == null)
.collect(
Collectors.toMap(
it -> getPropertyNameFromMethodName(it.getter().getName()),
PropertyDescriptor::getter
)
);
final List constructorsByBestFit = weightAndSortAllConstructors(onClass, propertiesWithoutSetter);
if (constructorsByBestFit.isEmpty()) {
return simplePropertiesWithSetter.collect(Collectors.toList());
} else {
final WeightedConstructorKey bestConstructor = constructorsByBestFit.get(0);
final long bestFitWeight = bestConstructor.weight();
if (bestFitWeight > 0) {
final List gettersCombined = getGettersForSettersAndBestConstructor(
simplePropertiesWithSetter, propertiesWithoutSetter, constructorsByBestFit,
bestConstructor, bestFitWeight
);
if (gettersCombined != null) {
return gettersCombined;
}
}
return simplePropertiesWithSetter.collect(Collectors.toList());
}
}
/**
* Method will assign weight for each constructor. Weight is simple count of all arguments of the constructor argument
* that match properties without setter method. All types of the arguments must match and there must not be any other
* non-paired argument.
*/
@Nonnull
private List weightAndSortAllConstructors(@Nonnull Class onClass, Map propertiesInjectedByConstructor) {
return mapAndCacheConstructors(onClass)
.keySet()
.stream()
.map(it -> {
final WeightedConstructorKey weightedConstructorKey = new WeightedConstructorKey(
it,
it.arguments()
.stream()
.filter(x ->
ofNullable(propertiesInjectedByConstructor.get(x.getName()))
.filter(y -> y.getReturnType().isAssignableFrom(x.getType()))
.isPresent()
)
.count()
);
if (weightedConstructorKey.weight() == weightedConstructorKey.constructorKey().arguments().size()) {
return weightedConstructorKey;
} else {
return new WeightedConstructorKey(it, -1);
}
})
.sorted(Comparator.comparingLong(WeightedConstructorKey::weight).reversed())
.collect(Collectors.toList());
}
/**
* Goes through inheritance chain and looks up for annotations.
*/
private void getClassAnnotationsThroughSuperClasses(Set annotations, Class examinedClass, Set> alreadyDetectedAnnotations) {
do {
final Annotation[] someAnnotation = examinedClass.getAnnotations();
final Set> addedAnnotationsInThisRound = new HashSet<>();
if (someAnnotation.length > 0) {
for (Annotation annotation : expand(someAnnotation)) {
processRepeatableAnnotations(annotations, alreadyDetectedAnnotations, addedAnnotationsInThisRound, annotation);
}
}
for (Class implementedInterface : examinedClass.getInterfaces()) {
final List interfaceAnnotation = getClassAnnotations(implementedInterface);
if (!interfaceAnnotation.isEmpty()) {
annotations.addAll(expand(interfaceAnnotation.toArray(new Annotation[0])));
}
}
alreadyDetectedAnnotations.addAll(addedAnnotationsInThisRound);
examinedClass = examinedClass.getSuperclass();
} while (examinedClass != null && !Objects.equals(Object.class, examinedClass));
}
private record MethodAnnotationKey(String methodName, Annotation annotation) {
}
private record PropertyDescriptor(Field field, Method getter, Method setter) {
}
/**
* Constructor key identifies certain arg specific constructor.
*
* @param arguments this SHOULD BE LinkedHashSet implementation or emptySet
*/
private record ConstructorKey(Class type, Set arguments) {
@Override
public String toString() {
return type.getSimpleName() + "(" +
arguments.stream()
.map(it -> it.getType().getSimpleName() + " " + it.getName())
.collect(Collectors.joining(", "))
+ ")";
}
}
/**
* Contains constructor key and its weight (i.e. match with non-assigned properties).
*/
private record WeightedConstructorKey(ConstructorKey constructorKey, long weight) {
@Override
public String toString() {
return constructorKey.toString();
}
}
/**
* Argument key is used for looking up for constructors. Type is not used in equals and hash code because we would
* like to search by property name and type may not exactly match (i.e. it could be super type)
*/
@Data
@EqualsAndHashCode(of = "name")
public static class ArgumentKey {
private final String name;
private final Class type;
}
/**
* Cache key for {@link #hasAnnotationInSamePackage(Method, Class)}.
*/
private record MethodAndPackage(
@Nonnull Method method,
@Nonnull Package annotationPackage) {
}
/**
* Cache key for {@link #hasAnnotationForPropertyInSamePackage(Method, Class)}.
*/
private record FieldAndPackage(
@Nonnull Field field,
@Nonnull Package annotationPackage) {
}
}