From 9324e5847e309e76436b2bbee1b903142d99dab8 Mon Sep 17 00:00:00 2001 From: Nicolas Pepin-Perreault Date: Wed, 2 Mar 2022 18:25:41 +0100 Subject: [PATCH 1/2] feat: introduce TypeResolver API Introduces a new `TypeResolver` interface to the public API. This interface's default implementation is in `ReflectionUtils`, which is then delegated to the static helpers in `ClassGraphFacade`. This means the new feature remains backwards compatible with previous behavior. Users can now specify a new parameter, `EasyRandomParameters#typeResolver(TypeResolver)`, which lets them specify how concrete types will be resolved without coupling the library to `ClassGraph` in any way, though users can still use `ClassGraph` to implement it themselves. An internal proxy class is used to wrap custom instances of `TypeResolver` set via the parameters. This is mostly because when creating the default `ObjenesisObjectFactory`, we need to pass it the current type resolver. However, it may get modified later on. So instead, we pass a proxy, which we can always modify thereafter. The proxy is not accessible to the user, so the only way to modify it is via the parameters. --- .../java/org/jeasy/random/EasyRandom.java | 3 +- .../jeasy/random/EasyRandomParameters.java | 28 ++++++++++- .../java/org/jeasy/random/FieldPopulator.java | 9 +++- .../jeasy/random/ObjenesisObjectFactory.java | 14 +++++- .../org/jeasy/random/api/TypeResolver.java | 42 ++++++++++++++++ .../jeasy/random/util/TypeResolverProxy.java | 48 +++++++++++++++++++ .../org/jeasy/random/FieldPopulatorTest.java | 4 +- 7 files changed, 140 insertions(+), 8 deletions(-) create mode 100644 easy-random-core/src/main/java/org/jeasy/random/api/TypeResolver.java create mode 100644 easy-random-core/src/main/java/org/jeasy/random/util/TypeResolverProxy.java diff --git a/easy-random-core/src/main/java/org/jeasy/random/EasyRandom.java b/easy-random-core/src/main/java/org/jeasy/random/EasyRandom.java index 040d750db..2e1ee545f 100644 --- a/easy-random-core/src/main/java/org/jeasy/random/EasyRandom.java +++ b/easy-random-core/src/main/java/org/jeasy/random/EasyRandom.java @@ -83,7 +83,8 @@ public EasyRandom(final EasyRandomParameters easyRandomParameters) { enumRandomizersByType = new ConcurrentHashMap<>(); fieldPopulator = new FieldPopulator(this, this.randomizerProvider, arrayPopulator, - collectionPopulator, mapPopulator, optionalPopulator); + collectionPopulator, mapPopulator, optionalPopulator, + easyRandomParameters.getTypeResolver()); exclusionPolicy = easyRandomParameters.getExclusionPolicy(); parameters = easyRandomParameters; } diff --git a/easy-random-core/src/main/java/org/jeasy/random/EasyRandomParameters.java b/easy-random-core/src/main/java/org/jeasy/random/EasyRandomParameters.java index 991647685..c601cfa9a 100644 --- a/easy-random-core/src/main/java/org/jeasy/random/EasyRandomParameters.java +++ b/easy-random-core/src/main/java/org/jeasy/random/EasyRandomParameters.java @@ -33,6 +33,7 @@ import java.time.*; import java.util.*; import java.util.function.Predicate; +import org.jeasy.random.util.TypeResolverProxy; import static java.lang.String.format; import static java.time.ZonedDateTime.of; @@ -107,6 +108,7 @@ public class EasyRandomParameters { private RandomizerProvider randomizerProvider; // internal params + private final TypeResolverProxy typeResolverProxy; private CustomRandomizerRegistry customRandomizerRegistry; private ExclusionRandomizerRegistry exclusionRandomizerRegistry; private Set userRegistries; @@ -135,7 +137,8 @@ public EasyRandomParameters() { fieldExclusionPredicates = new HashSet<>(); typeExclusionPredicates = new HashSet<>(); exclusionPolicy = new DefaultExclusionPolicy(); - objectFactory = new ObjenesisObjectFactory(); + typeResolverProxy = new TypeResolverProxy(); + objectFactory = new ObjenesisObjectFactory(typeResolverProxy); } public Range getCollectionSizeRange() { @@ -274,6 +277,13 @@ Set getUserRegistries() { return userRegistries; } + TypeResolver getTypeResolver() { + return typeResolverProxy.getTypeResolver(); + } + public void setTypeResolver(final TypeResolver typeResolver) { + this.typeResolverProxy.setTypeResolver(typeResolver); + } + /** * Register a custom randomizer for the given field predicate. * The predicate must at least specify the field type @@ -551,7 +561,7 @@ public EasyRandomParameters overrideDefaultInitialization(boolean overrideDefaul /** * Flag to bypass setters if any and use reflection directly instead. False by default. - * + * * @param bypassSetters true if setters should be ignored * @return the current {@link EasyRandomParameters} instance for method chaining */ @@ -560,6 +570,19 @@ public EasyRandomParameters bypassSetters(boolean bypassSetters) { return this; } + /** + * Defines how concrete types will be resolved. Setting this will also set + * {@link #scanClasspathForConcreteTypes(boolean)} to true. + * + * @param typeResolver the concrete type resolver + * @return the current {@link EasyRandomParameters} instance for method chaining + */ + public EasyRandomParameters typeResolver(final TypeResolver typeResolver) { + setTypeResolver(typeResolver); + scanClasspathForConcreteTypes(true); + return this; + } + /** * Utility class to hold a range of values. * @@ -618,6 +641,7 @@ public EasyRandomParameters copy() { copy.userRegistries = this.getUserRegistries(); copy.fieldExclusionPredicates = this.getFieldExclusionPredicates(); copy.typeExclusionPredicates = this.getTypeExclusionPredicates(); + copy.setTypeResolver(this.getTypeResolver()); return copy; } } diff --git a/easy-random-core/src/main/java/org/jeasy/random/FieldPopulator.java b/easy-random-core/src/main/java/org/jeasy/random/FieldPopulator.java index 482c75833..00241c135 100644 --- a/easy-random-core/src/main/java/org/jeasy/random/FieldPopulator.java +++ b/easy-random-core/src/main/java/org/jeasy/random/FieldPopulator.java @@ -27,6 +27,7 @@ import java.lang.reflect.TypeVariable; import java.util.List; +import org.jeasy.random.api.TypeResolver; import org.jeasy.random.api.ContextAwareRandomizer; import org.jeasy.random.api.Randomizer; import org.jeasy.random.api.RandomizerProvider; @@ -64,15 +65,19 @@ class FieldPopulator { private final RandomizerProvider randomizerProvider; + private final TypeResolver typeResolver; + FieldPopulator(final EasyRandom easyRandom, final RandomizerProvider randomizerProvider, final ArrayPopulator arrayPopulator, final CollectionPopulator collectionPopulator, - final MapPopulator mapPopulator, OptionalPopulator optionalPopulator) { + final MapPopulator mapPopulator, OptionalPopulator optionalPopulator, + final TypeResolver typeResolver) { this.easyRandom = easyRandom; this.randomizerProvider = randomizerProvider; this.arrayPopulator = arrayPopulator; this.collectionPopulator = collectionPopulator; this.mapPopulator = mapPopulator; this.optionalPopulator = optionalPopulator; + this.typeResolver = typeResolver; } void populateField(final Object target, final Field field, final RandomizationContext context) throws IllegalAccessException { @@ -143,7 +148,7 @@ private Object generateRandomValue(final Field field, final RandomizationContext return optionalPopulator.getRandomOptional(field, context); } else { if (context.getParameters().isScanClasspathForConcreteTypes() && isAbstract(fieldType) && !isEnumType(fieldType) /*enums can be abstract, but cannot inherit*/) { - List> parameterizedTypes = filterSameParameterizedTypes(getPublicConcreteSubTypesOf(fieldType), fieldGenericType); + List> parameterizedTypes = filterSameParameterizedTypes(typeResolver.getPublicConcreteSubTypesOf(fieldType), fieldGenericType); if (parameterizedTypes.isEmpty()) { throw new ObjectCreationException("Unable to find a matching concrete subtype of type: " + fieldType); } else { diff --git a/easy-random-core/src/main/java/org/jeasy/random/ObjenesisObjectFactory.java b/easy-random-core/src/main/java/org/jeasy/random/ObjenesisObjectFactory.java index 9db492fd9..ea9de1f09 100644 --- a/easy-random-core/src/main/java/org/jeasy/random/ObjenesisObjectFactory.java +++ b/easy-random-core/src/main/java/org/jeasy/random/ObjenesisObjectFactory.java @@ -25,6 +25,7 @@ import java.util.List; import java.util.Random; +import org.jeasy.random.api.TypeResolver; import org.jeasy.random.api.ObjectFactory; import org.jeasy.random.api.RandomizerContext; import org.objenesis.Objenesis; @@ -32,7 +33,6 @@ import java.lang.reflect.Constructor; -import static org.jeasy.random.util.ReflectionUtils.getPublicConcreteSubTypesOf; import static org.jeasy.random.util.ReflectionUtils.isAbstract; /** @@ -45,15 +45,25 @@ public class ObjenesisObjectFactory implements ObjectFactory { private final Objenesis objenesis = new ObjenesisStd(); + private final TypeResolver typeResolver; + private Random random; + public ObjenesisObjectFactory(final TypeResolver typeResolver) { + this.typeResolver = typeResolver; + } + + ObjenesisObjectFactory() { + this(TypeResolver.defaultConcreteTypeResolver()); + } + @Override public T createInstance(Class type, RandomizerContext context) { if (random == null) { random = new Random(context.getParameters().getSeed()); } if (context.getParameters().isScanClasspathForConcreteTypes() && isAbstract(type)) { - List> publicConcreteSubTypes = getPublicConcreteSubTypesOf(type); + List> publicConcreteSubTypes = typeResolver.getPublicConcreteSubTypesOf(type); if (publicConcreteSubTypes.isEmpty()) { throw new InstantiationError("Unable to find a matching concrete subtype of type: " + type + " in the classpath"); } else { diff --git a/easy-random-core/src/main/java/org/jeasy/random/api/TypeResolver.java b/easy-random-core/src/main/java/org/jeasy/random/api/TypeResolver.java new file mode 100644 index 000000000..274248e95 --- /dev/null +++ b/easy-random-core/src/main/java/org/jeasy/random/api/TypeResolver.java @@ -0,0 +1,42 @@ +package org.jeasy.random.api; + +import java.util.List; +import org.jeasy.random.util.ReflectionUtils; + +/** + * Interface describing the API to resolve concrete types for a given abstract type. + * + * For example, a resolver which resolves all {@link java.util.List} to {@link java.util.ArrayList}, + * and delegates other types to the default resolver: + * + *
{@code
+ * public class ListConcreteTypeResolver implements ConcreteTypeResolver {
+ *    List> getPublicConcreteSubTypesOf(final Class type) {
+ *     if (List.class.equals(type)) {
+ *       return ArrayList.class;
+ *     }
+ *
+ *     return ConcreteTypeResolver.defaultConcreteTypeResolver().getPublicConcreteSubTypesOf(type);
+ *   }
+ * }
+ * }
+ */ +@FunctionalInterface +public interface TypeResolver { + + /** + * Returns a list of concrete types for the given {@code type}. + * + * @param type the abstract type to resolve + * @param the actual type to introspect + * @return a list of all concrete subtypes to use + */ + List> getPublicConcreteSubTypesOf(final Class type); + + /** + * @return a default concrete type resolver which will scan the whole classpath for concrete types + */ + static TypeResolver defaultConcreteTypeResolver() { + return ReflectionUtils::getPublicConcreteSubTypesOf; + } +} diff --git a/easy-random-core/src/main/java/org/jeasy/random/util/TypeResolverProxy.java b/easy-random-core/src/main/java/org/jeasy/random/util/TypeResolverProxy.java new file mode 100644 index 000000000..6d1524d73 --- /dev/null +++ b/easy-random-core/src/main/java/org/jeasy/random/util/TypeResolverProxy.java @@ -0,0 +1,48 @@ +package org.jeasy.random.util; + +import java.util.List; +import java.util.Objects; +import org.jeasy.random.api.TypeResolver; + +/** + * A proxy implementation for {@link TypeResolver} which allows modifying the resolver after it, and + * any classes depending on it (e.g. {@link org.jeasy.random.ObjenesisObjectFactory}), have been + * created. It will ensure there is always a non-null, concrete type resolver. + * + * This class is intended for internal use only. All public methods + * might change between minor versions without notice. + */ +public final class TypeResolverProxy implements TypeResolver { + + private TypeResolver typeResolver; + + public TypeResolverProxy() { + this(TypeResolver.defaultConcreteTypeResolver()); + } + + public TypeResolverProxy(final TypeResolver typeResolver) { + this.typeResolver = typeResolver; + } + + @Override + public List> getPublicConcreteSubTypesOf(final Class type) { + return typeResolver.getPublicConcreteSubTypesOf(type); + } + + /** + * Sets the type resolver to the given instance. + * + * @param typeResolver the new type resolver + * @throws NullPointerException if the given resolver is null + */ + public void setTypeResolver(final TypeResolver typeResolver) { + this.typeResolver = Objects.requireNonNull(typeResolver, "Type resolver must not be null"); + } + + /** + * @return the current type resolver; guaranteed to be non-null + */ + public TypeResolver getTypeResolver() { + return typeResolver; + } +} diff --git a/easy-random-core/src/test/java/org/jeasy/random/FieldPopulatorTest.java b/easy-random-core/src/test/java/org/jeasy/random/FieldPopulatorTest.java index de3a6dd61..5c06e33b1 100644 --- a/easy-random-core/src/test/java/org/jeasy/random/FieldPopulatorTest.java +++ b/easy-random-core/src/test/java/org/jeasy/random/FieldPopulatorTest.java @@ -33,6 +33,7 @@ import java.util.HashMap; import java.util.Map; +import org.jeasy.random.api.ConcreteTypeResolver; import org.jeasy.random.api.ContextAwareRandomizer; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -76,7 +77,8 @@ class FieldPopulatorTest { @BeforeEach void setUp() { - fieldPopulator = new FieldPopulator(easyRandom, randomizerProvider, arrayPopulator, collectionPopulator, mapPopulator, optionalPopulator); + fieldPopulator = new FieldPopulator(easyRandom, randomizerProvider, arrayPopulator, collectionPopulator, mapPopulator, optionalPopulator, + ConcreteTypeResolver.defaultConcreteTypeResolver()); } @Test From 55b8effbf681bf827a3f4632bf1a3b475963e9af Mon Sep 17 00:00:00 2001 From: Nicolas Pepin-Perreault Date: Wed, 2 Mar 2022 19:16:33 +0100 Subject: [PATCH 2/2] test: add tests for the new type resolver --- .../java/org/jeasy/random/EasyRandomTest.java | 23 +++++++++++++++++++ .../org/jeasy/random/FieldPopulatorTest.java | 4 ++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/easy-random-core/src/test/java/org/jeasy/random/EasyRandomTest.java b/easy-random-core/src/test/java/org/jeasy/random/EasyRandomTest.java index 7011e0f5a..576354e23 100644 --- a/easy-random-core/src/test/java/org/jeasy/random/EasyRandomTest.java +++ b/easy-random-core/src/test/java/org/jeasy/random/EasyRandomTest.java @@ -23,6 +23,7 @@ */ package org.jeasy.random; +import org.jeasy.random.api.TypeResolver; import org.jeasy.random.api.Randomizer; import org.jeasy.random.beans.*; import org.jeasy.random.util.ReflectionUtils; @@ -43,6 +44,7 @@ import static org.assertj.core.api.Assertions.*; import static org.assertj.core.api.BDDAssertions.then; import static org.jeasy.random.FieldPredicates.*; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) @@ -507,4 +509,25 @@ void tryToRandomizeAllPublicConcreteTypesInTheClasspath(){ System.out.println("Failure: " + failure); } + @Test + void shouldUseCustomConcreteTypeResolver() { + // given + final TypeResolver typeResolver = new TypeResolver() { + @Override + public List> getPublicConcreteSubTypesOf(final Class type) { + return Collections.singletonList(MyAbstractType.MyConcreteType.class); + } + }; + final EasyRandom sutRandom = new EasyRandom(new EasyRandomParameters().typeResolver(typeResolver)); + + // when + final MyAbstractType instance = sutRandom.nextObject(MyAbstractType.class); + + // then + assertThat(instance).isInstanceOf(MyAbstractType.MyConcreteType.class); + } + + private interface MyAbstractType { + final class MyConcreteType implements MyAbstractType {} + } } diff --git a/easy-random-core/src/test/java/org/jeasy/random/FieldPopulatorTest.java b/easy-random-core/src/test/java/org/jeasy/random/FieldPopulatorTest.java index 5c06e33b1..3eb64df07 100644 --- a/easy-random-core/src/test/java/org/jeasy/random/FieldPopulatorTest.java +++ b/easy-random-core/src/test/java/org/jeasy/random/FieldPopulatorTest.java @@ -33,7 +33,7 @@ import java.util.HashMap; import java.util.Map; -import org.jeasy.random.api.ConcreteTypeResolver; +import org.jeasy.random.api.TypeResolver; import org.jeasy.random.api.ContextAwareRandomizer; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -78,7 +78,7 @@ class FieldPopulatorTest { @BeforeEach void setUp() { fieldPopulator = new FieldPopulator(easyRandom, randomizerProvider, arrayPopulator, collectionPopulator, mapPopulator, optionalPopulator, - ConcreteTypeResolver.defaultConcreteTypeResolver()); + TypeResolver.defaultConcreteTypeResolver()); } @Test