diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/AnnotationsTransformer.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/AnnotationsTransformer.java index c24a44fb15ca96..341412e49fe503 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/AnnotationsTransformer.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/AnnotationsTransformer.java @@ -1,9 +1,18 @@ package io.quarkus.arc.processor; +import java.lang.annotation.Annotation; +import java.util.Arrays; import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Collectors; + import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationTarget; import org.jboss.jandex.AnnotationTarget.Kind; +import org.jboss.jandex.DotName; /** * Allows a build-time extension to override the annotations that exist on bean classes. @@ -11,7 +20,7 @@ * The container should use {@link AnnotationStore} to obtain annotations of any {@link org.jboss.jandex.ClassInfo}, * {@link org.jboss.jandex.FieldInfo} and {@link org.jboss.jandex.MethodInfo}. * - * @author Martin Kouba + * @see Builder */ public interface AnnotationsTransformer extends BuildExtension { @@ -31,6 +40,17 @@ default boolean appliesTo(Kind kind) { */ void transform(TransformationContext transformationContext); + /** + * + * @return a new builder instance + */ + static Builder builder() { + return new Builder(); + } + + /** + * A transformation context. + */ interface TransformationContext extends BuildContext { AnnotationTarget getTarget(); @@ -64,4 +84,211 @@ default boolean isMethod() { } + /** + * A convenient builder. + */ + static final class Builder { + + private int priority; + private Predicate appliesTo; + private Consumer transform; + private Predicate predicate; + + public Builder() { + this.priority = DEFAULT_PRIORITY; + } + + /** + * + * @param appliesToKind + * @return self + * @see AnnotationsTransformer#appliesTo(Kind) + */ + public Builder appliesTo(Kind appliesToKind) { + return appliesTo(kind -> kind == appliesToKind); + } + + /** + * + * @param appliesTo + * @return self + * @see AnnotationsTransformer#appliesTo(Kind) + */ + public Builder appliesTo(Predicate appliesTo) { + this.appliesTo = appliesTo; + return this; + } + + /** + * + * @param priority + * @return self + */ + public Builder priority(int priority) { + this.priority = priority; + return this; + } + + /** + * {@link TransformationContext#getAnnotations()} must contain ALL of the given annotations. + * + * @param annotationNames + * @return self + */ + public Builder containsAll(List annotationNames) { + return and(context -> { + for (DotName annotationName : annotationNames) { + if (!Annotations.contains(context.getAnnotations(), annotationName)) { + return false; + } + } + return true; + }); + } + + /** + * {@link TransformationContext#getAnnotations()} must contain ALL of the given annotations. + * + * @param annotationNames + * @return self + */ + public Builder containsAll(DotName... annotationNames) { + return containsAll(List.of(annotationNames)); + } + + /** + * {@link TransformationContext#getAnnotations()} must contain ALL of the given annotations. + * + * @param annotationNames + * @return self + */ + @SafeVarargs + public final Builder containsAll(Class... annotationNames) { + return containsAll( + Arrays.stream(annotationNames).map(a -> DotName.createSimple(a.getName())).collect(Collectors.toList())); + } + + /** + * {@link TransformationContext#getAnnotations()} must contain ANY of the given annotations. + * + * @param annotationNames + * @return self + */ + public Builder containsAny(List annotationNames) { + return and(context -> Annotations.containsAny(context.getAnnotations(), annotationNames)); + } + + /** + * {@link TransformationContext#getAnnotations()} must contain ANY of the given annotations. + * + * @param annotationNames + * @return self + */ + public Builder containsAny(DotName... annotationNames) { + return containsAny(List.of(annotationNames)); + } + + /** + * {@link TransformationContext#getAnnotations()} must contain ANY of the given annotations. + * + * @param annotationNames + * @return self + */ + @SafeVarargs + public final Builder containsAny(Class... annotationNames) { + return containsAny( + Arrays.stream(annotationNames).map(a -> DotName.createSimple(a.getName())).collect(Collectors.toList())); + } + + /** + * {@link TransformationContext#getAnnotations()} must NOT contain any of the given annotations. + * + * @param annotationNames + * @return self + */ + public Builder containsNone(List annotationNames) { + return and(context -> !Annotations.containsAny(context.getAnnotations(), annotationNames)); + } + + /** + * {@link TransformationContext#getAnnotations()} must NOT contain any of the given annotations. + * + * @param annotationNames + * @return self + */ + public Builder containsNone(DotName... annotationNames) { + return containsNone(List.of(annotationNames)); + } + + /** + * {@link TransformationContext#getAnnotations()} must NOT contain any of the given annotations. + * + * @param annotationNames + * @return self + */ + @SafeVarargs + public final Builder containsNone(Class... annotationNames) { + return containsNone( + Arrays.stream(annotationNames).map(a -> DotName.createSimple(a.getName())).collect(Collectors.toList())); + } + + /** + * + * @param transform + * @return self + */ + public Builder transform(Consumer transform) { + this.transform = transform; + return this; + } + + /** + * + * @return a new annotation transformer + */ + public AnnotationsTransformer build() { + Predicate appliesTo = this.appliesTo; + int priority = this.priority; + Consumer transform = Objects.requireNonNull(this.transform); + Predicate predicate = this.predicate; + return new AnnotationsTransformer() { + + @Override + public int getPriority() { + return priority; + } + + @Override + public boolean appliesTo(Kind kind) { + return appliesTo != null ? appliesTo.test(kind) : true; + } + + @Override + public void transform(TransformationContext context) { + if (predicate == null) { + transform.accept(context); + } else if (predicate.test(context)) { + transform.accept(context); + } + } + + }; + } + + /** + * + * @param other + * @return self + */ + public Builder and(Predicate other) { + if (predicate == null) { + predicate = other; + } else { + predicate = predicate.and(other); + } + return this; + } + + } + } diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/annotations/AnnotationsTransformerBuilderTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/annotations/AnnotationsTransformerBuilderTest.java new file mode 100644 index 00000000000000..414672cc3c6e37 --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/annotations/AnnotationsTransformerBuilderTest.java @@ -0,0 +1,126 @@ +package io.quarkus.arc.test.buildextension.annotations; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.AbstractList; + +import javax.enterprise.context.Dependent; +import javax.enterprise.inject.Produces; +import javax.enterprise.inject.Vetoed; +import javax.inject.Inject; + +import org.jboss.jandex.AnnotationTarget.Kind; +import org.jboss.jandex.PrimitiveType; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Arc; +import io.quarkus.arc.ArcContainer; +import io.quarkus.arc.InstanceHandle; +import io.quarkus.arc.processor.AnnotationsTransformer; +import io.quarkus.arc.test.ArcTestContainer; + +public class AnnotationsTransformerBuilderTest { + + @RegisterExtension + public ArcTestContainer container = ArcTestContainer.builder() + .beanClasses(Seven.class, One.class, IWantToBeABean.class) + .annotationsTransformers( + AnnotationsTransformer.builder() + .appliesTo(Kind.CLASS) + .containsAny(Dependent.class) + .transform(context -> { + if (context.getTarget().asClass().name().toString().equals(One.class.getName())) { + // Veto bean class One + context.transform().add(Vetoed.class).done(); + } + }).build(), + AnnotationsTransformer.builder() + .appliesTo(Kind.CLASS) + .transform(context -> { + if (context.getTarget().asClass().name().local().equals(IWantToBeABean.class.getSimpleName())) { + context.transform().add(Dependent.class).done(); + } + }).build(), + AnnotationsTransformer.builder() + .appliesTo(Kind.CLASS) + .transform(context -> { + if (context.getTarget().asField().name().equals("seven")) { + context.transform().add(Inject.class).done(); + } + }).build(), + // Add @Produces to a method that returns int and is not annoated with @Simple + AnnotationsTransformer.builder() + .appliesTo(Kind.CLASS) + .containsNone(Simple.class) + .and(context -> context.getTarget().asMethod().returnType().name().equals(PrimitiveType.INT.name())) + .transform(context -> context.transform().add(Produces.class)).build()) + .build(); + + @Test + public void testVetoed() { + ArcContainer arc = Arc.container(); + assertTrue(arc.instance(Seven.class).isAvailable()); + // One is vetoed + assertFalse(arc.instance(One.class).isAvailable()); + assertEquals(Integer.valueOf(7), Integer.valueOf(arc.instance(Seven.class).get().size())); + + // Scope annotation and @Inject are added by transformer + InstanceHandle iwant = arc.instance(IWantToBeABean.class); + assertTrue(iwant.isAvailable()); + assertEquals(Integer.valueOf(7), Integer.valueOf(iwant.get().size())); + } + + // => add @Dependent here + static class IWantToBeABean { + + // => add @Inject here + Seven seven; + + // => add @Produces here + public int size() { + return seven.size(); + } + + // => do not add @Produces here + @Simple + public int anotherSize() { + return seven.size(); + } + + } + + @Dependent + static class Seven extends AbstractList { + + @Override + public Integer get(int index) { + return Integer.valueOf(7); + } + + @Override + public int size() { + return 7; + } + + } + + // => add @Vetoed here + @Dependent + static class One extends AbstractList { + + @Override + public Integer get(int index) { + return Integer.valueOf(1); + } + + @Override + public int size() { + return 1; + } + + } + +}