Skip to content

Commit c3de7f6

Browse files
authored
Bug fixes (#30)
Fixed #28 and improved depth check and code refactoring.
1 parent 16d68ae commit c3de7f6

File tree

6 files changed

+337
-45
lines changed

6 files changed

+337
-45
lines changed

easy-random-core/src/main/java/org/jeasy/random/EasyRandom.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ public EasyRandom(final EasyRandomParameters easyRandomParameters) {
9494
);
9595
exclusionPolicy = easyRandomParameters.getExclusionPolicy();
9696
parameters = easyRandomParameters;
97-
recordFactory = new RecordFactory();
97+
recordFactory = new RecordFactory(this);
9898
}
9999

100100
/**
@@ -246,4 +246,12 @@ private Collection<RandomizerRegistry> loadRegistries() {
246246
ServiceLoader.load(RandomizerRegistry.class).forEach(registries::add);
247247
return registries;
248248
}
249+
250+
public RandomizerProvider getRandomizerProvider() {
251+
return randomizerProvider;
252+
}
253+
254+
public ObjectFactory getObjectFactory() {
255+
return objectFactory;
256+
}
249257
}

easy-random-core/src/main/java/org/jeasy/random/FieldPopulator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ private Class<?> getParametrizedType(Field field, Class<?> fieldTargetType) {
198198
}
199199
}
200200
if (actualTypeArgument == null) {
201-
return field.getClass();
201+
return field.getType();
202202
}
203203
Class<?> aClass;
204204
String typeName = null;

easy-random-core/src/main/java/org/jeasy/random/RecordFactory.java

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,9 @@
2323
*/
2424
package org.jeasy.random;
2525

26-
import static org.jeasy.random.util.ReflectionUtils.*;
27-
2826
import java.lang.reflect.Constructor;
27+
import java.lang.reflect.Field;
2928
import java.lang.reflect.RecordComponent;
30-
import java.lang.reflect.Type;
3129
import org.jeasy.random.api.RandomizerContext;
3230

