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 c24a44fb15ca9..dd6e55eb50973 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,17 @@ 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 +19,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 +39,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 +83,202 @@ default boolean isMethod() { } + /** + * A convenient builder. + */ + static final class Builder { + + private int priority; + private Predicate appliesTo; + private Predicate predicate; + + private 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 whenContainsAll(List annotationNames) { + return when(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 whenContainsAll(DotName... annotationNames) { + return whenContainsAll(List.of(annotationNames)); + } + + /** + * {@link TransformationContext#getAnnotations()} must contain ALL of the given annotations. + * + * @param annotationNames + * @return self + */ + @SafeVarargs + public final Builder whenContainsAll(Class... annotationNames) { + return whenContainsAll( + 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 whenContainsAny(List annotationNames) { + return when(context -> Annotations.containsAny(context.getAnnotations(), annotationNames)); + } + + /** + * {@link TransformationContext#getAnnotations()} must contain ANY of the given annotations. + * + * @param annotationNames + * @return self + */ + public Builder whenContainsAny(DotName... annotationNames) { + return whenContainsAny(List.of(annotationNames)); + } + + /** + * {@link TransformationContext#getAnnotations()} must contain ANY of the given annotations. + * + * @param annotationNames + * @return self + */ + @SafeVarargs + public final Builder whenContainsAny(Class... annotationNames) { + return whenContainsAny( + 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 whenContainsNone(List annotationNames) { + return when(context -> !Annotations.containsAny(context.getAnnotations(), annotationNames)); + } + + /** + * {@link TransformationContext#getAnnotations()} must NOT contain any of the given annotations. + * + * @param annotationNames + * @return self + */ + public Builder whenContainsNone(DotName... annotationNames) { + return whenContainsNone(List.of(annotationNames)); + } + + /** + * {@link TransformationContext#getAnnotations()} must NOT contain any of the given annotations. + * + * @param annotationNames + * @return self + */ + @SafeVarargs + public final Builder whenContainsNone(Class... annotationNames) { + return whenContainsNone( + Arrays.stream(annotationNames).map(a -> DotName.createSimple(a.getName())).collect(Collectors.toList())); + } + + /** + * The transformation logic is only performed if the given predicate is evaluated to true. Multiple predicates are + * logically-ANDed. + * + * @param predicate + * @return self + */ + public Builder when(Predicate when) { + if (predicate == null) { + predicate = when; + } else { + predicate = predicate.and(when); + } + return this; + } + + /** + * The given transformation logic is only performed if all conditions added via {@link #when(Predicate)} are met. + * + * @param consumer + * @return a new annotation transformer + */ + public AnnotationsTransformer transform(Consumer consumer) { + Predicate appliesTo = this.appliesTo; + int priority = this.priority; + Consumer transform = Objects.requireNonNull(consumer); + 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 || predicate.test(context)) { + transform.accept(context); + } + } + + }; + } + + } + } 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 0000000000000..81ab4229becbf --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/annotations/AnnotationsTransformerBuilderTest.java @@ -0,0 +1,127 @@ +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 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; +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; + +public class AnnotationsTransformerBuilderTest { + + @RegisterExtension + public ArcTestContainer container = ArcTestContainer.builder() + .beanClasses(Seven.class, One.class, IWantToBeABean.class) + .annotationsTransformers( + AnnotationsTransformer.builder() + .appliesTo(Kind.CLASS) + .whenContainsAny(Dependent.class) + .transform(context -> { + if (context.getTarget().asClass().name().toString().equals(One.class.getName())) { + // Veto bean class One + context.transform().add(Vetoed.class).done(); + } + }), + AnnotationsTransformer.builder() + .appliesTo(Kind.CLASS) + .when(context -> context.getTarget().asClass().name().local() + .equals(IWantToBeABean.class.getSimpleName())) + .transform(context -> context.transform().add(Dependent.class).done()), + AnnotationsTransformer.builder() + .appliesTo(Kind.FIELD) + .transform(context -> { + if (context.getTarget().asField().name().equals("seven")) { + context.transform().add(Inject.class).done(); + } + }), + // Add @Produces to a method that returns int and is not annoated with @Simple + AnnotationsTransformer.builder() + .appliesTo(Kind.METHOD) + .whenContainsNone(Simple.class) + .when(context -> context.getTarget().asMethod().returnType().name() + .equals(PrimitiveType.INT.name())) + .transform(context -> { + context.transform().add(Produces.class).done(); + })) + .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())); + + assertEquals(Integer.valueOf(7), arc.select(int.class).get()); + } + + // => 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); + } + + @Simple + @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; + } + + } + +}