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

#471 Allow flexible concrete type resolution #474

Open
wants to merge 2 commits into
base: master
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 @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -107,6 +108,7 @@ public class EasyRandomParameters {
private RandomizerProvider randomizerProvider;

// internal params
private final TypeResolverProxy typeResolverProxy;
private CustomRandomizerRegistry customRandomizerRegistry;
private ExclusionRandomizerRegistry exclusionRandomizerRegistry;
private Set<RandomizerRegistry> userRegistries;
Expand Down Expand Up @@ -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<Integer> getCollectionSizeRange() {
Expand Down Expand Up @@ -274,6 +277,13 @@ Set<RandomizerRegistry> 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.
* <strong>The predicate must at least specify the field type</strong>
Expand Down Expand Up @@ -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
*/
Expand All @@ -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.
*
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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<Class<?>> parameterizedTypes = filterSameParameterizedTypes(getPublicConcreteSubTypesOf(fieldType), fieldGenericType);
List<Class<?>> parameterizedTypes = filterSameParameterizedTypes(typeResolver.getPublicConcreteSubTypesOf(fieldType), fieldGenericType);
if (parameterizedTypes.isEmpty()) {
throw new ObjectCreationException("Unable to find a matching concrete subtype of type: " + fieldType);
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@

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;
import org.objenesis.ObjenesisStd;

import java.lang.reflect.Constructor;

import static org.jeasy.random.util.ReflectionUtils.getPublicConcreteSubTypesOf;
import static org.jeasy.random.util.ReflectionUtils.isAbstract;

/**
Expand All @@ -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> T createInstance(Class<T> type, RandomizerContext context) {
if (random == null) {
random = new Random(context.getParameters().getSeed());
}
if (context.getParameters().isScanClasspathForConcreteTypes() && isAbstract(type)) {
List<Class<?>> publicConcreteSubTypes = getPublicConcreteSubTypesOf(type);
List<Class<?>> publicConcreteSubTypes = typeResolver.getPublicConcreteSubTypesOf(type);
if (publicConcreteSubTypes.isEmpty()) {
throw new InstantiationError("Unable to find a matching concrete subtype of type: " + type + " in the classpath");
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -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:
*
* <pre>{@code
* public class ListConcreteTypeResolver implements ConcreteTypeResolver {
* <T> List<Class<?>> getPublicConcreteSubTypesOf(final Class<T> type) {
* if (List.class.equals(type)) {
* return ArrayList.class;
* }
*
* return ConcreteTypeResolver.defaultConcreteTypeResolver().getPublicConcreteSubTypesOf(type);
* }
* }
* }</pre>
*/
@FunctionalInterface
public interface TypeResolver {

/**
* Returns a list of concrete types for the given {@code type}.
*
* @param type the abstract type to resolve
* @param <T> the actual type to introspect
* @return a list of all concrete subtypes to use
*/
<T> List<Class<?>> getPublicConcreteSubTypesOf(final Class<T> type);

/**
* @return a default concrete type resolver which will scan the whole classpath for concrete types
*/
static TypeResolver defaultConcreteTypeResolver() {
return ReflectionUtils::getPublicConcreteSubTypesOf;
}
}
Original file line number Diff line number Diff line change
@@ -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.
*
* <strong>This class is intended for internal use only. All public methods
* might change between minor versions without notice.</strong>
*/
public final class TypeResolverProxy implements TypeResolver {

private TypeResolver typeResolver;

public TypeResolverProxy() {
this(TypeResolver.defaultConcreteTypeResolver());
}

public TypeResolverProxy(final TypeResolver typeResolver) {
this.typeResolver = typeResolver;
}

@Override
public <T> List<Class<?>> getPublicConcreteSubTypesOf(final Class<T> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)
Expand Down Expand Up @@ -507,4 +509,25 @@ void tryToRandomizeAllPublicConcreteTypesInTheClasspath(){
System.out.println("Failure: " + failure);
}

@Test
void shouldUseCustomConcreteTypeResolver() {
// given
final TypeResolver typeResolver = new TypeResolver() {
@Override
public <T> List<Class<?>> getPublicConcreteSubTypesOf(final Class<T> 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 {}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import java.util.HashMap;
import java.util.Map;

import org.jeasy.random.api.TypeResolver;
import org.jeasy.random.api.ContextAwareRandomizer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -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,
TypeResolver.defaultConcreteTypeResolver());
}

@Test
Expand Down