Skip to content

Commit f0317c9

Browse files
authored
Merge pull request #231 from psycho-ir/annotation-processor-test
Native Image Friendly
2 parents 0c5faaf + 5a0af91 commit f0317c9

File tree

21 files changed

+413
-69
lines changed

21 files changed

+413
-69
lines changed

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,7 @@ public class Runner {
7878
The Controller implements the business logic and describes all the classes needed to handle the CRD.
7979

8080
```java
81-
@Controller(customResourceClass = WebServer.class,
82-
crdName = "webservers.sample.javaoperatorsdk")
81+
@Controller(crdName = "webservers.sample.javaoperatorsdk")
8382
public class WebServerController implements ResourceController<WebServer> {
8483

8584
@Override

operator-framework/pom.xml

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
</plugins>
3232
</build>
3333

34+
3435
<dependencies>
3536
<dependency>
3637
<groupId>io.fabric8</groupId>
@@ -77,10 +78,26 @@
7778
<version>4.0.3</version>
7879
<scope>test</scope>
7980
</dependency>
81+
82+
<dependency>
83+
<groupId>com.google.testing.compile</groupId>
84+
<artifactId>compile-testing</artifactId>
85+
<version>0.19</version>
86+
<scope>test</scope>
87+
</dependency>
88+
89+
<dependency>
90+
<groupId>com.google.auto.service</groupId>
91+
<artifactId>auto-service</artifactId>
92+
<version>1.0-rc2</version>
93+
<scope>compile</scope>
94+
</dependency>
95+
8096
<dependency>
81-
<groupId>org.javassist</groupId>
82-
<artifactId>javassist</artifactId>
83-
<version>3.27.0-GA</version>
97+
<groupId>com.squareup</groupId>
98+
<artifactId>javapoet</artifactId>
99+
<version>1.13.0</version>
100+
<scope>compile</scope>
84101
</dependency>
85102
</dependencies>
86103
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package io.javaoperatorsdk.operator;
2+
3+
import io.fabric8.kubernetes.client.CustomResource;
4+
import io.javaoperatorsdk.operator.api.ResourceController;
5+
import org.apache.commons.lang3.ClassUtils;
6+
import org.slf4j.Logger;
7+
import org.slf4j.LoggerFactory;
8+
9+
import java.io.BufferedReader;
10+
import java.io.IOException;
11+
import java.io.InputStreamReader;
12+
import java.net.URL;
13+
import java.util.*;
14+
import java.util.stream.Collectors;
15+
16+
17+
class ControllerToCustomResourceMappingsProvider {
18+
private static final Logger log = LoggerFactory.getLogger(ControllerUtils.class);
19+
20+
static Map<Class<? extends ResourceController>, Class<? extends CustomResource>> provide(final String resourcePath) {
21+
Map<Class<? extends ResourceController>, Class<? extends CustomResource>> controllerToCustomResourceMappings = new HashMap();
22+
try {
23+
final Enumeration<URL> customResourcesMetadataList = ControllerUtils.class.getClassLoader().getResources(resourcePath);
24+
for (Iterator<URL> it = customResourcesMetadataList.asIterator(); it.hasNext(); ) {
25+
URL url = it.next();
26+
27+
List<String> classNamePairs = retrieveClassNamePairs(url);
28+
classNamePairs.forEach(clazzPair -> {
29+
try {
30+
final String[] classNames = clazzPair.split(",");
31+
if (classNames.length != 2) {
32+
throw new IllegalStateException(String.format("%s is not valid CustomResource metadata defined in %s", clazzPair, url.toString()));
33+
}
34+
35+
controllerToCustomResourceMappings.put(
36+
(Class<? extends ResourceController>) ClassUtils.getClass(classNames[0]),
37+
(Class<? extends CustomResource>) ClassUtils.getClass(classNames[1])
38+
);
39+
} catch (ClassNotFoundException e) {
40+
throw new RuntimeException(e);
41+
}
42+
});
43+
}
44+
log.debug("Loaded Controller to CustomResource mappings {}", controllerToCustomResourceMappings);
45+
return controllerToCustomResourceMappings;
46+
} catch (IOException e) {
47+
throw new RuntimeException(e);
48+
}
49+
}
50+
51+
private static List<String> retrieveClassNamePairs(URL url) throws IOException {
52+
return new BufferedReader(
53+
new InputStreamReader(url.openStream())
54+
).lines().collect(Collectors.toList());
55+
}
56+
}

operator-framework/src/main/java/io/javaoperatorsdk/operator/ControllerUtils.java

Lines changed: 28 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,49 @@
11
package io.javaoperatorsdk.operator;
22

3-
import io.javaoperatorsdk.operator.api.Controller;
4-
import io.javaoperatorsdk.operator.api.ResourceController;
5-
import io.fabric8.kubernetes.api.builder.Function;
63
import io.fabric8.kubernetes.client.CustomResource;
74
import io.fabric8.kubernetes.client.CustomResourceDoneable;
8-
import javassist.*;
9-
import org.slf4j.Logger;
10-
import org.slf4j.LoggerFactory;
5+
import io.javaoperatorsdk.operator.api.Controller;
6+
import io.javaoperatorsdk.operator.api.ResourceController;
117

12-
import java.util.HashMap;
138
import java.util.Map;
149

1510

1611
public class ControllerUtils {
1712

18-
private final static double JAVA_VERSION = Double.parseDouble(System.getProperty("java.specification.version"));
1913
private static final String FINALIZER_NAME_SUFFIX = "/finalizer";
14+
public static final String CONTROLLERS_RESOURCE_PATH = "javaoperatorsdk/controllers";
15+
private static Map<Class<? extends ResourceController>, Class<? extends CustomResource>> controllerToCustomResourceMappings;
2016

21-
// this is just to support testing, this way we don't try to create class multiple times in memory with same name.
22-
// note that other solution is to add a random string to doneable class name
23-
private static Map<Class<? extends CustomResource>, Class<? extends CustomResourceDoneable<? extends CustomResource>>>
24-
doneableClassCache = new HashMap<>();
17+
static {
18+
controllerToCustomResourceMappings =
19+
ControllerToCustomResourceMappingsProvider
20+
.provide(CONTROLLERS_RESOURCE_PATH);
21+
}
2522

2623
static String getFinalizer(ResourceController controller) {
2724
final String annotationFinalizerName = getAnnotation(controller).finalizerName();
2825
if (!Controller.NULL.equals(annotationFinalizerName)) {
2926
return annotationFinalizerName;
3027
}
31-
final String crdName = getAnnotation(controller).crdName() + FINALIZER_NAME_SUFFIX;
32-
return crdName;
28+
return getAnnotation(controller).crdName() + FINALIZER_NAME_SUFFIX;
3329
}
3430

35-
static boolean getGenerationEventProcessing(ResourceController controller) {
31+
static boolean getGenerationEventProcessing(ResourceController<?> controller) {
3632
return getAnnotation(controller).generationAwareEventProcessing();
3733
}
3834

3935
static <R extends CustomResource> Class<R> getCustomResourceClass(ResourceController<R> controller) {
40-
return (Class<R>) getAnnotation(controller).customResourceClass();
36+
final Class<? extends CustomResource> customResourceClass = controllerToCustomResourceMappings
37+
.get(controller.getClass());
38+
if (customResourceClass == null) {
39+
throw new IllegalArgumentException(
40+
String.format(
41+
"No custom resource has been found for controller %s",
42+
controller.getClass().getCanonicalName()
43+
)
44+
);
45+
}
46+
return (Class<R>) customResourceClass;
4147
}
4248

4349
static String getCrdName(ResourceController controller) {
@@ -48,38 +54,15 @@ static String getCrdName(ResourceController controller) {
4854
public static <T extends CustomResource> Class<? extends CustomResourceDoneable<T>>
4955
getCustomResourceDoneableClass(ResourceController<T> controller) {
5056
try {
51-
Class<? extends CustomResource> customResourceClass = getAnnotation(controller).customResourceClass();
52-
String className = customResourceClass.getPackage().getName() + "." + customResourceClass.getSimpleName() + "CustomResourceDoneable";
53-
54-
if (doneableClassCache.containsKey(customResourceClass)) {
55-
return (Class<? extends CustomResourceDoneable<T>>) doneableClassCache.get(customResourceClass);
56-
}
57-
58-
ClassPool pool = ClassPool.getDefault();
59-
pool.appendClassPath(new LoaderClassPath(Thread.currentThread().getContextClassLoader()));
60-
61-
CtClass superClass = pool.get(CustomResourceDoneable.class.getName());
62-
CtClass function = pool.get(Function.class.getName());
63-
CtClass customResource = pool.get(customResourceClass.getName());
64-
CtClass[] argTypes = {customResource, function};
65-
CtClass customDoneable = pool.makeClass(className, superClass);
66-
CtConstructor ctConstructor = CtNewConstructor.make(argTypes, null, "super($1, $2);", customDoneable);
67-
customDoneable.addConstructor(ctConstructor);
68-
69-
Class<? extends CustomResourceDoneable<T>> doneableClass;
70-
if (JAVA_VERSION >= 9) {
71-
doneableClass = (Class<? extends CustomResourceDoneable<T>>) customDoneable.toClass(customResourceClass);
72-
} else {
73-
doneableClass = (Class<? extends CustomResourceDoneable<T>>) customDoneable.toClass();
74-
}
75-
doneableClassCache.put(customResourceClass, doneableClass);
76-
return doneableClass;
77-
} catch (CannotCompileException | NotFoundException e) {
78-
throw new IllegalStateException(e);
57+
final Class<T> customResourceClass = getCustomResourceClass(controller);
58+
return (Class<? extends CustomResourceDoneable<T>>) Class.forName(customResourceClass.getCanonicalName() + "Doneable");
59+
} catch (ClassNotFoundException e) {
60+
e.printStackTrace();
61+
return null;
7962
}
8063
}
8164

82-
private static Controller getAnnotation(ResourceController controller) {
65+
private static Controller getAnnotation(ResourceController<?> controller) {
8366
return controller.getClass().getAnnotation(Controller.class);
8467
}
8568

operator-framework/src/main/java/io/javaoperatorsdk/operator/api/Controller.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package io.javaoperatorsdk.operator.api;
22

3-
import io.fabric8.kubernetes.client.CustomResource;
43

54
import java.lang.annotation.ElementType;
65
import java.lang.annotation.Retention;
@@ -14,8 +13,6 @@
1413

1514
String crdName();
1615

17-
Class<? extends CustomResource> customResourceClass();
18-
1916
/**
2017
* Optional finalizer name, if it is not,
2118
* the crdName will be used as the name of the finalizer too.
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package io.javaoperatorsdk.operator.processing;
2+
3+
import com.google.auto.service.AutoService;
4+
import com.squareup.javapoet.*;
5+
import io.fabric8.kubernetes.api.builder.Function;
6+
import io.fabric8.kubernetes.client.CustomResourceDoneable;
7+
import io.javaoperatorsdk.operator.api.ResourceController;
8+
import javax.annotation.processing.*;
9+
import javax.lang.model.SourceVersion;
10+
import javax.lang.model.element.*;
11+
import javax.lang.model.type.DeclaredType;
12+
import javax.lang.model.type.TypeKind;
13+
import javax.lang.model.type.TypeMirror;
14+
import javax.tools.Diagnostic;
15+
import javax.tools.FileObject;
16+
import javax.tools.JavaFileObject;
17+
import javax.tools.StandardLocation;
18+
import java.io.IOException;
19+
import java.io.PrintWriter;
20+
import java.util.ArrayList;
21+
import java.util.HashSet;
22+
import java.util.List;
23+
import java.util.Set;
24+
import java.util.stream.Collectors;
25+
26+
import static io.javaoperatorsdk.operator.ControllerUtils.CONTROLLERS_RESOURCE_PATH;
27+
28+
@SupportedAnnotationTypes(
29+
"io.javaoperatorsdk.operator.api.Controller")
30+
@SupportedSourceVersion(SourceVersion.RELEASE_8)
31+
@AutoService(Processor.class)
32+
public class ControllerAnnotationProcessor extends AbstractProcessor {
33+
private FileObject resource;
34+
PrintWriter printWriter = null;
35+
private Set<String> generatedDoneableClassFiles = new HashSet<>();
36+
37+
@Override
38+
public synchronized void init(ProcessingEnvironment processingEnv) {
39+
super.init(processingEnv);
40+
try {
41+
resource = processingEnv.getFiler().createResource(StandardLocation.CLASS_OUTPUT, "", CONTROLLERS_RESOURCE_PATH);
42+
} catch (IOException e) {
43+
throw new RuntimeException(e);
44+
}
45+
try {
46+
printWriter = new PrintWriter(resource.openOutputStream());
47+
} catch (IOException e) {
48+
throw new RuntimeException(e);
49+
}
50+
}
51+
52+
@Override
53+
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
54+
try {
55+
for (TypeElement annotation : annotations) {
56+
Set<? extends Element> annotatedElements
57+
= roundEnv.getElementsAnnotatedWith(annotation);
58+
annotatedElements.stream().filter(element -> element.getKind().equals(ElementKind.CLASS))
59+
.map(e -> (TypeElement) e)
60+
.forEach(e -> this.generateDoneableClass(e, printWriter));
61+
}
62+
} finally {
63+
printWriter.close();
64+
}
65+
return true;
66+
}
67+
68+
private void generateDoneableClass(TypeElement controllerClassSymbol, PrintWriter printWriter) {
69+
try {
70+
final TypeMirror resourceType = findResourceType(controllerClassSymbol);
71+
72+
TypeElement customerResourceTypeElement = processingEnv
73+
.getElementUtils()
74+
.getTypeElement(resourceType.toString());
75+
76+
final String doneableClassName = customerResourceTypeElement.getSimpleName() + "Doneable";
77+
final String destinationClassFileName = customerResourceTypeElement.getQualifiedName() + "Doneable";
78+
final TypeName customResourceType = TypeName.get(resourceType);
79+
80+
if (!generatedDoneableClassFiles.add(destinationClassFileName)) {
81+
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE,
82+
String.format(
83+
"%s already exist! adding the mapping to the %s",
84+
destinationClassFileName,
85+
CONTROLLERS_RESOURCE_PATH)
86+
);
87+
printWriter.println(controllerClassSymbol.getQualifiedName() + "," + customResourceType.toString());
88+
return;
89+
}
90+
JavaFileObject builderFile = processingEnv.getFiler()
91+
.createSourceFile(destinationClassFileName);
92+
93+
try (PrintWriter out = new PrintWriter(builderFile.openWriter())) {
94+
printWriter.println(controllerClassSymbol.getQualifiedName() + "," + customResourceType.toString());
95+
final MethodSpec constructor = MethodSpec.constructorBuilder()
96+
.addModifiers(Modifier.PUBLIC)
97+
.addParameter(customResourceType, "resource")
98+
.addParameter(Function.class, "function")
99+
.addStatement("super(resource,function)")
100+
.build();
101+
102+
103+
final TypeSpec typeSpec = TypeSpec.classBuilder(doneableClassName)
104+
.superclass(ParameterizedTypeName.get(ClassName.get(CustomResourceDoneable.class), customResourceType))
105+
.addModifiers(Modifier.PUBLIC)
106+
.addMethod(constructor)
107+
.build();
108+
109+
final PackageElement packageElement = processingEnv.getElementUtils().getPackageOf(customerResourceTypeElement);
110+
JavaFile file = JavaFile.builder(packageElement.getQualifiedName().toString(), typeSpec)
111+
.build();
112+
file.writeTo(out);
113+
}
114+
} catch (Exception ioException) {
115+
ioException.printStackTrace();
116+
}
117+
}
118+
119+
private TypeMirror findResourceType(TypeElement controllerClassSymbol) throws Exception {
120+
try {
121+
final DeclaredType controllerType = collectAllInterfaces(controllerClassSymbol)
122+
.stream()
123+
.filter(i -> i.toString()
124+
.startsWith(ResourceController.class.getCanonicalName())
125+
)
126+
.findFirst()
127+
.orElseThrow(() -> new Exception("ResourceController is not implemented by " + controllerClassSymbol.toString()));
128+
129+
return controllerType.getTypeArguments().get(0);
130+
} catch (Exception e) {
131+
e.printStackTrace();
132+
return null;
133+
}
134+
}
135+
136+
private List<DeclaredType> collectAllInterfaces(TypeElement element) {
137+
try {
138+
List<DeclaredType> interfaces = new ArrayList<>(element.getInterfaces()).stream().map(t -> (DeclaredType) t).collect(Collectors.toList());
139+
TypeElement superclass = ((TypeElement) ((DeclaredType) element.getSuperclass()).asElement());
140+
while (superclass.getSuperclass().getKind() != TypeKind.NONE) {
141+
interfaces.addAll(superclass.getInterfaces().stream().map(t -> (DeclaredType) t).collect(Collectors.toList()));
142+
superclass = ((TypeElement) ((DeclaredType) superclass.getSuperclass()).asElement());
143+
}
144+
return interfaces;
145+
} catch (Exception e) {
146+
e.printStackTrace();
147+
return null;
148+
}
149+
}
150+
}

operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerUtilsTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public void returnsValuesFromControllerAnnotationFinalizer() {
2525
assertTrue(CustomResourceDoneable.class.isAssignableFrom(ControllerUtils.getCustomResourceDoneableClass(new TestCustomResourceController(null))));
2626
}
2727

28-
@Controller(crdName = "test.crd", customResourceClass = TestCustomResource.class, finalizerName = CUSTOM_FINALIZER_NAME)
28+
@Controller(crdName = "test.crd", finalizerName = CUSTOM_FINALIZER_NAME)
2929
static class TestCustomFinalizerController implements ResourceController<TestCustomResource> {
3030

3131
@Override

0 commit comments

Comments
 (0)