Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make usage of ClassGraphFacade replacable with custom logic in Reflec… #46

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,42 +23,27 @@
*/
package org.jeasy.random.util;

import java.util.Collections;
import java.util.List;

import io.github.classgraph.ClassGraph;
import io.github.classgraph.ClassInfoList;
import io.github.classgraph.ScanResult;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;

/**
* Facade for {@link io.github.classgraph.ClassGraph}. It is a separate class from {@link ReflectionUtils},
* so that the classpath scanning - which can take a few seconds - is only done when necessary.
*
* @author Pascal Schumacher (https://github.com/PascalSchumacher)
*/
abstract class ClassGraphFacade {

private static final ConcurrentHashMap<Class<?>, List<Class<?>>> typeToConcreteSubTypes = new ConcurrentHashMap<>();
private static final ScanResult scanResult = new ClassGraph().enableSystemJarsAndModules().enableClassInfo().scan();
class ClassGraphFacade {

/**
* Searches the classpath for all public concrete subtypes of the given interface or abstract class.
*
* @param type to search concrete subtypes of
* @return a list of all concrete subtypes found
*/
public static <T> List<Class<?>> getPublicConcreteSubTypesOf(final Class<T> type) {
return typeToConcreteSubTypes.computeIfAbsent(type, ClassGraphFacade::searchForPublicConcreteSubTypesOf);
}
private final static ScanResult scanResult = new ClassGraph().enableSystemJarsAndModules().enableClassInfo().scan();

private static <T> List<Class<?>> searchForPublicConcreteSubTypesOf(final Class<T> type) {
static List<Class<?>> searchForPublicConcreteSubTypesOf(final Class<?> type) {
String typeName = type.getName();
ClassInfoList subTypes = type.isInterface()
? scanResult.getClassesImplementing(typeName)
: scanResult.getSubclasses(typeName);
List<Class<?>> loadedSubTypes = subTypes
.filter(subType -> subType.isPublic() && !subType.isAbstract())
.loadClasses(true);
ClassInfoList subTypes = type.isInterface() ? scanResult.getClassesImplementing(typeName) : scanResult.getSubclasses(typeName);
List<Class<?>> loadedSubTypes = subTypes.filter(subType -> subType.isPublic() && !subType.isAbstract()).loadClasses(true);
return Collections.unmodifiableList(loadedSubTypes);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,24 @@
*/
package org.jeasy.random.util;

import static java.lang.String.format;
import static java.util.Arrays.asList;
import static java.util.Locale.ENGLISH;
import static org.jeasy.random.util.ConversionUtils.convertArguments;
import org.jeasy.random.annotation.RandomizerArgument;
import org.jeasy.random.ObjectCreationException;
import org.jeasy.random.api.Randomizer;
import org.objenesis.ObjenesisStd;

import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Stream;
import org.jeasy.random.ObjectCreationException;
import org.jeasy.random.annotation.RandomizerArgument;
import org.jeasy.random.api.Randomizer;
import org.objenesis.ObjenesisStd;

import static java.lang.String.format;
import static java.util.Arrays.asList;
import static java.util.Locale.ENGLISH;
import static java.util.stream.Collectors.toList;
import static org.jeasy.random.util.ConversionUtils.convertArguments;

/**
* Reflection utility methods.
Expand All @@ -50,7 +53,13 @@
*/
public final class ReflectionUtils {

private ReflectionUtils() {}
private static final ConcurrentHashMap<Class<?>, List<Class<?>>> typeToConcreteSubTypes = new ConcurrentHashMap<>();

// NOTE: indirection to avoid loading ClassGraphFacade class too early
private static volatile Function<Class<?>, List<Class<?>>> typeToConcreteSubTypesProvider = ReflectionUtils::defaultTypeToConcreteSubTypesProvider;

private ReflectionUtils() {
}

/**
* Create a dynamic proxy that adapts the given {@link Supplier} to a {@link Randomizer}.
Expand All @@ -60,6 +69,7 @@ private ReflectionUtils() {}
*/
@SuppressWarnings("unchecked")
public static <T> Randomizer<T> asRandomizer(final Supplier<T> supplier) {

class RandomizerProxy implements InvocationHandler {

private final Supplier<?> target;
Expand All @@ -80,10 +90,9 @@ public Object invoke(final Object proxy, final Method method, final Object[] arg
}

return (Randomizer<T>) Proxy.newProxyInstance(
Randomizer.class.getClassLoader(),
new Class[] { Randomizer.class },
new RandomizerProxy(supplier)
);
Randomizer.class.getClassLoader(),
new Class[]{Randomizer.class},
new RandomizerProxy(supplier));
}

/**
Expand Down Expand Up @@ -114,7 +123,7 @@ public static List<Field> getInheritedFields(Class<?> type) {
}

/**
* Set a value in a field of a target object. If the target object provides
* Set a value in a field of a target object. If the target object provides
* a setter for the field, this setter will be used. Otherwise, the field
* will be set using reflection.
*
Expand All @@ -123,8 +132,7 @@ public static List<Field> getInheritedFields(Class<?> type) {
* @param value value to set
* @throws IllegalAccessException if the property cannot be set
*/
public static void setProperty(final Object object, final Field field, final Object value)
throws IllegalAccessException, InvocationTargetException {
public static void setProperty(final Object object, final Field field, final Object value) throws IllegalAccessException, InvocationTargetException {
try {
Optional<Method> setter = getWriteMethod(field);
if (setter.isPresent()) {
Expand All @@ -146,8 +154,7 @@ public static void setProperty(final Object object, final Field field, final Obj
* @param value value to set
* @throws IllegalAccessException if the property cannot be set
*/
public static void setFieldValue(final Object object, final Field field, final Object value)
throws IllegalAccessException {
public static void setFieldValue(final Object object, final Field field, final Object value) throws IllegalAccessException {
boolean access = field.trySetAccessible();
field.set(object, value);
field.setAccessible(access);
Expand Down Expand Up @@ -175,8 +182,8 @@ public static Object getFieldValue(final Object object, final Field field) throw
* @return the wrapper type of the given primitive type
*/
public static Class<?> getWrapperType(Class<?> primitiveType) {
for (PrimitiveEnum p : PrimitiveEnum.values()) {
if (p.getType().equals(primitiveType)) {
for(PrimitiveEnum p : PrimitiveEnum.values()) {
if(p.getType().equals(primitiveType)) {
return p.getClazz();
}
}
Expand All @@ -192,8 +199,7 @@ public static Class<?> getWrapperType(Class<?> primitiveType) {
* @return true if the field is primitive and is set to the default value, false otherwise
* @throws IllegalAccessException if field cannot be accessed
*/
public static boolean isPrimitiveFieldWithDefaultValue(final Object object, final Field field)
throws IllegalAccessException {
public static boolean isPrimitiveFieldWithDefaultValue(final Object object, final Field field) throws IllegalAccessException {
Class<?> fieldType = field.getType();
if (!fieldType.isPrimitive()) {
return false;
Expand All @@ -202,14 +208,14 @@ public static boolean isPrimitiveFieldWithDefaultValue(final Object object, fina
if (fieldValue == null) {
return false;
}
if (fieldType.equals(boolean.class) && !((boolean) fieldValue)) {
if (fieldType.equals(boolean.class) && (boolean) fieldValue == false) {
return true;
}
if (fieldType.equals(byte.class) && (byte) fieldValue == (byte) 0) {
return true;
}
if (fieldType.equals(short.class) && (short) fieldValue == (short) 0) {
return true;
return true;
}
if (fieldType.equals(int.class) && (int) fieldValue == 0) {
return true;
Expand Down Expand Up @@ -328,12 +334,10 @@ public static boolean isPopulatable(final Type type) {
* @return true if the type should be introspected, false otherwise
*/
public static boolean isIntrospectable(final Class<?> type) {
return (
!isEnumType(type) &&
!isArrayType(type) &&
!(isCollectionType(type) && isJdkBuiltIn(type)) &&
!(isMapType(type) && isJdkBuiltIn(type))
);
return !isEnumType(type)
&& !isArrayType(type)
&& !(isCollectionType(type) && isJdkBuiltIn(type))
&& !(isMapType(type) && isJdkBuiltIn(type));
}

/**
Expand Down Expand Up @@ -373,7 +377,7 @@ public static boolean isJdkBuiltIn(final Class<?> type) {
* @return true if the type is parameterized, false otherwise
*/
public static boolean isParameterizedType(final Type type) {
return (type instanceof ParameterizedType && ((ParameterizedType) type).getActualTypeArguments().length > 0);
return type != null && type instanceof ParameterizedType && ((ParameterizedType) type).getActualTypeArguments().length > 0;
}

/**
Expand Down Expand Up @@ -404,7 +408,25 @@ public static boolean isTypeVariable(final Type type) {
* @return a list of all concrete subtypes found
*/
public static <T> List<Class<?>> getPublicConcreteSubTypesOf(final Class<T> type) {
return ClassGraphFacade.getPublicConcreteSubTypesOf(type);
return typeToConcreteSubTypes.computeIfAbsent(type, typeToConcreteSubTypesProvider);
}

/**
* Override the default implementation of ClassGraphFacade for searching the
* classpath for any concrete subtype of given interface or abstract class.
*
* @param typeToConcreteSubTypesProvider custom implementation
*/
public static void setPublicConcreteSubTypeProvider(
final Function<Class<?>, List<Class<?>>> typeToConcreteSubTypesProvider) {
if (typeToConcreteSubTypesProvider == null) {
throw new IllegalArgumentException();
}
ReflectionUtils.typeToConcreteSubTypesProvider = typeToConcreteSubTypesProvider;
}

private static List<Class<?>> defaultTypeToConcreteSubTypesProvider(final Class<?> cls) {
return ClassGraphFacade.searchForPublicConcreteSubTypesOf(cls);
}

/**
Expand All @@ -415,18 +437,12 @@ public static <T> List<Class<?>> getPublicConcreteSubTypesOf(final Class<T> type
* @return a list of types having the same parameterized types as the given type
*/
public static List<Class<?>> filterSameParameterizedTypes(final List<Class<?>> types, final Type type) {
if (type instanceof ParameterizedType parameterizedType) {
Type[] fieldArugmentTypes = parameterizedType.getActualTypeArguments();
if (type instanceof ParameterizedType) {
Type[] fieldArugmentTypes = ((ParameterizedType) type).getActualTypeArguments();
List<Class<?>> typesWithSameParameterizedTypes = new ArrayList<>();
for (Class<?> currentConcreteType : types) {
List<Type[]> actualTypeArguments = getActualTypeArgumentsOfGenericInterfaces(currentConcreteType);
typesWithSameParameterizedTypes.addAll(
actualTypeArguments
.stream()
.filter(currentTypeArguments -> Arrays.equals(fieldArugmentTypes, currentTypeArguments))
.map(currentTypeArguments -> currentConcreteType)
.toList()
);
typesWithSameParameterizedTypes.addAll(actualTypeArguments.stream().filter(currentTypeArguments -> Arrays.equals(fieldArugmentTypes, currentTypeArguments)).map(currentTypeArguments -> currentConcreteType).collect(toList()));
}
return typesWithSameParameterizedTypes;
}
Expand All @@ -442,9 +458,8 @@ public static List<Class<?>> filterSameParameterizedTypes(final List<Class<?>> t
* @return given annotation if field or read method has this annotation or null.
*/
public static <T extends Annotation> T getAnnotation(Field field, Class<T> annotationType) {
return field.getAnnotation(annotationType) == null
? getAnnotationFromReadMethod(getReadMethod(field).orElse(null), annotationType)
: field.getAnnotation(annotationType);
return field.getAnnotation(annotationType) == null ? getAnnotationFromReadMethod(getReadMethod(field).orElse(null),
annotationType) : field.getAnnotation(annotationType);
}

/**
Expand All @@ -456,10 +471,7 @@ public static <T extends Annotation> T getAnnotation(Field field, Class<T> annot
*/
public static boolean isAnnotationPresent(Field field, Class<? extends Annotation> annotationType) {
final Optional<Method> readMethod = getReadMethod(field);
return (
field.isAnnotationPresent(annotationType) ||
(readMethod.isPresent() && readMethod.get().isAnnotationPresent(annotationType))
);
return field.isAnnotationPresent(annotationType) || readMethod.isPresent() && readMethod.get().isAnnotationPresent(annotationType);
}

/**
Expand Down Expand Up @@ -503,9 +515,7 @@ public static Collection<?> createEmptyCollectionForType(Class<?> fieldType, int
Collection<?> collection;
try {
collection = (Collection<?>) fieldType.getDeclaredConstructor().newInstance();
} catch (
InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e
) {
} catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
if (fieldType.equals(ArrayBlockingQueue.class)) {
collection = new ArrayBlockingQueue<>(initialSize);
} else {
Expand Down Expand Up @@ -593,8 +603,8 @@ private static List<Type[]> getActualTypeArgumentsOfGenericInterfaces(final Clas
List<Type[]> actualTypeArguments = new ArrayList<>();
Type[] genericInterfaceTypes = type.getGenericInterfaces();
for (Type currentGenericInterfaceType : genericInterfaceTypes) {
if (currentGenericInterfaceType instanceof ParameterizedType parameterizedType) {
actualTypeArguments.add((parameterizedType).getActualTypeArguments());
if (currentGenericInterfaceType instanceof ParameterizedType) {
actualTypeArguments.add(((ParameterizedType) currentGenericInterfaceType).getActualTypeArguments());
}
}
return actualTypeArguments;
Expand All @@ -604,47 +614,29 @@ private static List<Type[]> getActualTypeArgumentsOfGenericInterfaces(final Clas
public static <T> Randomizer<T> newInstance(final Class<T> type, final RandomizerArgument[] randomizerArguments) {
try {
if (notEmpty(randomizerArguments)) {
Optional<Constructor<?>> matchingConstructor = Stream
.of(type.getConstructors())
.filter(constructor ->
hasSameArgumentNumber(constructor, randomizerArguments) &&
hasSameArgumentTypes(constructor, randomizerArguments)
)
.findFirst();
Optional<Constructor<?>> matchingConstructor = Stream.of(type.getConstructors())
.filter(constructor -> hasSameArgumentNumber(constructor, randomizerArguments) &&
hasSameArgumentTypes(constructor, randomizerArguments))
.findFirst();
if (matchingConstructor.isPresent()) {
return (Randomizer<T>) matchingConstructor.get().newInstance(convertArguments(randomizerArguments));
}
}
return (Randomizer<T>) type.getDeclaredConstructor().newInstance();
} catch (
IllegalAccessException | InvocationTargetException | InstantiationException | NoSuchMethodException e
) {
throw new ObjectCreationException(
format(
"Could not create Randomizer of type: %s with constructor arguments: %s",
type,
Arrays.toString(randomizerArguments)
),
e
);
} catch (IllegalAccessException | InvocationTargetException | InstantiationException | NoSuchMethodException e) {
throw new ObjectCreationException(format("Could not create Randomizer of type: %s with constructor arguments: %s", type, Arrays.toString(randomizerArguments)), e);
}
}

private static boolean notEmpty(final RandomizerArgument[] randomizerArguments) {
return randomizerArguments != null && randomizerArguments.length > 0;
}

private static boolean hasSameArgumentNumber(
final Constructor<?> constructor,
final RandomizerArgument[] randomizerArguments
) {
private static boolean hasSameArgumentNumber(final Constructor<?> constructor, final RandomizerArgument[] randomizerArguments) {
return constructor.getParameterCount() == randomizerArguments.length;
}

private static boolean hasSameArgumentTypes(
final Constructor<?> constructor,
final RandomizerArgument[] randomizerArguments
) {
private static boolean hasSameArgumentTypes(final Constructor<?> constructor, final RandomizerArgument[] randomizerArguments) {
Class<?>[] constructorParameterTypes = constructor.getParameterTypes();
for (int i = 0; i < randomizerArguments.length; i++) {
if (!constructorParameterTypes[i].isAssignableFrom(randomizerArguments[i].type())) {
Expand All @@ -654,4 +646,8 @@ private static boolean hasSameArgumentTypes(
}
return true;
}




}