Skip to content

Commit fc9186d

Browse files
committed
tests: improve TestObjects.fill(...) to support interface params that have builder implementations
- For example, v3.GetApplicationResponse.Builder had a nested param of type LifecycleData. That's a method-less interface, with multiple possible implementations. Each implementation has a builder. We now pick whatever implementation we find, fill its builder, and use that in the root object.
1 parent 6f43cf0 commit fc9186d

File tree

3 files changed

+181
-89
lines changed

3 files changed

+181
-89
lines changed
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package org.cloudfoundry.operations;
2+
3+
import java.io.File;
4+
import java.lang.reflect.Modifier;
5+
import java.net.JarURLConnection;
6+
import java.net.URL;
7+
import java.util.ArrayList;
8+
import java.util.Arrays;
9+
import java.util.Collections;
10+
import java.util.Enumeration;
11+
import java.util.List;
12+
import java.util.Objects;
13+
import java.util.jar.JarFile;
14+
import java.util.stream.Collectors;
15+
import java.util.stream.Stream;
16+
17+
class ReflectionUtils {
18+
19+
private ReflectionUtils() { // do not instantiate this class
20+
}
21+
22+
/**
23+
* Find implementations for a given interface type. Uses reflection.
24+
*/
25+
public static <T> List<Class<? extends T>> findImplementations(Class<T> interfaceType) {
26+
try {
27+
ClassLoader classLoader = interfaceType.getClassLoader();
28+
29+
String path = interfaceType.getPackage().getName().replace('.', '/');
30+
Enumeration<URL> resources = classLoader.getResources(path);
31+
ArrayList<URL> lr = Collections.list(resources);
32+
33+
return lr.stream()
34+
.flatMap(
35+
url -> {
36+
if (url.getProtocol().equals("jar")) {
37+
// Handle JAR URLs
38+
return scanJar(
39+
url,
40+
interfaceType.getPackage().getName(),
41+
interfaceType);
42+
} else {
43+
return scanDirectory(
44+
new File(url.getFile()),
45+
interfaceType.getPackage().getName(),
46+
interfaceType);
47+
}
48+
})
49+
.collect(Collectors.toList());
50+
} catch (Exception ignored) {
51+
52+
}
53+
return Collections.emptyList();
54+
}
55+
56+
/**
57+
* Find implementations for the given interface type in a source directory.
58+
*/
59+
private static <T> Stream<Class<? extends T>> scanDirectory(
60+
File directory, String packageName, Class<T> interfaceType) {
61+
File[] files = directory.listFiles();
62+
if (files == null) {
63+
return Stream.empty();
64+
}
65+
66+
Stream<Class<? extends T>> classes =
67+
Arrays.stream(files)
68+
.filter(fileName -> fileName.getName().endsWith(".class"))
69+
.map(
70+
fileName ->
71+
packageName
72+
+ '.'
73+
+ fileName.getName().replaceAll("\\.class$", ""))
74+
.<Class<? extends T>>map(
75+
className ->
76+
getClassIfImplementsInterface(className, interfaceType))
77+
.filter(Objects::nonNull);
78+
Stream<Class<? extends T>> directories =
79+
Arrays.stream(files)
80+
.filter(File::isDirectory)
81+
.flatMap(
82+
fileName ->
83+
scanDirectory(
84+
fileName,
85+
packageName + "." + fileName.getName(),
86+
interfaceType));
87+
return Stream.concat(classes, directories);
88+
}
89+
90+
/**
91+
* Find implementations for the given interface type in a packaged jar.
92+
* When running {@code mvn package}, class files are packaged in jar files,
93+
* and is not available directly on the filesystem.
94+
*/
95+
private static <T> Stream<Class<? extends T>> scanJar(
96+
URL jarUrl, String packageName, Class<T> interfaceType) {
97+
try {
98+
JarURLConnection jarConnection = (JarURLConnection) jarUrl.openConnection();
99+
JarFile jarFile = jarConnection.getJarFile();
100+
String packagePath = packageName.replace('.', '/');
101+
102+
return jarFile.stream()
103+
.filter(
104+
entry -> {
105+
String name = entry.getName();
106+
return name.startsWith(packagePath)
107+
&& name.endsWith(".class")
108+
&& !name.equals(packagePath + ".class");
109+
})
110+
.map(entry -> entry.getName().replace('/', '.').replaceAll("\\.class$", ""))
111+
.<Class<? extends T>>map(
112+
className -> getClassIfImplementsInterface(className, interfaceType))
113+
.filter(Objects::nonNull);
114+
} catch (Exception e) {
115+
return Stream.empty();
116+
}
117+
}
118+
119+
/**
120+
* Return the {@link Class} instance for {@code className}, if it implements {@code interfaceType}. Otherwise, return null.
121+
*/
122+
private static <T> Class<? extends T> getClassIfImplementsInterface(
123+
String className, Class<T> interfaceType) {
124+
try {
125+
Class<?> clazz = Class.forName(className);
126+
if (interfaceType.isAssignableFrom(clazz)
127+
&& !clazz.isInterface()
128+
&& !Modifier.isAbstract(clazz.getModifiers())) {
129+
Class<? extends T> subclass = clazz.asSubclass(interfaceType);
130+
return subclass;
131+
}
132+
} catch (ClassNotFoundException ignored) {
133+
}
134+
return null;
135+
}
136+
}

cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/TestObjects.java

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,12 @@ private static <T> T fill(T builder, Optional<String> modifier) {
109109
return getConfigurationMethods(builderType, builderMethods, builtGetters).stream()
110110
.collect(
111111
() -> builder,
112-
(b, method) ->
113-
ReflectionUtils.invokeMethod(
114-
method, b, getConfiguredValue(method, modifier)),
112+
(b, method) -> {
113+
Object configuredValue = getConfiguredValue(method, modifier);
114+
if (configuredValue != null) {
115+
ReflectionUtils.invokeMethod(method, b, configuredValue);
116+
}
117+
},
115118
(a, b) -> {});
116119
}
117120

@@ -190,10 +193,19 @@ private static Object getConfiguredValue(
190193
return getConfiguredString(configurationMethod, modifier);
191194
} else if (parameterType.isArray()) {
192195
return Array.newInstance(parameterType.getComponentType(), 0);
196+
} else if (parameterType == Map.Entry.class) {
197+
return null;
193198
} else {
194-
throw new IllegalStateException(
195-
String.format("Unable to configure %s", configurationMethod));
199+
for (Class<?> implementation :
200+
org.cloudfoundry.operations.ReflectionUtils.findImplementations(
201+
parameterType)) {
202+
if (isBuiltType(implementation)) {
203+
return getConfiguredBuilder(implementation, modifier);
204+
}
205+
}
196206
}
207+
throw new IllegalStateException(
208+
String.format("Unable to configure %s", configurationMethod));
197209
}
198210

199211
private static List<Method> getMethods(Class<?> builderType) {

0 commit comments

Comments
 (0)