diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/AutoAddScopeBuildItem.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/AutoAddScopeBuildItem.java index d0c586447bc56..28f278fbbebfd 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/AutoAddScopeBuildItem.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/AutoAddScopeBuildItem.java @@ -1,6 +1,7 @@ package io.quarkus.arc.deployment; import java.util.Collection; +import java.util.function.BiConsumer; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.ClassInfo; @@ -14,7 +15,7 @@ /** * This build item can be used to turn a class that is not annotated with a CDI scope annotation into a bean, i.e. the default - * scope annotation is added automatically if conditions are met. + * scope annotation is added automatically if all conditions are met. */ public final class AutoAddScopeBuildItem extends MultiBuildItem { @@ -27,14 +28,19 @@ public static Builder builder() { private final DotName defaultScope; private final boolean unremovable; private final String reason; + private final int priority; + private final BiConsumer scopeAlreadyAdded; private AutoAddScopeBuildItem(MatchPredicate matchPredicate, boolean containerServicesRequired, - DotName defaultScope, boolean unremovable, String reason) { + DotName defaultScope, boolean unremovable, String reason, int priority, + BiConsumer scopeAlreadyAdded) { this.matchPredicate = matchPredicate; this.containerServicesRequired = containerServicesRequired; this.defaultScope = defaultScope; this.unremovable = unremovable; this.reason = reason; + this.priority = priority; + this.scopeAlreadyAdded = scopeAlreadyAdded; } public boolean isContainerServicesRequired() { @@ -50,7 +56,15 @@ public boolean isUnremovable() { } public String getReason() { - return reason != null ? ": " + reason : ""; + return reason != null ? reason : "unknown"; + } + + public int getPriority() { + return priority; + } + + public BiConsumer getScopeAlreadyAdded() { + return scopeAlreadyAdded; } public boolean test(ClassInfo clazz, Collection annotations, IndexView index) { @@ -86,11 +100,14 @@ public static class Builder { private DotName defaultScope; private boolean unremovable; private String reason; + private int priority; + private BiConsumer scopeAlreadyAdded; private Builder() { this.defaultScope = BuiltinScope.DEPENDENT.getName(); this.unremovable = false; this.requiresContainerServices = false; + this.priority = 0; } /** @@ -222,6 +239,32 @@ public Builder reason(String reason) { return this; } + /** + * Set the priority. The default priority is {@code 0}. An {@link AutoAddScopeBuildItem} with higher priority takes + * precedence. + * + * @param priority + * @return self + */ + public Builder priority(int priority) { + this.priority = priority; + return this; + } + + /** + * If a scope was already added by another {@link AutoAddScopeBuildItem} then this consumer is used to handle this + * situation, i.e. log a warning or throw an exception. The first argument is the + * {@link AutoAddScopeBuildItem#getDefaultScope()} and the second argument is the + * {@link AutoAddScopeBuildItem#getReason()}. + * + * @param consumer + * @return self + */ + public Builder scopeAlreadyAdded(BiConsumer consumer) { + this.scopeAlreadyAdded = consumer; + return this; + } + private Builder and(MatchPredicate other) { if (matchPredicate == null) { matchPredicate = other; @@ -235,7 +278,8 @@ public AutoAddScopeBuildItem build() { if (matchPredicate == null) { throw new IllegalStateException("A matching predicate must be set!"); } - return new AutoAddScopeBuildItem(matchPredicate, requiresContainerServices, defaultScope, unremovable, reason); + return new AutoAddScopeBuildItem(matchPredicate, requiresContainerServices, defaultScope, unremovable, reason, + priority, scopeAlreadyAdded); } } diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/AutoAddScopeProcessor.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/AutoAddScopeProcessor.java index eb6eaf51c2538..8d2a371ff8208 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/AutoAddScopeProcessor.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/AutoAddScopeProcessor.java @@ -1,8 +1,10 @@ package io.quarkus.arc.deployment; +import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.function.BiConsumer; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -31,6 +33,10 @@ void annotationTransformer(List autoScopes, CustomScopeAn if (autoScopes.isEmpty()) { return; } + + List sortedAutoScopes = autoScopes.stream() + .sorted(Comparator.comparingInt(AutoAddScopeBuildItem::getPriority).reversed()).collect(Collectors.toList()); + Set containerAnnotationNames = autoInjectAnnotations.stream().flatMap(a -> a.getAnnotationNames().stream()) .collect(Collectors.toSet()); containerAnnotationNames.add(DotNames.POST_CONSTRUCT); @@ -46,6 +52,12 @@ public boolean appliesTo(Kind kind) { return kind == Kind.CLASS; } + @Override + public int getPriority() { + // Make sure this annotation transformer runs before all transformers with the default priority + return DEFAULT_PRIORITY + 1000; + } + @Override public void transform(TransformationContext context) { if (scopes.isScopeIn(context.getAnnotations())) { @@ -53,11 +65,14 @@ public void transform(TransformationContext context) { return; } ClassInfo clazz = context.getTarget().asClass(); + DotName scope = null; Boolean requiresContainerServices = null; + String reason = null; - for (AutoAddScopeBuildItem autoScope : autoScopes) { + for (AutoAddScopeBuildItem autoScope : sortedAutoScopes) { if (autoScope.isContainerServicesRequired()) { if (requiresContainerServices == null) { + // Analyze the class hierarchy lazily requiresContainerServices = requiresContainerServices(clazz, containerAnnotationNames, beanArchiveIndex.getIndex()); } @@ -67,13 +82,22 @@ public void transform(TransformationContext context) { } } if (autoScope.test(clazz, context.getAnnotations(), beanArchiveIndex.getIndex())) { - context.transform().add(autoScope.getDefaultScope()).done(); + if (scope != null) { + BiConsumer consumer = autoScope.getScopeAlreadyAdded(); + if (consumer != null) { + consumer.accept(scope, reason); + } else { + LOGGER.debugf("Scope %s was already added for reason: %s", scope, reason); + } + continue; + } + scope = autoScope.getDefaultScope(); + reason = autoScope.getReason(); + context.transform().add(scope).done(); if (autoScope.isUnremovable()) { unremovables.add(clazz.name()); } - LOGGER.debugf("Automatically added scope %s to class %s" + autoScope.getReason(), - autoScope.getDefaultScope(), clazz, autoScope.getReason()); - break; + LOGGER.debugf("Automatically added scope %s to class %s: %s", scope, clazz, autoScope.getReason()); } } } diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/CustomScopeAnnotationsBuildItem.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/CustomScopeAnnotationsBuildItem.java index cbfa7f4b3ffa9..586ed5e429e13 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/CustomScopeAnnotationsBuildItem.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/CustomScopeAnnotationsBuildItem.java @@ -1,6 +1,7 @@ package io.quarkus.arc.deployment; import java.util.Collection; +import java.util.Optional; import java.util.Set; import org.jboss.jandex.AnnotationInstance; @@ -84,4 +85,21 @@ public boolean isScopeDeclaredOn(ClassInfo clazz) { public boolean isScopeIn(Collection annotations) { return !annotations.isEmpty() && (BuiltinScope.isIn(annotations) || isCustomScopeIn(annotations)); } + + /** + * + * @param annotations + * @return the scope or empty optional + */ + public Optional getScope(Collection annotations) { + if (annotations.isEmpty()) { + return Optional.empty(); + } + for (AnnotationInstance annotationInstance : annotations) { + if (BuiltinScope.from(annotationInstance.name()) != null || customScopeNames.contains(annotationInstance.name())) { + return Optional.of(annotationInstance); + } + } + return Optional.empty(); + } } diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/autoscope/AutoScopeBuildItemTest.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/autoscope/AutoScopeBuildItemTest.java new file mode 100644 index 0000000000000..9e53e084f0227 --- /dev/null +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/autoscope/AutoScopeBuildItemTest.java @@ -0,0 +1,80 @@ +package io.quarkus.arc.test.autoscope; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.UUID; + +import javax.annotation.PostConstruct; +import javax.enterprise.inject.Instance; +import javax.inject.Inject; + +import org.jboss.logging.Logger; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.deployment.AutoAddScopeBuildItem; +import io.quarkus.arc.processor.BuiltinScope; +import io.quarkus.builder.BuildContext; +import io.quarkus.builder.BuildStep; +import io.quarkus.test.QuarkusUnitTest; + +public class AutoScopeBuildItemTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(root -> root + .addClasses(SimpleBean.class)) + .addBuildChainCustomizer(b -> { + b.addBuildStep(new BuildStep() { + @Override + public void execute(BuildContext context) { + context.produce(AutoAddScopeBuildItem.builder().match((clazz, annotations, index) -> { + return clazz.name().toString().equals(SimpleBean.class.getName()); + }).defaultScope(BuiltinScope.DEPENDENT) + .scopeAlreadyAdded((scope, reason) -> { + // We cant's pass the state directly to AutoScopeBuildItemTest because it's loaded by a different classloader + Logger.getLogger("AutoScopeBuildItemTest").info(scope + ":" + reason); + }).build()); + } + }).produces(AutoAddScopeBuildItem.class).build(); + b.addBuildStep(new BuildStep() { + @Override + public void execute(BuildContext context) { + context.produce(AutoAddScopeBuildItem.builder().match((clazz, annotations, index) -> { + return clazz.name().toString().equals(SimpleBean.class.getName()); + }).defaultScope(BuiltinScope.SINGLETON).priority(10).reason("Foo!").build()); + } + }).produces(AutoAddScopeBuildItem.class).build(); + }).setLogRecordPredicate(log -> "AutoScopeBuildItemTest".equals(log.getLoggerName())) + .assertLogRecords(records -> { + assertEquals(1, records.size()); + assertEquals("javax.inject.Singleton:Foo!", records.get(0).getMessage()); + }); + + @Inject + Instance instance; + + @Test + public void testBean() { + assertTrue(instance.isResolvable()); + // The scope should be @Singleton + assertEquals(instance.get().ping(), instance.get().ping()); + } + + static class SimpleBean { + + private String id; + + public String ping() { + return id; + } + + @PostConstruct + void init() { + id = UUID.randomUUID().toString(); + } + + } + +}