Skip to content

Commit

Permalink
Merge pull request #255 from psycho-ir/improve-annotation-processor
Browse files Browse the repository at this point in the history
Support Parameterized abstract controllers
  • Loading branch information
s-soroosh authored Dec 14, 2020
2 parents 62ea9f6 + e72bf95 commit 39c00ef
Show file tree
Hide file tree
Showing 15 changed files with 329 additions and 72 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@
import javax.annotation.processing.ProcessingEnvironment;
import javax.tools.StandardLocation;

/**
* The writer is able to load an existing resource file as a Map and override it with the new
* mappings added to the existing mappings. Every entry corresponds to a line in the resource file
* where key and values are separated by comma.
*/
class AccumulativeMappingWriter {

private Map<String, String> mappings = new ConcurrentHashMap<>();
private final String resourcePath;
private final ProcessingEnvironment processingEnvironment;
Expand Down Expand Up @@ -41,11 +47,16 @@ public AccumulativeMappingWriter loadExistingMappings() {
return this;
}

/** Add a new mapping */
public AccumulativeMappingWriter add(String key, String value) {
this.mappings.put(key, value);
return this;
}

/**
* Generates or overrise the resource file with the given path
* ({@linkAccumulativeMappingWriter#resourcePath})
*/
public void flush() {
PrintWriter printWriter = null;
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,12 @@
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import io.fabric8.kubernetes.api.builder.Function;
import io.fabric8.kubernetes.client.CustomResource;
import io.fabric8.kubernetes.client.CustomResourceDoneable;
import io.javaoperatorsdk.operator.api.ResourceController;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
Expand All @@ -32,7 +30,6 @@
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
Expand All @@ -41,9 +38,12 @@
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@AutoService(Processor.class)
public class ControllerAnnotationProcessor extends AbstractProcessor {

private AccumulativeMappingWriter controllersResourceWriter;
private AccumulativeMappingWriter doneablesResourceWriter;
private Set<String> generatedDoneableClassFiles = new HashSet<>();
private TypeParameterResolver typeParameterResolver;
private final Set<String> generatedDoneableClassFiles = new HashSet<>();
private DeclaredType fallbackCustomResourceType;

@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
Expand All @@ -54,6 +54,18 @@ public synchronized void init(ProcessingEnvironment processingEnv) {
doneablesResourceWriter =
new AccumulativeMappingWriter(DONEABLES_RESOURCE_PATH, processingEnv)
.loadExistingMappings();

doneablesResourceWriter.add(
CustomResource.class.getCanonicalName(), CustomResourceDoneable.class.getCanonicalName());

typeParameterResolver = initializeResolver(processingEnv);
fallbackCustomResourceType =
processingEnv
.getTypeUtils()
.getDeclaredType(
processingEnv
.getElementUtils()
.getTypeElement(CustomResourceDoneable.class.getCanonicalName()));
}

@Override
Expand All @@ -64,7 +76,7 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
annotatedElements.stream()
.filter(element -> element.getKind().equals(ElementKind.CLASS))
.map(e -> (TypeElement) e)
.forEach(e -> this.generateDoneableClass(e));
.forEach(this::generateDoneableClass);
}
} finally {
if (roundEnv.processingOver()) {
Expand All @@ -75,9 +87,27 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
return true;
}

private TypeParameterResolver initializeResolver(ProcessingEnvironment processingEnv) {
final DeclaredType resourceControllerType =
processingEnv
.getTypeUtils()
.getDeclaredType(
processingEnv
.getElementUtils()
.getTypeElement(ResourceController.class.getCanonicalName()),
processingEnv.getTypeUtils().getWildcardType(null, null));
return new TypeParameterResolver(resourceControllerType, 0);
}

private void generateDoneableClass(TypeElement controllerClassSymbol) {
try {
final TypeMirror resourceType = findResourceType(controllerClassSymbol);
if (resourceType == null) {
controllersResourceWriter.add(
controllerClassSymbol.getQualifiedName().toString(),
CustomResource.class.getCanonicalName());
return;
}

TypeElement customerResourceTypeElement =
processingEnv.getElementUtils().getTypeElement(resourceType.toString());
Expand Down Expand Up @@ -136,38 +166,11 @@ private void generateDoneableClass(TypeElement controllerClassSymbol) {
}
}

private TypeMirror findResourceType(TypeElement controllerClassSymbol) throws Exception {
private TypeMirror findResourceType(TypeElement controllerClassSymbol) {
try {
final DeclaredType controllerType =
collectAllInterfaces(controllerClassSymbol).stream()
.filter(i -> i.toString().startsWith(ResourceController.class.getCanonicalName()))
.findFirst()
.orElseThrow(
() ->
new Exception(
"ResourceController is not implemented by "
+ controllerClassSymbol.toString()));
return controllerType.getTypeArguments().get(0);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
return typeParameterResolver.resolve(
processingEnv.getTypeUtils(), (DeclaredType) controllerClassSymbol.asType());

private List<DeclaredType> collectAllInterfaces(TypeElement element) {
try {
List<DeclaredType> interfaces =
new ArrayList<>(element.getInterfaces())
.stream().map(t -> (DeclaredType) t).collect(Collectors.toList());
TypeElement superclass = ((TypeElement) ((DeclaredType) element.getSuperclass()).asElement());
while (superclass.getSuperclass().getKind() != TypeKind.NONE) {
interfaces.addAll(
superclass.getInterfaces().stream()
.map(t -> (DeclaredType) t)
.collect(Collectors.toList()));
superclass = ((TypeElement) ((DeclaredType) superclass.getSuperclass()).asElement());
}
return interfaces;
} catch (Exception e) {
e.printStackTrace();
return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package io.javaoperatorsdk.operator.processing.annotation;

import static javax.lang.model.type.TypeKind.DECLARED;
import static javax.lang.model.type.TypeKind.TYPEVAR;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.TypeParameterElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.type.TypeVariable;
import javax.lang.model.util.Types;

/** This class can resolve a type parameter in the given index to the actual type defined. */
class TypeParameterResolver {

private final DeclaredType interestedClass;
private final int interestedTypeArgumentIndex;

public TypeParameterResolver(DeclaredType interestedClass, int interestedTypeArgumentIndex) {

this.interestedClass = interestedClass;
this.interestedTypeArgumentIndex = interestedTypeArgumentIndex;
}

/**
* @param typeUtils Type utilities, During the annotation processing processingEnv.getTypeUtils()
* can be passed.
* @param declaredType Class or Interface which extends or implements the interestedClass, and the
* interest is getting the actual declared type is used.
* @return the type of the parameter if it can be resolved from the the given declareType,
* otherwise it returns null
*/
public TypeMirror resolve(Types typeUtils, DeclaredType declaredType) {
final var chain = findChain(typeUtils, declaredType);
var lastIndex = chain.size() - 1;
String typeName = "";
final List<? extends TypeMirror> typeArguments = (chain.get(lastIndex)).getTypeArguments();
if (typeArguments.isEmpty()) {
return null;
}
if (typeArguments.get(interestedTypeArgumentIndex).getKind() == TYPEVAR) {
typeName =
((TypeVariable) typeArguments.get(interestedTypeArgumentIndex))
.asElement()
.getSimpleName()
.toString();
} else if (typeArguments.get(interestedTypeArgumentIndex).getKind() == DECLARED) {
return typeArguments.get(0);
}

while (lastIndex > 0) {
lastIndex -= 1;
final List<? extends TypeMirror> tArguments = (chain.get(lastIndex)).getTypeArguments();
final List<? extends TypeParameterElement> typeParameters =
((TypeElement) ((chain.get(lastIndex)).asElement())).getTypeParameters();

final var typeIndex = getTypeIndexWithName(typeName, typeParameters);

final TypeMirror matchedType = tArguments.get(typeIndex);
if (matchedType.getKind() == TYPEVAR) {
typeName = ((TypeVariable) matchedType).asElement().getSimpleName().toString();
} else if (matchedType.getKind() == DECLARED) {
return matchedType;
}
}
return null;
}

private int getTypeIndexWithName(
String typeName, List<? extends TypeParameterElement> typeParameters) {
return IntStream.range(0, typeParameters.size())
.filter(i -> typeParameters.get(i).getSimpleName().toString().equals(typeName))
.findFirst()
.getAsInt();
}

private List<DeclaredType> findChain(Types typeUtils, DeclaredType declaredType) {

final var result = new ArrayList<DeclaredType>();
result.add(declaredType);
var superElement = ((TypeElement) declaredType.asElement());
var superclass = (DeclaredType) superElement.getSuperclass();

final var matchingInterfaces = getMatchingInterfaces(typeUtils, superElement);
// if chain of interfaces is not empty, there is no reason to continue the lookup
// as interfaces do not extend the classes
if (matchingInterfaces.size() > 0) {
result.addAll(matchingInterfaces);
return result;
}

while (superclass.getKind() != TypeKind.NONE) {

if (typeUtils.isAssignable(superclass, interestedClass)) {
result.add(superclass);
}

superElement = (TypeElement) superclass.asElement();
ArrayList<DeclaredType> ifs = getMatchingInterfaces(typeUtils, superElement);
if (ifs.size() > 0) {
result.addAll(ifs);
return result;
}

if (superElement.getSuperclass().getKind() == TypeKind.NONE) {
break;
}
superclass = (DeclaredType) superElement.getSuperclass();
}
return result;
}

private ArrayList<DeclaredType> getMatchingInterfaces(Types typeUtils, TypeElement superElement) {
final var result = new ArrayList<DeclaredType>();

final var matchedInterfaces =
superElement.getInterfaces().stream()
.filter(intface -> typeUtils.isAssignable(intface, interestedClass))
.map(i -> (DeclaredType) i)
.collect(Collectors.toList());
if (matchedInterfaces.size() > 0) {
result.addAll(matchedInterfaces);
final var lastFoundInterface = result.get(result.size() - 1);
final var marchingInterfaces = findChainOfInterfaces(typeUtils, lastFoundInterface);
result.addAll(marchingInterfaces);
}
return result;
}

private List<DeclaredType> findChainOfInterfaces(Types typeUtils, DeclaredType parentInterface) {
final var result = new ArrayList<DeclaredType>();
var matchingInterfaces =
((TypeElement) parentInterface.asElement())
.getInterfaces().stream()
.filter(i -> typeUtils.isAssignable(i, interestedClass))
.map(i -> (DeclaredType) i)
.collect(Collectors.toList());
while (matchingInterfaces.size() > 0) {
result.addAll(matchingInterfaces);
final var lastFoundInterface = matchingInterfaces.get(matchingInterfaces.size() - 1);
matchingInterfaces =
((TypeElement) lastFoundInterface.asElement())
.getInterfaces().stream()
.filter(i -> typeUtils.isAssignable(i, interestedClass))
.map(i -> (DeclaredType) i)
.collect(Collectors.toList());
}
return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,55 @@
import org.junit.jupiter.api.Test;

class ControllerAnnotationProcessorTest {

@Test
public void generateCorrectDoneableClassIfInterfaceIsSecond() {
Compilation compilation =
Compiler.javac()
.withProcessors(new ControllerAnnotationProcessor())
.compile(JavaFileObjects.forResource("ControllerImplemented2Interfaces.java"));
.compile(
JavaFileObjects.forResource(
"compile-fixtures/ControllerImplemented2Interfaces.java"));
CompilationSubject.assertThat(compilation).succeeded();

final JavaFileObject expectedResource =
JavaFileObjects.forResource("ControllerImplemented2InterfacesExpected.java");
JavaFileObjects.forResource(
"compile-fixtures/ControllerImplemented2InterfacesExpected.java");
JavaFileObjectSubject.assertThat(compilation.generatedSourceFiles().get(0))
.hasSourceEquivalentTo(expectedResource);
}

@Test
public void generateCorrectDoneableClassIfThereIsAbstractBaseController() {
Compilation compilation =
Compiler.javac()
.withProcessors(new ControllerAnnotationProcessor())
.compile(
JavaFileObjects.forResource("compile-fixtures/AbstractController.java"),
JavaFileObjects.forResource(
"compile-fixtures/ControllerImplementedIntermediateAbstractClass.java"));
CompilationSubject.assertThat(compilation).succeeded();

final JavaFileObject expectedResource =
JavaFileObjects.forResource(
"compile-fixtures/ControllerImplementedIntermediateAbstractClassExpected.java");
JavaFileObjectSubject.assertThat(compilation.generatedSourceFiles().get(0))
.hasSourceEquivalentTo(expectedResource);
}

@Test
public void generateDoneableClasswithMultilevelHierarchy() {
Compilation compilation =
Compiler.javac()
.withProcessors(new ControllerAnnotationProcessor())
.compile(
JavaFileObjects.forResource("AbstractController.java"),
JavaFileObjects.forResource("ControllerImplementedIntermediateAbstractClass.java"));
JavaFileObjects.forResource("compile-fixtures/AdditionalControllerInterface.java"),
JavaFileObjects.forResource("compile-fixtures/MultilevelAbstractController.java"),
JavaFileObjects.forResource("compile-fixtures/MultilevelController.java"));
CompilationSubject.assertThat(compilation).succeeded();

final JavaFileObject expectedResource =
JavaFileObjects.forResource("ControllerImplementedIntermediateAbstractClassExpected.java");
JavaFileObjects.forResource("compile-fixtures/MultilevelControllerExpected.java");
JavaFileObjectSubject.assertThat(compilation.generatedSourceFiles().get(0))
.hasSourceEquivalentTo(expectedResource);
}
Expand Down
12 changes: 0 additions & 12 deletions operator-framework/src/test/resources/AbstractController.java

This file was deleted.

Loading

0 comments on commit 39c00ef

Please sign in to comment.