3331
/**
@@ -39,22 +37,36 @@
3937
*/
4038
public class RecordFactory extends ObjenesisObjectFactory {
4139

42-
private EasyRandom easyRandom;
40+
private final EasyRandom easyRandom;
41+
private final RecordFieldPopulator recordFieldPopulator;
42+
43+
public RecordFactory(EasyRandom easyRandom) {
44+
this.easyRandom = easyRandom;
45+
recordFieldPopulator =
46+
new RecordFieldPopulator(
47+
easyRandom,
48+
easyRandom.getRandomizerProvider(),
49+
new ArrayPopulator(easyRandom),
50+
new CollectionPopulator(easyRandom),
51+
new MapPopulator(easyRandom, easyRandom.getObjectFactory()),
52+
new OptionalPopulator(easyRandom)
53+
);
54+
}
4355

4456
@Override
4557
public <T> T createInstance(Class<T> type, RandomizerContext context) {
46-
if (easyRandom == null) {
47-
easyRandom = new EasyRandom(context.getParameters());
48-
}
49-
5058
return createRandomRecord(type, (RandomizationContext) context);
5159
}
5260

61+
private <T> boolean isRecord(final Class<T> type) {
62+
return type.isRecord();
63+
}
64+
5365
private <T> T createRandomRecord(Class<T> recordType, RandomizationContext context) {
5466
// generate random values for record components
67+
Field[] fields = recordType.getDeclaredFields();
5568
RecordComponent[] recordComponents = recordType.getRecordComponents();
5669
Object[] randomValues = new Object[recordComponents.length];
57-
5870
if (context.hasExceededRandomizationDepth()) {
5971
for (int i = 0; i < recordComponents.length; i++) {
6072
Class<?> type = recordComponents[i].getType();
@@ -64,25 +76,21 @@ private <T> T createRandomRecord(Class<T> recordType, RandomizationContext conte
6476
for (int i = 0; i < recordComponents.length; i++) {
6577
context.pushStackItem(new RandomizationContextStackItem(recordType, null));
6678
Class<?> type = recordComponents[i].getType();
67-
Type genericType = recordComponents[i].getGenericType();
68-
if (isArrayType(type)) {
69-
randomValues[i] = new ArrayPopulator(easyRandom).getRandomArray(type, context);
70-
} else if (isMapType(type)) {
71-
randomValues[i] =
72-
new MapPopulator(easyRandom, context.getParameters().getObjectFactory())
73-
.getRandomMap(genericType, type, context);
74-
} else if (isOptionalType(type)) {
75-
randomValues[i] = new OptionalPopulator(easyRandom).getRandomOptional(genericType, context);
76-
} else if (isCollectionType(type)) {
77-
randomValues[i] =
78-
new CollectionPopulator(easyRandom).getRandomCollection(genericType, type, context);
79-
} else {
80-
randomValues[i] = easyRandom.doPopulateBean(type, context);
79+
try {
80+
if (isRecord(type)) {
81+
randomValues[i] = easyRandom.doPopulateBean(type, context);
82+
} else {
83+
randomValues[i] = this.recordFieldPopulator.populateField(fields[i], recordType, context);
84+
}
85+
} catch (IllegalAccessException e) {
86+
throw new ObjectCreationException(
87+
"Unable to create a random instance of recordType " + recordType,
88+
e
89+
);
8190
}
8291
context.popStackItem();
8392
}
8493
}
85-
8694
// create a random instance with random values
8795
try {
8896
Constructor<T> canonicalConstructor = getCanonicalConstructor(recordType);
@@ -107,7 +115,7 @@ private <T> Constructor<T> getCanonicalConstructor(Class<T> recordType) {
107115
// should not happen, from Record javadoc:
108116
// "A record class has the following mandated members: a public canonical constructor ,
109117
// whose descriptor is the same as the record descriptor;"
110-
throw new RuntimeException("Invalid record definition", e);
118+
throw new ObjectCreationException("Invalid record definition", e);
111119
}
112120
}
113121
}
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
/*
2+
* The MIT License
3+
*
4+
* Copyright (c) 2020, Mahmoud Ben Hassine (mahmoud.benhassine@icloud.com)
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in
14+
* all copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
* THE SOFTWARE.
23+
*/
24+
package org.jeasy.random;
25+
26+
import static org.jeasy.random.util.ReflectionUtils.*;
27+
28+
import java.lang.reflect.Field;
29+
import java.lang.reflect.ParameterizedType;
30+
import java.lang.reflect.Type;
31+
import java.lang.reflect.TypeVariable;
32+
import java.util.List;
33+
import org.jeasy.random.api.ContextAwareRandomizer;
34+
import org.jeasy.random.api.Randomizer;
35+
import org.jeasy.random.api.RandomizerProvider;
36+
import org.jeasy.random.randomizers.misc.SkipRandomizer;
37+
38+
/**
39+
* Component that encapsulates the logic of generating a random value for a given field.
40+
* It collaborates with a:
41+
* <ul>
42+
* <li>{@link EasyRandom} whenever the field is a user defined type.</li>
43+
* <li>{@link ArrayPopulator} whenever the field is an array type.</li>
44+
* <li>{@link CollectionPopulator} whenever the field is a collection type.</li>
45+
* <li>{@link CollectionPopulator}whenever the field is a map type.</li>
46+
* </ul>
47+
*
48+
* @author Mahmoud Ben Hassine (mahmoud.benhassine@icloud.com)
49+
*/
50+
class RecordFieldPopulator {
51+
52+
private final EasyRandom easyRandom;
53+
54+
private final ArrayPopulator arrayPopulator;
55+
56+
private final CollectionPopulator collectionPopulator;
57+
58+
private final MapPopulator mapPopulator;
59+
60+
private final OptionalPopulator optionalPopulator;
61+
62+
private final RandomizerProvider randomizerProvider;
63+
64+
RecordFieldPopulator(
65+
final EasyRandom easyRandom,
66+
final RandomizerProvider randomizerProvider,
67+
final ArrayPopulator arrayPopulator,
68+
final CollectionPopulator collectionPopulator,
69+
final MapPopulator mapPopulator,
70+
OptionalPopulator optionalPopulator
71+
) {
72+
this.easyRandom = easyRandom;
73+
this.randomizerProvider = randomizerProvider;
74+
this.arrayPopulator = arrayPopulator;
75+
this.collectionPopulator = collectionPopulator;
76+
this.mapPopulator = mapPopulator;
77+
this.optionalPopulator = optionalPopulator;
78+
}
79+
80+
Object populateField(final Field field, Class<?> enclosingType, final RandomizationContext context)
81+
throws IllegalAccessException {
82+
Randomizer<?> randomizer = getRandomizer(field, context, enclosingType);
83+
if (randomizer instanceof SkipRandomizer) {
84+
return null;
85+
}
86+
//context.pushStackItem(new RandomizationContextStackItem(null, field));
87+
if (randomizer instanceof ContextAwareRandomizer) {
88+
((ContextAwareRandomizer<?>) randomizer).setRandomizerContext(context);
89+
}
90+
if (!context.hasExceededRandomizationDepth()) {
91+
Object value;
92+
if (randomizer != null) {
93+
value = randomizer.getRandomValue();
94+
} else {
95+
try {
96+
value = generateRandomValue(field, context);
97+
} catch (ObjectCreationException e) {
98+
String exceptionMessage = String.format(
99+
"Unable to create type: %s for field: %s of class: %s",
100+
field.getType().getName(),
101+
field.getName(),
102+
enclosingType.getName()
103+
);
104+
// FIXME catch ObjectCreationException and throw ObjectCreationException ?
105+
throw new ObjectCreationException(exceptionMessage, e);
106+
}
107+
}
108+
return value;
109+
}
110+
//context.popStackItem();
111+
return null;
112+
}
113+
114+
private Randomizer<?> getRandomizer(Field field, RandomizationContext context, Class<?> fieldTargetType) {
115+
// issue 241: if there is no custom randomizer by field, then check by type
116+
Randomizer<?> randomizer = randomizerProvider.getRandomizerByField(field, context);
117+
if (randomizer == null) {
118+
Type genericType = field.getGenericType();
119+
if (isTypeVariable(genericType)) {
120+
// if generic type, retrieve actual type from declaring class
121+
Class<?> type = getParametrizedType(field, fieldTargetType);
122+
randomizer = randomizerProvider.getRandomizerByType(type, context);
123+
} else {
124+
randomizer = randomizerProvider.getRandomizerByType(field.getType(), context);
125+
}
126+
}
127+
return randomizer;
128+
}
129+
130+
private Object generateRandomValue(final Field field, final RandomizationContext context) {
131+
Class<?> fieldType = field.getType();
132+
Type fieldGenericType = field.getGenericType();
133+
134+
if (isArrayType(fieldType)) {
135+
return arrayPopulator.getRandomArray(fieldType, context);
136+
} else if (isCollectionType(fieldType)) {
137+
return collectionPopulator.getRandomCollection(field, context);
138+
} else if (isMapType(fieldType)) {
139+
return mapPopulator.getRandomMap(field, context);
140+
} else if (isOptionalType(fieldType)) {
141+
return optionalPopulator.getRandomOptional(field, context);
142+
} else {
143+
if (
144+
context.getParameters().isScanClasspathForConcreteTypes() &&
145+
isAbstract(fieldType) &&
146+
!isEnumType(fieldType)
147+
/*enums can be abstract, but cannot inherit*/
148+
) {
149+
List<Class<?>> parameterizedTypes = filterSameParameterizedTypes(
150+
getPublicConcreteSubTypesOf(fieldType),
151+
fieldGenericType
152+
);
153+
if (parameterizedTypes.isEmpty()) {
154+
throw new ObjectCreationException(
155+
"Unable to find a matching concrete subtype of type: " + fieldType
156+
);
157+
} else {
158+
Class<?> randomConcreteSubType = parameterizedTypes.get(
159+
easyRandom.nextInt(parameterizedTypes.size())
160+
);
161+
return easyRandom.doPopulateBean(randomConcreteSubType, context);
162+
}
163+
} else {
164+
Type genericType = field.getGenericType();
165+
if (isTypeVariable(genericType)) {
166+
// if generic type, try to retrieve actual type from hierarchy
167+
Class<?> type = getParametrizedType(field, context.getTargetType());
168+
return easyRandom.doPopulateBean(type, context);
169+
}
170+
return easyRandom.doPopulateBean(fieldType, context);
171+
}
172+
}
173+
}
174+
175+
private Class<?> getParametrizedType(Field field, Class<?> fieldTargetType) {
176+
Class<?> declaringClass = field.getDeclaringClass();
177+
TypeVariable<? extends Class<?>>[] typeParameters = declaringClass.getTypeParameters();
178+
Type genericSuperclass = getGenericSuperClass(fieldTargetType);
179+
ParameterizedType parameterizedGenericSuperType = (ParameterizedType) genericSuperclass;
180+
Type[] actualTypeArguments = parameterizedGenericSuperType.getActualTypeArguments();
181+
Type actualTypeArgument = null;
182+
for (int i = 0; i < typeParameters.length; i++) {
183+
if (field.getGenericType().equals(typeParameters[i])) {
184+
actualTypeArgument = actualTypeArguments[i];
185+
}
186+
}
187+
if (actualTypeArgument == null) {
188+
return field.getType();
189+
}
190+
Class<?> aClass;
191+
String typeName = null;
192+
try {
193+
typeName = actualTypeArgument.getTypeName();
194+
aClass = Class.forName(typeName);
195+
} catch (ClassNotFoundException e) {
196+
String message = String.format(
197+
"Unable to load class %s of generic field %s in class %s. " +
198+
"Please refer to the documentation as this generic type may not be supported for randomization.",
199+
typeName,
200+
field.getName(),
201+
field.getDeclaringClass().getName()
202+
);
203+
throw new ObjectCreationException(message, e);
204+
}
205+
return aClass;
206+
}
207+
208+
// find the generic base class in the hierarchy (which might not be the first super type)
209+
private Type getGenericSuperClass(Class<?> targetType) {
210+
Type genericSuperclass = targetType.getGenericSuperclass();
211+
while (targetType != null && !(genericSuperclass instanceof ParameterizedType)) {
212+
targetType = targetType.getSuperclass();
213+
if (targetType != null) {
214+
genericSuperclass = targetType.getGenericSuperclass();
215+
}
216+
}
217+
return genericSuperclass;
218+
}
219+
}

0 commit comments

Comments
 (0)