Skip to content

Commit 70f7f2b

Browse files
committed
refactor: decouple ClassGraph from SpecimenFactory
1 parent 45614a3 commit 70f7f2b

File tree

3 files changed

+208
-71
lines changed

3 files changed

+208
-71
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package com.github.nylle.javafixture;
2+
3+
import io.github.classgraph.ClassGraph;
4+
import io.github.classgraph.ClassInfo;
5+
import io.github.classgraph.ScanResult;
6+
7+
import java.lang.reflect.Type;
8+
import java.util.List;
9+
import java.util.Optional;
10+
import java.util.Random;
11+
import java.util.stream.Collectors;
12+
13+
public class ClassPathScanner {
14+
15+
public <T> Optional<SpecimenType<T>> findRandomClassFor(SpecimenType<T> type) {
16+
try (ScanResult scanResult = new ClassGraph().enableAllInfo().scan()) {
17+
18+
var result = filter(scanResult, type);
19+
20+
if (result.isEmpty()) {
21+
return Optional.empty();
22+
}
23+
24+
var implementingClass = result.get(new Random().nextInt(result.size()));
25+
26+
if (isNotParametrized(implementingClass)) {
27+
return Optional.of(SpecimenType.fromClass(implementingClass.loadClass()));
28+
}
29+
30+
return Optional.of(SpecimenType.fromRawType(implementingClass.loadClass(), resolveTypeArguments(type, implementingClass)));
31+
32+
} catch (Exception ex) {
33+
return Optional.empty();
34+
}
35+
}
36+
37+
private <T> List<ClassInfo> filter(ScanResult scanResult, SpecimenType<T> type) {
38+
if(type.isInterface()) {
39+
return scanResult.getClassesImplementing(type.asClass()).stream()
40+
.filter(x -> isNotParametrized(x) || type.isParameterized())
41+
.filter(x -> isNotParametrized(x) || typeParametersMatch(x, type))
42+
.collect(Collectors.toList());
43+
}
44+
45+
if(type.isAbstract()) {
46+
return scanResult.getSubclasses(type.asClass()).stream()
47+
.filter(x -> !x.isAbstract())
48+
.filter(x -> isNotParametrized(x) || type.isParameterized())
49+
.filter(x -> isNotParametrized(x) || typeParametersMatch(x, type))
50+
.collect(Collectors.toList());
51+
}
52+
53+
return List.of();
54+
}
55+
56+
private static boolean isNotParametrized(ClassInfo classInfo) {
57+
return classInfo.getTypeSignature() == null || classInfo.getTypeSignature().getTypeParameters().isEmpty();
58+
}
59+
60+
private static <T> boolean typeParametersMatch(ClassInfo implementingClass, SpecimenType<T> genericType) {
61+
return resolveTypeArguments(genericType, implementingClass).length >= implementingClass.getTypeSignature().getTypeParameters().size();
62+
}
63+
64+
private static <T> Type[] resolveTypeArguments(SpecimenType<T> genericType, ClassInfo implementingClass) {
65+
var typeParameters = genericType.getTypeParameterNamesAndTypes(x -> x);
66+
return implementingClass.getTypeSignature().getTypeParameters().stream()
67+
.map(x -> typeParameters.getOrDefault(x.getName(), null))
68+
.filter(x -> x != null)
69+
.map(x -> x.asClass())
70+
.toArray(size -> new Type[size]);
71+
}
72+
}

src/main/java/com/github/nylle/javafixture/SpecimenFactory.java

Lines changed: 10 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,6 @@
1313
import com.github.nylle.javafixture.specimen.SpecialSpecimen;
1414
import com.github.nylle.javafixture.specimen.TimeSpecimen;
1515

