All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.evitadb.utils.ReflectionLookup Maven / Gradle / Ivy

There is a newer version: 2024.9.3
Show newest version
/*
 *
 *                         _ _        ____  ____
 *               _____   _(_) |_ __ _|  _ \| __ )
 *              / _ \ \ / / | __/ _` | | | |  _ \
 *             |  __/\ 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) {

	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy