diff --git a/operator-framework/pom.xml b/operator-framework/pom.xml index d3e3df7b19..60bca8c8ff 100644 --- a/operator-framework/pom.xml +++ b/operator-framework/pom.xml @@ -40,11 +40,6 @@ org.apache.commons commons-lang3 - - com.squareup - javapoet - compile - org.slf4j slf4j-api diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/ControllerConfigurationAnnotationProcessor.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/ControllerConfigurationAnnotationProcessor.java index d94233d8d8..700d5a40c0 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/ControllerConfigurationAnnotationProcessor.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/ControllerConfigurationAnnotationProcessor.java @@ -34,8 +34,6 @@ import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; -import com.squareup.javapoet.TypeName; - import static io.javaoperatorsdk.operator.config.runtime.RuntimeControllerMetadata.RECONCILERS_RESOURCE_PATH; @SupportedAnnotationTypes("io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration") @@ -103,9 +101,12 @@ private void recordCRType(TypeElement controllerClassSymbol) { + "': ignoring!"); return; } - final TypeName customResourceType = TypeName.get(resourceType); + // the resolved resource type is always a declared type, so its element is a TypeElement + final var customResourceType = + (TypeElement) processingEnv.getTypeUtils().asElement(resourceType); controllersResourceWriter.add( - controllerClassSymbol.getQualifiedName().toString(), customResourceType.toString()); + controllerClassSymbol.getQualifiedName().toString(), + customResourceType.getQualifiedName().toString()); } catch (Exception ioException) { log.error("Error", ioException); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/ControllerConfigurationAnnotationProcessorTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/ControllerConfigurationAnnotationProcessorTest.java index 058709aeb3..e623bf61be 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/ControllerConfigurationAnnotationProcessorTest.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/ControllerConfigurationAnnotationProcessorTest.java @@ -15,6 +15,11 @@ */ package io.javaoperatorsdk.operator.config.runtime; +import java.io.IOException; +import java.io.UncheckedIOException; + +import javax.tools.StandardLocation; + import org.junit.jupiter.api.Test; import com.google.testing.compile.Compilation; @@ -22,6 +27,9 @@ import com.google.testing.compile.Compiler; import com.google.testing.compile.JavaFileObjects; +import static io.javaoperatorsdk.operator.config.runtime.RuntimeControllerMetadata.RECONCILERS_RESOURCE_PATH; +import static org.assertj.core.api.Assertions.assertThat; + class ControllerConfigurationAnnotationProcessorTest { @Test @@ -33,6 +41,9 @@ public void generateCorrectDoneableClassIfInterfaceIsSecond() { JavaFileObjects.forResource( "compile-fixtures/ReconcilerImplemented2Interfaces.java")); CompilationSubject.assertThat(compilation).succeeded(); + assertMapping( + compilation, + "io.ReconcilerImplemented2Interfaces,io.ReconcilerImplemented2Interfaces.MyCustomResource"); } @Test @@ -45,6 +56,9 @@ public void generateCorrectDoneableClassIfThereIsAbstractBaseController() { JavaFileObjects.forResource( "compile-fixtures/ReconcilerImplementedIntermediateAbstractClass.java")); CompilationSubject.assertThat(compilation).succeeded(); + assertMapping( + compilation, + "io.ReconcilerImplementedIntermediateAbstractClass,io.AbstractReconciler.MyCustomResource"); } @Test @@ -57,5 +71,74 @@ public void generateDoneableClassWithMultilevelHierarchy() { JavaFileObjects.forResource("compile-fixtures/MultilevelAbstractReconciler.java"), JavaFileObjects.forResource("compile-fixtures/MultilevelReconciler.java")); CompilationSubject.assertThat(compilation).succeeded(); + assertMapping(compilation, "io.MultilevelReconciler,io.MultilevelReconciler.MyCustomResource"); + } + + /** + * When the reconciled resource is itself generic, the resolved type is a parameterized {@code + * DeclaredType}. Only its erasure may be written to the mapping resource: {@link + * ClassMappingProvider} loads the recorded name with {@code ClassUtils.getClass(String)}, which + * cannot parse type arguments. + */ + @Test + public void writesErasureOfGenericResourceType() { + Compilation compilation = + Compiler.javac() + .withProcessors(new ControllerConfigurationAnnotationProcessor()) + .compile( + JavaFileObjects.forResource("compile-fixtures/GenericResourceReconciler.java")); + CompilationSubject.assertThat(compilation).succeeded(); + assertMapping( + compilation, + "io.GenericResourceReconciler,io.GenericResourceReconciler.MyGenericCustomResource"); + assertLoadableMapping(compilation); + } + + /** + * Checks that the generated mapping resource contains the expected {@code + * reconciler,resource-class} line, using the same fully qualified, dot separated names that + * {@link ClassMappingProvider} expects to be able to load at runtime. + */ + private static void assertMapping(Compilation compilation, String expectedMapping) { + CompilationSubject.assertThat(compilation) + .generatedFile(StandardLocation.CLASS_OUTPUT, RECONCILERS_RESOURCE_PATH) + .contentsAsUtf8String() + .contains(expectedMapping); + } + + /** + * Checks that every recorded name in the generated mapping resource is a plain binary-ish class + * name, i.e. one that {@code ClassUtils.getClass(String)} can actually resolve, rather than a + * generic type signature such as {@code io.Foo}. + */ + private static void assertLoadableMapping(Compilation compilation) { + final var contents = + compilation + .generatedFile(StandardLocation.CLASS_OUTPUT, RECONCILERS_RESOURCE_PATH) + .map( + file -> { + try { + return file.getCharContent(true).toString(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }) + .orElseThrow(() -> new AssertionError("no mapping resource was generated")); + contents + .lines() + .filter(line -> !line.isBlank()) + .forEach( + line -> { + final var names = line.split(","); + assertThat(names).as("mapping line '%s'", line).hasSize(2); + for (String name : names) { + assertThat(name) + .as("recorded class name '%s' must be loadable at runtime", name) + .doesNotContain("<") + .doesNotContain(">") + .doesNotContain(" ") + .matches("[\\p{L}_$][\\p{L}\\p{N}_$]*(\\.[\\p{L}_$][\\p{L}\\p{N}_$]*)*"); + } + }); } } diff --git a/operator-framework/src/test/resources/compile-fixtures/GenericResourceReconciler.java b/operator-framework/src/test/resources/compile-fixtures/GenericResourceReconciler.java new file mode 100644 index 0000000000..d17f97b1f9 --- /dev/null +++ b/operator-framework/src/test/resources/compile-fixtures/GenericResourceReconciler.java @@ -0,0 +1,42 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io; + +import io.fabric8.kubernetes.client.CustomResource; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; + +/** + * The reconciled resource is itself a generic type, so the resolved resource type is a + * parameterized {@code DeclaredType}. Only its erasure can be written to the mapping resource, + * because that is the only form {@code ClassMappingProvider} is able to load at runtime. + */ +@ControllerConfiguration +public class GenericResourceReconciler implements + Reconciler> { + + public static class MyGenericCustomResource extends CustomResource { + } + + @Override + public UpdateControl> reconcile( + MyGenericCustomResource customResource, + Context> context) { + return UpdateControl.noUpdate(); + } +} diff --git a/pom.xml b/pom.xml index 78d2c7a35b..c144225241 100644 --- a/pom.xml +++ b/pom.xml @@ -77,7 +77,6 @@ 5.23.0 3.20.0 0.23.0 - 1.13.0 3.27.7 4.3.0 2.7.3 @@ -148,11 +147,6 @@ micrometer-core ${micrometer-core.version} - - com.squareup - javapoet - ${javapoet.version} - org.awaitility awaitility