16-
import io.github.classgraph.ClassGraph;
17-
import io.github.classgraph.ClassInfo;
18-
import io.github.classgraph.ScanResult;
19-
20-
import java.lang.reflect.Type;
21-
import java.util.Random;
22-
import java.util.stream.Collectors;
23-
2416
public class SpecimenFactory {
2517

2618
private final Context context;
@@ -102,72 +94,19 @@ public <T> ISpecimen<T> build(final SpecimenType<T> type) {
10294
}
10395

10496
private <T> ISpecimen<T> implementationOrProxy(final SpecimenType<T> interfaceType) {
105-
try (ScanResult scanResult = new ClassGraph().enableAllInfo().scan()) {
106-
var implementingClasses = scanResult.getClassesImplementing(interfaceType.asClass()).stream()
107-
.filter(x -> isNotParametrized(x) || interfaceType.isParameterized())
108-
.filter(x -> isNotParametrized(x) || typeParametersMatch(x, interfaceType))
109-
.collect(Collectors.toList());
110-
111-
if (implementingClasses.isEmpty()) {
112-
return new InterfaceSpecimen<>(interfaceType, context, this);
113-
}
114-
115-
var implementingClass = implementingClasses.get(new Random().nextInt(implementingClasses.size()));
116-
if (isNotParametrized(implementingClass)) {
117-
return new ObjectSpecimen<>(SpecimenType.fromClass(implementingClass.loadClass()), context, this);
118-
}
119-
120-
return new GenericSpecimen<>(
121-
SpecimenType.fromRawType(implementingClass.loadClass(), resolveTypeArguments(interfaceType, implementingClass)),
122-
context,
123-
this);
124-
} catch (Exception ex) {
125-
return new InterfaceSpecimen<>(interfaceType, context, this);
126-
}
97+
return new ClassPathScanner().findRandomClassFor(interfaceType)
98+
.map(x -> x.isParameterized()
99+
? new GenericSpecimen<>(x, context, this)
100+
: new ObjectSpecimen<>(x, context, this))
101+
.orElseGet(() -> new InterfaceSpecimen<>(interfaceType, context, this));
127102
}
128103

129104
private <T> ISpecimen<T> subClassOrProxy(final SpecimenType<T> abstractType) {
130-
try (ScanResult scanResult = new ClassGraph().enableAllInfo().scan()) {
131-
var subClasses = scanResult.getSubclasses(abstractType.asClass()).stream()
132-
.filter(x -> !x.isAbstract())
133-
.filter(x -> isNotParametrized(x) || abstractType.isParameterized())
134-
.filter(x -> isNotParametrized(x) || typeParametersMatch(x, abstractType))
135-
.collect(Collectors.toList());
136-
137-
if (subClasses.isEmpty()) {
138-
return new AbstractSpecimen<>(abstractType, context, this);
139-
}
140-
141-
var implementingClass = subClasses.get(new Random().nextInt(subClasses.size()));
142-
if (isNotParametrized(implementingClass)) {
143-
return new ObjectSpecimen<>(SpecimenType.fromClass(implementingClass.loadClass()), context, this);
144-
}
145-
146-
return new GenericSpecimen<>(
147-
SpecimenType.fromRawType(implementingClass.loadClass(), resolveTypeArguments(abstractType, implementingClass)),
148-
context,
149-
this);
150-
} catch (Exception ex) {
151-
return new AbstractSpecimen<>(abstractType, context, this);
152-
}
153-
}
154-
155-
private static boolean isNotParametrized(ClassInfo classInfo) {
156-
return classInfo.getTypeSignature() == null || classInfo.getTypeSignature().getTypeParameters().isEmpty();
157-
}
158-
159-
private static <T> boolean typeParametersMatch(ClassInfo implementingClass, SpecimenType<T> genericType) {
160-
return resolveTypeArguments(genericType, implementingClass).length >= implementingClass.getTypeSignature().getTypeParameters().size();
161-
}
162-
163-
private static <T> Type[] resolveTypeArguments(SpecimenType<T> genericType, ClassInfo implementingClass) {
164-
var typeParameters = genericType.getTypeParameterNamesAndTypes(x -> x);
165-
166-
return implementingClass.getTypeSignature().getTypeParameters().stream()
167-
.map(x -> typeParameters.getOrDefault(x.getName(), null))
168-
.filter(x -> x != null)
169-
.map(x -> x.asClass())
170-
.toArray(size -> new Type[size]);
105+
return new ClassPathScanner().findRandomClassFor(abstractType)
106+
.map(x -> x.isParameterized()
107+
? new GenericSpecimen<>(x, context, this)
108+
: new ObjectSpecimen<>(x, context, this))
109+
.orElseGet(() -> new AbstractSpecimen<>(abstractType, context, this));
171110
}
172111
}
173112

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package com.github.nylle.javafixture;
2+
3+
import com.github.nylle.javafixture.testobjects.abstractclasses.AbstractClassWithImplementation;
4+
import com.github.nylle.javafixture.testobjects.abstractclasses.AbstractClassWithImplementationImpl;
5+
import com.github.nylle.javafixture.testobjects.abstractclasses.AbstractClassWithoutImplementation;
6+
import com.github.nylle.javafixture.testobjects.abstractclasses.GenericAbstractClassTUWithGenericImplementationU;
7+
import com.github.nylle.javafixture.testobjects.abstractclasses.GenericAbstractClassTUWithGenericImplementationUImpl;
8+
import com.github.nylle.javafixture.testobjects.interfaces.GenericInterfaceTUWithGenericImplementationU;
9+
import com.github.nylle.javafixture.testobjects.interfaces.GenericInterfaceTUWithGenericImplementationUImpl;
10+
import com.github.nylle.javafixture.testobjects.interfaces.InterfaceWithImplementation;
11+
import com.github.nylle.javafixture.testobjects.interfaces.InterfaceWithImplementationImpl;
12+
import com.github.nylle.javafixture.testobjects.interfaces.InterfaceWithoutImplementation;
13+
import org.junit.jupiter.api.DisplayName;
14+
import org.junit.jupiter.api.Nested;
15+
import org.junit.jupiter.api.Test;
16+
import org.mockito.Mockito;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
import static org.mockito.Mockito.doThrow;
20+
21+
class ClassPathScannerTest {
22+
23+
private ClassPathScanner sut = new ClassPathScanner();
24+
25+
@Nested
26+
@DisplayName("trying to resolve a random implementation of an interface")
27+
class FindRandomClassForInterface {
28+
29+
@Test
30+
@DisplayName("returns an empty Optional if none was found")
31+
void returnsAnEmptyOptionalIfNoneWasFound() {
32+
var actual = sut.findRandomClassFor(SpecimenType.fromClass(InterfaceWithoutImplementation.class));
33+
34+
assertThat(actual).isEmpty();
35+
}
36+
37+
@Test
38+
@DisplayName("returns an empty Optional if not an interface nor abstract")
39+
void returnsAnEmptyOptionalIfNotAnInterfaceNorAbstract() {
40+
var actual = sut.findRandomClassFor(SpecimenType.fromClass(String.class));
41+
42+
assertThat(actual).isEmpty();
43+
}
44+
45+
@Test
46+
@DisplayName("returns an empty Optional if exception was thrown")
47+
void returnsAnEmptyOptionalIfExceptionWasThrown() {
48+
var throwingType = Mockito.mock(SpecimenType.class);
49+
doThrow(new IllegalArgumentException("expected for test")).when(throwingType).isInterface();
50+
51+
var actual = sut.findRandomClassFor(throwingType);
52+
53+
assertThat(actual).isEmpty();
54+
}
55+
56+
@Test
57+
@DisplayName("returns a SpecimenType representing an implementing class")
58+
void returnsASpecimenTypeRepresentingAnImplementingClass() {
59+
var actual = sut.findRandomClassFor(SpecimenType.fromClass(InterfaceWithImplementation.class));
60+
61+
assertThat(actual).isNotEmpty();
62+
assertThat(actual.get().asClass()).isEqualTo(InterfaceWithImplementationImpl.class);
63+
}
64+
65+
@Test
66+
@DisplayName("returns a SpecimenType representing an implementing generic class")
67+
void returnsASpecimenTypeRepresentingAnImplementingGenericClass() {
68+
var actual = sut.findRandomClassFor(new SpecimenType<GenericInterfaceTUWithGenericImplementationU<String, Integer>>() {});
69+
70+
assertThat(actual).isNotEmpty();
71+
assertThat(actual.get().asClass()).isEqualTo(GenericInterfaceTUWithGenericImplementationUImpl.class);
72+
assertThat(actual.get().getGenericTypeArgument(0).asClass()).isEqualTo(Integer.class);
73+
}
74+
}
75+
76+
@Nested
77+
@DisplayName("trying to resolve a random implementation of an abstract class")
78+
class FindRandomClassForAbstractClass {
79+
80+
@Test
81+
@DisplayName("returns an empty Optional if none was found")
82+
void returnsAnEmptyOptionalIfNoneWasFound() {
83+
var actual = sut.findRandomClassFor(SpecimenType.fromClass(AbstractClassWithoutImplementation.class));
84+
85+
assertThat(actual).isEmpty();
86+
}
87+
88+
@Test
89+
@DisplayName("returns an empty Optional if not an interface nor abstract")
90+
void returnsAnEmptyOptionalIfNotAnInterfaceNorAbstract() {
91+
var actual = sut.findRandomClassFor(SpecimenType.fromClass(String.class));
92+
93+
assertThat(actual).isEmpty();
94+
}
95+
96+
@Test
97+
@DisplayName("returns an empty Optional if exception was thrown")
98+
void returnsAnEmptyOptionalIfExceptionWasThrown() {
99+
var throwingType = Mockito.mock(SpecimenType.class);
100+
doThrow(new IllegalArgumentException("expected for test")).when(throwingType).isInterface();
101+
102+
var actual = sut.findRandomClassFor(throwingType);
103+
104+
assertThat(actual).isEmpty();
105+
}
106+
107+
@Test
108+
@DisplayName("returns a SpecimenType representing an extending class")
109+
void returnsASpecimenTypeRepresentingAnImplementingClass() {
110+
var actual = sut.findRandomClassFor(SpecimenType.fromClass(AbstractClassWithImplementation.class));
111+
112+
assertThat(actual).isNotEmpty();
113+
assertThat(actual.get().asClass()).isEqualTo(AbstractClassWithImplementationImpl.class);
114+
}
115+
116+
@Test
117+
@DisplayName("returns a SpecimenType representing an implementing generic class")
118+
void returnsASpecimenTypeRepresentingAnImplementingGenericClass() {
119+
var actual = sut.findRandomClassFor(new SpecimenType<GenericAbstractClassTUWithGenericImplementationU<String, Integer>>() {});
120+
121+
assertThat(actual).isNotEmpty();
122+
assertThat(actual.get().asClass()).isEqualTo(GenericAbstractClassTUWithGenericImplementationUImpl.class);
123+
assertThat(actual.get().getGenericTypeArgument(0).asClass()).isEqualTo(Integer.class);
124+
}
125+
}
126+
}

0 commit comments

Comments
 (0)