Skip to content

Commit 39c00ef

Browse files
authored
Merge pull request #255 from psycho-ir/improve-annotation-processor
Support Parameterized abstract controllers
2 parents 62ea9f6 + e72bf95 commit 39c00ef

15 files changed

+329
-72
lines changed

operator-framework/src/main/java/io/javaoperatorsdk/operator/processing/annotation/AccumulativeMappingWriter.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,13 @@
1010
import javax.annotation.processing.ProcessingEnvironment;
1111
import javax.tools.StandardLocation;
1212

13+
/**
14+
* The writer is able to load an existing resource file as a Map and override it with the new
15+
* mappings added to the existing mappings. Every entry corresponds to a line in the resource file
16+
* where key and values are separated by comma.
17+
*/
1318
class AccumulativeMappingWriter {
19+
1420
private Map<String, String> mappings = new ConcurrentHashMap<>();
1521
private final String resourcePath;
1622
private final ProcessingEnvironment processingEnvironment;
@@ -41,11 +47,16 @@ public AccumulativeMappingWriter loadExistingMappings() {
4147
return this;
4248
}
4349

50+
/** Add a new mapping */
4451
public AccumulativeMappingWriter add(String key, String value) {
4552
this.mappings.put(key, value);
4653
return this;
4754
}
4855

56+
/**
57+
* Generates or overrise the resource file with the given path
58+
* ({@linkAccumulativeMappingWriter#resourcePath})
59+
*/
4960
public void flush() {
5061
PrintWriter printWriter = null;
5162
try {

operator-framework/src/main/java/io/javaoperatorsdk/operator/processing/annotation/ControllerAnnotationProcessor.java

Lines changed: 39 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,12 @@
1111
import com.squareup.javapoet.TypeName;
1212
import com.squareup.javapoet.TypeSpec;
1313
import io.fabric8.kubernetes.api.builder.Function;
14+
import io.fabric8.kubernetes.client.CustomResource;
1415
import io.fabric8.kubernetes.client.CustomResourceDoneable;
1516
import io.javaoperatorsdk.operator.api.ResourceController;
1617
import java.io.PrintWriter;
17-
import java.util.ArrayList;
1818
import java.util.HashSet;
19-
import java.util.List;
2019
import java.util.Set;
21-
import java.util.stream.Collectors;
2220
import javax.annotation.processing.AbstractProcessor;
2321
import javax.annotation.processing.ProcessingEnvironment;
2422
import javax.annotation.processing.Processor;
@@ -32,7 +30,6 @@
3230
import javax.lang.model.element.PackageElement;
3331
import javax.lang.model.element.TypeElement;
3432
import javax.lang.model.type.DeclaredType;
35-
import javax.lang.model.type.TypeKind;
3633
import javax.lang.model.type.TypeMirror;
3734
import javax.tools.Diagnostic;
3835
import javax.tools.JavaFileObject;
@@ -41,9 +38,12 @@
4138
@SupportedSourceVersion(SourceVersion.RELEASE_8)
4239
@AutoService(Processor.class)
4340
public class ControllerAnnotationProcessor extends AbstractProcessor {
41+
4442
private AccumulativeMappingWriter controllersResourceWriter;
4543
private AccumulativeMappingWriter doneablesResourceWriter;
46-
private Set<String> generatedDoneableClassFiles = new HashSet<>();
44+
private TypeParameterResolver typeParameterResolver;
45+
private final Set<String> generatedDoneableClassFiles = new HashSet<>();
46+
private DeclaredType fallbackCustomResourceType;
4747

4848
@Override
4949
public synchronized void init(ProcessingEnvironment processingEnv) {
@@ -54,6 +54,18 @@ public synchronized void init(ProcessingEnvironment processingEnv) {
5454
doneablesResourceWriter =
5555
new AccumulativeMappingWriter(DONEABLES_RESOURCE_PATH, processingEnv)
5656
.loadExistingMappings();
57+
58+
doneablesResourceWriter.add(
59+
CustomResource.class.getCanonicalName(), CustomResourceDoneable.class.getCanonicalName());
60+
61+
typeParameterResolver = initializeResolver(processingEnv);
62+
fallbackCustomResourceType =
63+
processingEnv
64+
.getTypeUtils()
65+
.getDeclaredType(
66+
processingEnv
67+
.getElementUtils()
68+
.getTypeElement(CustomResourceDoneable.class.getCanonicalName()));
5769
}
5870

5971
@Override
@@ -64,7 +76,7 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
6476
annotatedElements.stream()
6577
.filter(element -> element.getKind().equals(ElementKind.CLASS))
6678
.map(e -> (TypeElement) e)
67-
.forEach(e -> this.generateDoneableClass(e));
79+
.forEach(this::generateDoneableClass);
6880
}
6981
} finally {
7082
if (roundEnv.processingOver()) {
@@ -75,9 +87,27 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
7587
return true;
7688
}
7789

90+
private TypeParameterResolver initializeResolver(ProcessingEnvironment processingEnv) {
91+
final DeclaredType resourceControllerType =
92+
processingEnv
93+
.getTypeUtils()
94+
.getDeclaredType(
95+
processingEnv
96+
.getElementUtils()
97+
.getTypeElement(ResourceController.class.getCanonicalName()),
98+
processingEnv.getTypeUtils().getWildcardType(null, null));
99+
return new TypeParameterResolver(resourceControllerType, 0);
100+
}
101+
78102
private void generateDoneableClass(TypeElement controllerClassSymbol) {
79103
try {
80104
final TypeMirror resourceType = findResourceType(controllerClassSymbol);
105+
if (resourceType == null) {
106+
controllersResourceWriter.add(
107+
controllerClassSymbol.getQualifiedName().toString(),
108+
CustomResource.class.getCanonicalName());
109+
return;
110+
}
81111

82112
TypeElement customerResourceTypeElement =
83113
processingEnv.getElementUtils().getTypeElement(resourceType.toString());
@@ -136,38 +166,11 @@ private void generateDoneableClass(TypeElement controllerClassSymbol) {
136166
}
137167
}
138168

139-
private TypeMirror findResourceType(TypeElement controllerClassSymbol) throws Exception {
169+
private TypeMirror findResourceType(TypeElement controllerClassSymbol) {
140170
try {
141-
final DeclaredType controllerType =
142-
collectAllInterfaces(controllerClassSymbol).stream()
143-
.filter(i -> i.toString().startsWith(ResourceController.class.getCanonicalName()))
144-
.findFirst()
145-
.orElseThrow(
146-
() ->
147-
new Exception(
148-
"ResourceController is not implemented by "
149-
+ controllerClassSymbol.toString()));
150-
return controllerType.getTypeArguments().get(0);
151-
} catch (Exception e) {
152-
e.printStackTrace();
153-
return null;
154-
}
155-
}
171+
return typeParameterResolver.resolve(
172+
processingEnv.getTypeUtils(), (DeclaredType) controllerClassSymbol.asType());
156173

157-
private List<DeclaredType> collectAllInterfaces(TypeElement element) {
158-
try {
159-
List<DeclaredType> interfaces =
160-
new ArrayList<>(element.getInterfaces())
161-
.stream().map(t -> (DeclaredType) t).collect(Collectors.toList());
162-
TypeElement superclass = ((TypeElement) ((DeclaredType) element.getSuperclass()).asElement());
163-
while (superclass.getSuperclass().getKind() != TypeKind.NONE) {
164-
interfaces.addAll(
165-
superclass.getInterfaces().stream()
166-
.map(t -> (DeclaredType) t)
167-
.collect(Collectors.toList()));
168-
superclass = ((TypeElement) ((DeclaredType) superclass.getSuperclass()).asElement());
169-
}
170-
return interfaces;
171174
} catch (Exception e) {
172175
e.printStackTrace();
173176
return null;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package io.javaoperatorsdk.operator.processing.annotation;
2+
3+
import static javax.lang.model.type.TypeKind.DECLARED;
4+
import static javax.lang.model.type.TypeKind.TYPEVAR;
5+
6+
import java.util.ArrayList;
7+
import java.util.List;
8+
import java.util.stream.Collectors;
9+
import java.util.stream.IntStream;
10+
import javax.lang.model.element.TypeElement;
11+
import javax.lang.model.element.TypeParameterElement;
12+
import javax.lang.model.type.DeclaredType;
13+
import javax.lang.model.type.TypeKind;
14+
import javax.lang.model.type.TypeMirror;
15+
import javax.lang.model.type.TypeVariable;
16+
import javax.lang.model.util.Types;
17+
18+
/** This class can resolve a type parameter in the given index to the actual type defined. */
19+
class TypeParameterResolver {
20+
21+
private final DeclaredType interestedClass;
22+
private final int interestedTypeArgumentIndex;
23+
24+
public TypeParameterResolver(DeclaredType interestedClass, int interestedTypeArgumentIndex) {
25+
26+
this.interestedClass = interestedClass;
27+
this.interestedTypeArgumentIndex = interestedTypeArgumentIndex;
28+
}
29+
30+
/**
31+
* @param typeUtils Type utilities, During the annotation processing processingEnv.getTypeUtils()
32+
* can be passed.
33+
* @param declaredType Class or Interface which extends or implements the interestedClass, and the
34+
* interest is getting the actual declared type is used.
35+
* @return the type of the parameter if it can be resolved from the the given declareType,
36+
* otherwise it returns null
37+
*/
38+
public TypeMirror resolve(Types typeUtils, DeclaredType declaredType) {
39+
final var chain = findChain(typeUtils, declaredType);
40+
var lastIndex = chain.size() - 1;
41+
String typeName = "";
42+
final List<? extends TypeMirror> typeArguments = (chain.get(lastIndex)).getTypeArguments();
43+
if (typeArguments.isEmpty()) {
44+
return null;
45+
}
46+
if (typeArguments.get(interestedTypeArgumentIndex).getKind() == TYPEVAR) {
47+
typeName =
48+
((TypeVariable) typeArguments.get(interestedTypeArgumentIndex))
49+
.asElement()
50+
.getSimpleName()
51+
.toString();
52+
} else if (typeArguments.get(interestedTypeArgumentIndex).getKind() == DECLARED) {
53+
return typeArguments.get(0);
54+
}
55+
56+
while (lastIndex > 0) {
57+
lastIndex -= 1;
58+
final List<? extends TypeMirror> tArguments = (chain.get(lastIndex)).getTypeArguments();
59+
final List<? extends TypeParameterElement> typeParameters =
60+
((TypeElement) ((chain.get(lastIndex)).asElement())).getTypeParameters();
61+
62+
final var typeIndex = getTypeIndexWithName(typeName, typeParameters);
63+
64+
final TypeMirror matchedType = tArguments.get(typeIndex);
65+
if (matchedType.getKind() == TYPEVAR) {
66+
typeName = ((TypeVariable) matchedType).asElement().getSimpleName().toString();
67+
} else if (matchedType.getKind() == DECLARED) {
68+
return matchedType;
69+
}
70+
}
71+
return null;
72+
}
73+
74+
private int getTypeIndexWithName(
75+
String typeName, List<? extends TypeParameterElement> typeParameters) {
76+
return IntStream.range(0, typeParameters.size())
77+
.filter(i -> typeParameters.get(i).getSimpleName().toString().equals(typeName))
78+
.findFirst()
79+
.getAsInt();
80+
}
81+
82+
private List<DeclaredType> findChain(Types typeUtils, DeclaredType declaredType) {
83+
84+
final var result = new ArrayList<DeclaredType>();
85+
result.add(declaredType);
86+
var superElement = ((TypeElement) declaredType.asElement());
87+
var superclass = (DeclaredType) superElement.getSuperclass();
88+
89+
final var matchingInterfaces = getMatchingInterfaces(typeUtils, superElement);
90+
// if chain of interfaces is not empty, there is no reason to continue the lookup
91+
// as interfaces do not extend the classes
92+
if (matchingInterfaces.size() > 0) {
93+
result.addAll(matchingInterfaces);
94+
return result;
95+
}
96+
97+
while (superclass.getKind() != TypeKind.NONE) {
98+
99+
if (typeUtils.isAssignable(superclass, interestedClass)) {
100+
result.add(superclass);
101+
}
102+
103+
superElement = (TypeElement) superclass.asElement();
104+
ArrayList<DeclaredType> ifs = getMatchingInterfaces(typeUtils, superElement);
105+
if (ifs.size() > 0) {
106+
result.addAll(ifs);
107+
return result;
108+
}
109+
110+
if (superElement.getSuperclass().getKind() == TypeKind.NONE) {
111+
break;
112+
}
113+
superclass = (DeclaredType) superElement.getSuperclass();
114+
}
115+
return result;
116+
}
117+
118+
private ArrayList<DeclaredType> getMatchingInterfaces(Types typeUtils, TypeElement superElement) {
119+
final var result = new ArrayList<DeclaredType>();
120+
121+
final var matchedInterfaces =
122+
superElement.getInterfaces().stream()
123+
.filter(intface -> typeUtils.isAssignable(intface, interestedClass))
124+
.map(i -> (DeclaredType) i)
125+
.collect(Collectors.toList());
126+
if (matchedInterfaces.size() > 0) {
127+
result.addAll(matchedInterfaces);
128+
final var lastFoundInterface = result.get(result.size() - 1);
129+
final var marchingInterfaces = findChainOfInterfaces(typeUtils, lastFoundInterface);
130+
result.addAll(marchingInterfaces);
131+
}
132+
return result;
133+
}
134+
135+
private List<DeclaredType> findChainOfInterfaces(Types typeUtils, DeclaredType parentInterface) {
136+
final var result = new ArrayList<DeclaredType>();
137+
var matchingInterfaces =
138+
((TypeElement) parentInterface.asElement())
139+
.getInterfaces().stream()
140+
.filter(i -> typeUtils.isAssignable(i, interestedClass))
141+
.map(i -> (DeclaredType) i)
142+
.collect(Collectors.toList());
143+
while (matchingInterfaces.size() > 0) {
144+
result.addAll(matchingInterfaces);
145+
final var lastFoundInterface = matchingInterfaces.get(matchingInterfaces.size() - 1);
146+
matchingInterfaces =
147+
((TypeElement) lastFoundInterface.asElement())
148+
.getInterfaces().stream()
149+
.filter(i -> typeUtils.isAssignable(i, interestedClass))
150+
.map(i -> (DeclaredType) i)
151+
.collect(Collectors.toList());
152+
}
153+
return result;
154+
}
155+
}

operator-framework/src/test/java/io/javaoperatorsdk/operator/processing/annotation/ControllerAnnotationProcessorTest.java

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,33 +9,55 @@
99
import org.junit.jupiter.api.Test;
1010

1111
class ControllerAnnotationProcessorTest {
12+
1213
@Test
1314
public void generateCorrectDoneableClassIfInterfaceIsSecond() {
1415
Compilation compilation =
1516
Compiler.javac()
1617
.withProcessors(new ControllerAnnotationProcessor())
17-
.compile(JavaFileObjects.forResource("ControllerImplemented2Interfaces.java"));
18+
.compile(
19+
JavaFileObjects.forResource(
20+
"compile-fixtures/ControllerImplemented2Interfaces.java"));
1821
CompilationSubject.assertThat(compilation).succeeded();
1922

2023
final JavaFileObject expectedResource =
21-
JavaFileObjects.forResource("ControllerImplemented2InterfacesExpected.java");
24+
JavaFileObjects.forResource(
25+
"compile-fixtures/ControllerImplemented2InterfacesExpected.java");
2226
JavaFileObjectSubject.assertThat(compilation.generatedSourceFiles().get(0))
2327
.hasSourceEquivalentTo(expectedResource);
2428
}
2529

2630
@Test
2731
public void generateCorrectDoneableClassIfThereIsAbstractBaseController() {
32+
Compilation compilation =
33+
Compiler.javac()
34+
.withProcessors(new ControllerAnnotationProcessor())
35+
.compile(
36+
JavaFileObjects.forResource("compile-fixtures/AbstractController.java"),
37+
JavaFileObjects.forResource(
38+
"compile-fixtures/ControllerImplementedIntermediateAbstractClass.java"));
39+
CompilationSubject.assertThat(compilation).succeeded();
2840

41+
final JavaFileObject expectedResource =
42+
JavaFileObjects.forResource(
43+
"compile-fixtures/ControllerImplementedIntermediateAbstractClassExpected.java");
44+
JavaFileObjectSubject.assertThat(compilation.generatedSourceFiles().get(0))
45+
.hasSourceEquivalentTo(expectedResource);
46+
}
47+
48+
@Test
49+
public void generateDoneableClasswithMultilevelHierarchy() {
2950
Compilation compilation =
3051
Compiler.javac()
3152
.withProcessors(new ControllerAnnotationProcessor())
3253
.compile(
33-
JavaFileObjects.forResource("AbstractController.java"),
34-
JavaFileObjects.forResource("ControllerImplementedIntermediateAbstractClass.java"));
54+
JavaFileObjects.forResource("compile-fixtures/AdditionalControllerInterface.java"),
55+
JavaFileObjects.forResource("compile-fixtures/MultilevelAbstractController.java"),
56+
JavaFileObjects.forResource("compile-fixtures/MultilevelController.java"));
3557
CompilationSubject.assertThat(compilation).succeeded();
3658

3759
final JavaFileObject expectedResource =
38-
JavaFileObjects.forResource("ControllerImplementedIntermediateAbstractClassExpected.java");
60+
JavaFileObjects.forResource("compile-fixtures/MultilevelControllerExpected.java");
3961
JavaFileObjectSubject.assertThat(compilation.generatedSourceFiles().get(0))
4062
.hasSourceEquivalentTo(expectedResource);
4163
}

operator-framework/src/test/resources/AbstractController.java

Lines changed: 0 additions & 12 deletions
This file was deleted.

0 commit comments

Comments
 (0)