From c197f41feceba4d4ac9e37386dfa554b8a70a08b Mon Sep 17 00:00:00 2001 From: Ladislav Thon Date: Fri, 24 Nov 2023 17:22:35 +0100 Subject: [PATCH] ArC: allow putting bean enablement annotations on stereotypes It would probably be best to write this algorithm in a lazy fashion (driven by the annotation transformation demands), but that would require breaking an extension API (specifically, it wouldn't be possible to produce `BuildTimeConditionBuildItem`). Hence, this commit enhances the eager algorithm for bean enablement scanning to also scan stereotypes, relying on subclass information in the Jandex index to support `@Inherited` stereotypes. --- docs/src/main/asciidoc/cdi-reference.adoc | 4 + .../BuildTimeConditionBuildItem.java | 2 +- .../deployment/BuildTimeEnabledProcessor.java | 396 ++++++++++++------ .../BuildTimeEnabledStereotypesBuildItem.java | 53 +++ .../profile/IfBuildProfileStereotypeTest.java | 195 +++++++++ .../UnlessBuildProfileStereotypeTest.java | 204 +++++++++ ...BuildPropertyRepeatableStereotypeTest.java | 202 +++++++++ .../IfBuildPropertyStereotypeTest.java | 199 +++++++++ ...BuildPropertyRepeatableStereotypeTest.java | 209 +++++++++ .../UnlessBuildPropertyStereotypeTest.java | 206 +++++++++ .../arc/properties/IfBuildProperty.java | 2 +- .../arc/properties/UnlessBuildProperty.java | 2 +- .../quarkus/arc/processor/BeanDeployment.java | 3 +- 13 files changed, 1540 insertions(+), 137 deletions(-) create mode 100644 extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BuildTimeEnabledStereotypesBuildItem.java create mode 100644 extensions/arc/deployment/src/test/java/io/quarkus/arc/test/profile/IfBuildProfileStereotypeTest.java create mode 100644 extensions/arc/deployment/src/test/java/io/quarkus/arc/test/profile/UnlessBuildProfileStereotypeTest.java create mode 100644 extensions/arc/deployment/src/test/java/io/quarkus/arc/test/properties/IfBuildPropertyRepeatableStereotypeTest.java create mode 100644 extensions/arc/deployment/src/test/java/io/quarkus/arc/test/properties/IfBuildPropertyStereotypeTest.java create mode 100644 extensions/arc/deployment/src/test/java/io/quarkus/arc/test/properties/UnlessBuildPropertyRepeatableStereotypeTest.java create mode 100644 extensions/arc/deployment/src/test/java/io/quarkus/arc/test/properties/UnlessBuildPropertyStereotypeTest.java diff --git a/docs/src/main/asciidoc/cdi-reference.adoc b/docs/src/main/asciidoc/cdi-reference.adoc index 8da9c60f50819..a41d9456f14a9 100644 --- a/docs/src/main/asciidoc/cdi-reference.adoc +++ b/docs/src/main/asciidoc/cdi-reference.adoc @@ -601,6 +601,8 @@ public class TracerConfiguration { NOTE: The runtime profile has absolutely no effect on the bean resolution using `@IfBuildProfile` and `@UnlessBuildProfile`. +TIP: It is also possible to use `@IfBuildProfile` and `@UnlessBuildProfile` on stereotypes. + [[enable_build_properties]] === Enabling Beans for Quarkus Build Properties @@ -654,6 +656,8 @@ public class TracerConfiguration { NOTE: Properties set at runtime have absolutely no effect on the bean resolution using `@IfBuildProperty`. +TIP: It is also possible to use `@IfBuildProperty` and `@UnlessBuildProperty` on stereotypes. + === Declaring Selected Alternatives In CDI, an alternative bean may be selected either globally for an application by means of `@Priority`, or for a bean archive using a `beans.xml` descriptor. diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BuildTimeConditionBuildItem.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BuildTimeConditionBuildItem.java index 1554e7b54eb8c..19c8c11543e9e 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BuildTimeConditionBuildItem.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BuildTimeConditionBuildItem.java @@ -17,7 +17,7 @@ public BuildTimeConditionBuildItem(AnnotationTarget target, boolean enabled) { this.target = target; break; default: - throw new IllegalArgumentException("'target' can only be a class, a field or a method"); + throw new IllegalArgumentException("'target' can only be a class, a field or a method: " + target); } this.enabled = enabled; } diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BuildTimeEnabledProcessor.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BuildTimeEnabledProcessor.java index d5ae68152c298..0aabb8a88f308 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BuildTimeEnabledProcessor.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BuildTimeEnabledProcessor.java @@ -5,16 +5,17 @@ import static java.util.function.Predicate.not; import static java.util.stream.Collectors.groupingBy; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; +import java.util.Deque; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.function.BiConsumer; -import java.util.function.BiFunction; +import java.util.function.Function; import java.util.stream.Collectors; import org.eclipse.microprofile.config.Config; @@ -23,14 +24,13 @@ import org.jboss.jandex.AnnotationTarget; import org.jboss.jandex.AnnotationTarget.Kind; import org.jboss.jandex.AnnotationValue; +import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; -import org.jboss.jandex.FieldInfo; +import org.jboss.jandex.EquivalenceKey; import org.jboss.jandex.IndexView; -import org.jboss.jandex.MethodInfo; import org.jboss.logging.Logger; import io.quarkus.arc.processor.AnnotationsTransformer; -import io.quarkus.arc.processor.AnnotationsTransformer.TransformationContext; import io.quarkus.arc.processor.DotNames; import io.quarkus.arc.processor.Transformation; import io.quarkus.arc.profile.IfBuildProfile; @@ -59,95 +59,212 @@ public class BuildTimeEnabledProcessor { IF_BUILD_PROPERTY, IF_BUILD_PROPERTY_CONTAINER, UNLESS_BUILD_PROPERTY, UNLESS_BUILD_PROPERTY_CONTAINER); @BuildStep - void ifBuildProfile(CombinedIndexBuildItem index, BuildProducer producer) { - List annotationInstances = getAnnotations(index.getIndex(), IF_BUILD_PROFILE); - for (AnnotationInstance instance : annotationInstances) { - boolean enabled = BuildProfile.from(instance).enabled(); - if (enabled) { - LOGGER.debug("Enabling " + instance.target() + " since the profile value matches the active profile."); - } else { - LOGGER.debug("Disabling " + instance.target() + " since the profile value does not match the active profile."); + BuildTimeEnabledStereotypesBuildItem findEnablementStereotypes(CombinedIndexBuildItem combinedIndex) { + IndexView index = combinedIndex.getIndex(); + + // find all stereotypes + Set stereotypeNames = new HashSet<>(); + for (AnnotationInstance annotation : index.getAnnotations(DotNames.STEREOTYPE)) { + if (annotation.target() != null + && annotation.target().kind() == Kind.CLASS + && annotation.target().asClass().isAnnotation()) { + stereotypeNames.add(annotation.target().asClass().name()); } - producer.produce(new BuildTimeConditionBuildItem(instance.target(), enabled)); } - } + // ideally, we would also consider all `StereotypeRegistrarBuildItem`s here, + // but there is a build step cycle involving Spring DI and RESTEasy Reactive + // that I'm not capable of breaking + + // for each stereotype, find all enablement annotations, present either directly or transitively + List buildTimeEnabledStereotypes = new ArrayList<>(); + for (DotName stereotypeToScan : stereotypeNames) { + Map> result = new HashMap<>(); + + Set alreadySeen = new HashSet<>(); // to guard against hypothetical stereotype cycle + Deque worklist = new ArrayDeque<>(); + worklist.add(stereotypeToScan); + while (!worklist.isEmpty()) { + DotName stereotype = worklist.poll(); + if (alreadySeen.contains(stereotype)) { + continue; + } + alreadySeen.add(stereotype); - @BuildStep - void unlessBuildProfile(CombinedIndexBuildItem index, BuildProducer producer) { - List annotationInstances = getAnnotations(index.getIndex(), UNLESS_BUILD_PROFILE); - for (AnnotationInstance instance : annotationInstances) { - boolean enabled = BuildProfile.from(instance).disabled(); - if (enabled) { - LOGGER.debug("Enabling " + instance.target() + " since the profile value matches the active profile."); - } else { - LOGGER.debug("Disabling " + instance.target() + " since the profile value does not match the active profile."); + ClassInfo stereotypeClass = index.getClassByName(stereotype); + if (stereotypeClass == null) { + continue; + } + + for (DotName enablementAnnotation : List.of(IF_BUILD_PROFILE, UNLESS_BUILD_PROFILE, IF_BUILD_PROPERTY, + UNLESS_BUILD_PROPERTY)) { + AnnotationInstance ann = stereotypeClass.declaredAnnotation(enablementAnnotation); + if (ann != null) { + result.computeIfAbsent(enablementAnnotation, ignored -> new ArrayList<>()).add(ann); + } + } + for (Map.Entry entry : Map.of(IF_BUILD_PROPERTY_CONTAINER, IF_BUILD_PROPERTY, + UNLESS_BUILD_PROPERTY_CONTAINER, UNLESS_BUILD_PROPERTY).entrySet()) { + DotName enablementContainerAnnotation = entry.getKey(); + DotName enablementAnnotation = entry.getValue(); + + AnnotationInstance containerAnn = stereotypeClass.declaredAnnotation(enablementContainerAnnotation); + if (containerAnn != null) { + for (AnnotationInstance ann : containerAnn.value().asNestedArray()) { + result.computeIfAbsent(enablementAnnotation, ignored -> new ArrayList<>()).add(ann); + } + } + } + + for (AnnotationInstance metaAnn : stereotypeClass.declaredAnnotations()) { + if (stereotypeNames.contains(metaAnn.name())) { + worklist.add(metaAnn.name()); + } + } + } + + if (!result.isEmpty()) { + ClassInfo stereotypeClass = index.getClassByName(stereotypeToScan); + boolean inheritable = stereotypeClass != null && stereotypeClass.hasDeclaredAnnotation(DotNames.INHERITED); + buildTimeEnabledStereotypes.add(new BuildTimeEnabledStereotypesBuildItem.BuildTimeEnabledStereotype( + stereotypeToScan, inheritable, result)); } - producer.produce(new BuildTimeConditionBuildItem(instance.target(), enabled)); } + + return new BuildTimeEnabledStereotypesBuildItem(buildTimeEnabledStereotypes); } @BuildStep - void ifBuildProperty(CombinedIndexBuildItem index, BuildProducer conditions) { - buildProperty(IF_BUILD_PROPERTY, IF_BUILD_PROPERTY_CONTAINER, new BiFunction() { - @Override - public Boolean apply(String stringValue, String expectedStringValue) { - return stringValue.equals(expectedStringValue); - } - }, index.getIndex(), new BiConsumer() { - @Override - public void accept(AnnotationTarget target, Boolean enabled) { - conditions.produce(new BuildTimeConditionBuildItem(target, enabled)); - } - }); + void ifBuildProfile(CombinedIndexBuildItem index, BuildTimeEnabledStereotypesBuildItem stereotypes, + BuildProducer producer) { + enablementAnnotations(IF_BUILD_PROFILE, null, index.getIndex(), stereotypes, producer, + new Function() { + @Override + public Boolean apply(AnnotationInstance annotation) { + return BuildProfile.from(annotation).enabled(); + } + }); } @BuildStep - void unlessBuildProperty(CombinedIndexBuildItem index, BuildProducer conditions) { - buildProperty(UNLESS_BUILD_PROPERTY, UNLESS_BUILD_PROPERTY_CONTAINER, new BiFunction() { - @Override - public Boolean apply(String stringValue, String expectedStringValue) { - return !stringValue.equals(expectedStringValue); - } - }, index.getIndex(), new BiConsumer() { - @Override - public void accept(AnnotationTarget target, Boolean enabled) { - conditions.produce(new BuildTimeConditionBuildItem(target, enabled)); - } - }); + void unlessBuildProfile(CombinedIndexBuildItem index, BuildTimeEnabledStereotypesBuildItem stereotypes, + BuildProducer producer) { + enablementAnnotations(UNLESS_BUILD_PROFILE, null, index.getIndex(), stereotypes, producer, + new Function() { + @Override + public Boolean apply(AnnotationInstance annotation) { + return BuildProfile.from(annotation).disabled(); + } + }); } - void buildProperty(DotName annotationName, DotName containingAnnotationName, BiFunction testFun, - IndexView index, BiConsumer producer) { + @BuildStep + void ifBuildProperty(CombinedIndexBuildItem index, BuildTimeEnabledStereotypesBuildItem stereotypes, + BuildProducer conditions) { Config config = ConfigProviderResolver.instance().getConfig(); + enablementAnnotations(IF_BUILD_PROPERTY, IF_BUILD_PROPERTY_CONTAINER, index.getIndex(), stereotypes, conditions, + new Function() { + @Override + public Boolean apply(AnnotationInstance annotation) { + return BuildProperty.from(annotation).enabled(config); + } + }); + } + + @BuildStep + void unlessBuildProperty(CombinedIndexBuildItem index, BuildTimeEnabledStereotypesBuildItem stereotypes, + BuildProducer conditions) { + Config config = ConfigProviderResolver.instance().getConfig(); + enablementAnnotations(UNLESS_BUILD_PROPERTY, UNLESS_BUILD_PROPERTY_CONTAINER, index.getIndex(), stereotypes, conditions, + new Function() { + @Override + public Boolean apply(AnnotationInstance annotation) { + return BuildProperty.from(annotation).disabled(config); + } + }); + } + + private void enablementAnnotations(DotName annotationName, DotName containingAnnotationName, IndexView index, + BuildTimeEnabledStereotypesBuildItem stereotypes, BuildProducer producer, + Function test) { + + // instances of enablement annotation directly on affected declarations List annotationInstances = getAnnotations(index, annotationName, containingAnnotationName); - for (AnnotationInstance instance : annotationInstances) { - String propertyName = instance.value("name").asString(); - String expectedStringValue = instance.value("stringValue").asString(); - AnnotationValue enableIfMissingValue = instance.value("enableIfMissing"); - boolean enableIfMissing = enableIfMissingValue != null && enableIfMissingValue.asBoolean(); + for (AnnotationInstance annotation : annotationInstances) { + AnnotationTarget target = annotation.target(); + boolean enabled = test.apply(annotation); + if (enabled) { + LOGGER.debugf("Enabling %s due to %s", target, annotation); + } else { + LOGGER.debugf("Disabling %s due to %s", target, annotation); + } + producer.produce(new BuildTimeConditionBuildItem(target, enabled)); + } - Optional optionalValue = config.getOptionalValue(propertyName, String.class); - boolean enabled; - if (optionalValue.isPresent()) { - if (testFun.apply(optionalValue.get(), expectedStringValue)) { - LOGGER.debugf("Enabling %s since the property value matches the expected one.", instance.target()); - enabled = true; - } else { - LOGGER.debugf("Disabling %s since the property value matches the specified value one.", instance.target()); - enabled = false; + // instances of stereotypes (with enablement annotation) directly on affected declarations + Set processedClasses = new HashSet<>(); + List classesWithPossiblyInheritedStereotype = new ArrayList<>(); + for (BuildTimeEnabledStereotypesBuildItem.BuildTimeEnabledStereotype stereotype : stereotypes.all()) { + for (AnnotationInstance stereotypeUsage : getAnnotations(index, stereotype.name)) { + AnnotationTarget target = stereotypeUsage.target(); + for (AnnotationInstance annotation : stereotype.getEnablementAnnotations(annotationName)) { + boolean enabled = test.apply(annotation); + if (enabled) { + LOGGER.debugf("Enabling %s due to %s on stereotype %s", target, annotation, stereotype.name); + } else { + LOGGER.debugf("Disabling %s due to %s on stereotype %s", target, annotation, stereotype.name); + } + producer.produce(new BuildTimeConditionBuildItem(target, enabled)); } - } else { - if (enableIfMissing) { - LOGGER.debugf("Enabling %s since the property has not been set and 'enableIfMissing' is set to 'true'.", - instance.target()); - enabled = true; - } else { - LOGGER.debugf("Disabling %s since the property has not been set and 'enableIfMissing' is set to 'false'.", - instance.target()); - enabled = false; + + // annotations are inherited only on classes (and only from superclasses) + if (target.kind() == Kind.CLASS) { + ClassInfo clazz = target.asClass(); + processedClasses.add(clazz.name()); + if (stereotype.inheritable && !clazz.isInterface()) { + classesWithPossiblyInheritedStereotype.addAll(index.getAllKnownSubclasses(clazz.name())); + } } } - producer.accept(instance.target(), enabled); + } + + // instances of stereotypes (with enablement annotation) inherited from a superclass + for (ClassInfo clazz : classesWithPossiblyInheritedStereotype) { + if (processedClasses.contains(clazz.name())) { + continue; + } + processedClasses.add(clazz.name()); + + ClassInfo superclass = index.getClassByName(clazz.superName()); + Set seenStereotypes = new HashSet<>(); // avoid "inheriting" the same annotation multiple times + while (superclass != null && !DotNames.OBJECT.equals(superclass.name())) { + for (AnnotationInstance ann : superclass.declaredAnnotations()) { + if (!stereotypes.isStereotype(ann.name()) || seenStereotypes.contains(ann.name())) { + continue; + } + + BuildTimeEnabledStereotypesBuildItem.BuildTimeEnabledStereotype stereotype = stereotypes + .getStereotype(ann.name()); + if (stereotype == null) { + continue; + } + + for (AnnotationInstance annotation : stereotype.getEnablementAnnotations(annotationName)) { + boolean enabled = test.apply(annotation); + if (enabled) { + LOGGER.debugf("Enabling %s due to %s on stereotype %s inherited from %s", + clazz, annotation, stereotype.name, superclass.name()); + } else { + LOGGER.debugf("Disabling %s due to %s on stereotype %s inherited from %s", + clazz, annotation, stereotype.name, superclass.name()); + } + producer.produce(new BuildTimeConditionBuildItem(clazz, enabled)); + } + + seenStereotypes.add(ann.name()); + } + + superclass = index.getClassByName(superclass.superName()); + } } } @@ -163,49 +280,29 @@ void conditionTransformer(List buildTimeConditions, * Done this way in order to support having different annotation specify different conditions * under which the bean is enabled and then combining all of them using a logical 'AND' */ - final Map classTargets = new HashMap<>(); //don't use ClassInfo because it doesn't implement equals and hashCode - final Map fieldTargets = new HashMap<>(); // don't use FieldInfo because it doesn't implement equals and hashCode - final Map methodTargets = new HashMap<>(); + final Map enabled = new HashMap<>(); for (BuildTimeConditionBuildItem buildTimeCondition : buildTimeConditions) { AnnotationTarget target = buildTimeCondition.getTarget(); - AnnotationTarget.Kind kind = target.kind(); - if (kind == AnnotationTarget.Kind.CLASS) { - DotName classDotName = target.asClass().name(); - Boolean allPreviousConditionsTrue = classTargets.getOrDefault(classDotName, true); - classTargets.put(classDotName, allPreviousConditionsTrue && buildTimeCondition.isEnabled()); - } else if (kind == AnnotationTarget.Kind.METHOD) { - MethodInfo method = target.asMethod(); - Boolean allPreviousConditionsTrue = methodTargets.getOrDefault(method, true); - methodTargets.put(method, allPreviousConditionsTrue && buildTimeCondition.isEnabled()); - } else if (kind == AnnotationTarget.Kind.FIELD) { - String uniqueFieldName = toUniqueString(target.asField()); - Boolean allPreviousConditionsTrue = fieldTargets.getOrDefault(uniqueFieldName, true); - fieldTargets.put(uniqueFieldName, allPreviousConditionsTrue && buildTimeCondition.isEnabled()); - } + EquivalenceKey key = EquivalenceKey.of(target); + Boolean allPreviousConditionsTrue = enabled.getOrDefault(key, true); + enabled.put(key, allPreviousConditionsTrue && buildTimeCondition.isEnabled()); } // the transformer just tries to match targets and then enables or disables the bean accordingly annotationsTransformer.produce(new AnnotationsTransformerBuildItem(new AnnotationsTransformer() { - @Override public void transform(TransformationContext ctx) { AnnotationTarget target = ctx.getTarget(); - if (ctx.isClass()) { - DotName classDotName = target.asClass().name(); - if (classTargets.containsKey(classDotName)) { - transformBean(target, ctx, classTargets.get(classDotName)); - } - } else if (ctx.isMethod()) { - MethodInfo method = target.asMethod(); - if (methodTargets.containsKey(method)) { - transformBean(target, ctx, methodTargets.get(method)); - } - } else if (ctx.isField()) { - FieldInfo field = target.asField(); - String uniqueFieldName = toUniqueString(field); - if (fieldTargets.containsKey(uniqueFieldName)) { - transformBean(target, ctx, fieldTargets.get(uniqueFieldName)); + if (!enabled.getOrDefault(EquivalenceKey.of(target), Boolean.TRUE)) { + Transformation transform = ctx.transform(); + if (target.kind() == Kind.CLASS) { + // Veto the class + transform.add(DotNames.VETOED); + } else { + // Veto the producer + transform.add(DotNames.VETOED_PRODUCER); } + transform.done(); } } })); @@ -230,42 +327,35 @@ BuildExclusionsBuildItem buildExclusions(List build map.getOrDefault(AnnotationTarget.Kind.FIELD, Collections.emptySet())); } - private String toUniqueString(FieldInfo field) { - return field.declaringClass().name().toString() + "." + field.name(); - } - - private void transformBean(AnnotationTarget target, TransformationContext ctx, boolean enabled) { - if (!enabled) { - Transformation transform = ctx.transform(); - if (target.kind() == Kind.CLASS) { - // Veto the class - transform.add(DotNames.VETOED); - } else { - // Veto the producer - transform.add(DotNames.VETOED_PRODUCER); + private static List getAnnotations(IndexView index, DotName annotationName) { + List result = new ArrayList<>(); + for (AnnotationInstance annotation : index.getAnnotations(annotationName)) { + AnnotationTarget target = annotation.target(); + if (target != null && (target.kind() != Kind.CLASS || !target.asClass().isAnnotation())) { + result.add(annotation); } - transform.done(); } + return result; } - private static List getAnnotations(IndexView index, DotName annotationName) { - return new ArrayList<>(index.getAnnotations(annotationName)); - } - - private static List getAnnotations( - IndexView index, - DotName annotationName, + private static List getAnnotations(IndexView index, DotName annotationName, DotName containingAnnotationName) { // Single annotation List annotationInstances = getAnnotations(index, annotationName); + if (containingAnnotationName == null) { + return annotationInstances; + } // Collect containing annotation instances // Note that we can't just use the IndexView.getAnnotationsWithRepeatable() method because the containing annotation is not part of the index for (AnnotationInstance containingInstance : index.getAnnotations(containingAnnotationName)) { - for (AnnotationInstance nestedInstance : containingInstance.value().asNestedArray()) { - // We need to set the target of the containing instance - annotationInstances.add( - AnnotationInstance.create(nestedInstance.name(), containingInstance.target(), nestedInstance.values())); + AnnotationTarget target = containingInstance.target(); + if (target != null && (target.kind() != Kind.CLASS || !target.asClass().isAnnotation())) { + for (AnnotationInstance nestedInstance : containingInstance.value().asNestedArray()) { + // We need to set the target of the containing instance + annotationInstances.add( + AnnotationInstance.create(nestedInstance.name(), target, nestedInstance.values())); + } } } @@ -333,4 +423,44 @@ private static BuildProfile from(AnnotationInstance instance) { return new BuildProfile(allOf, anyOf); } } + + static class BuildProperty { + private final String propertyName; + private final String expectedStringValue; + private final boolean enableIfMissing; + + private BuildProperty(String propertyName, String expectedStringValue, boolean enableIfMissing) { + this.propertyName = propertyName; + this.expectedStringValue = expectedStringValue; + this.enableIfMissing = enableIfMissing; + } + + boolean enabled(Config config) { + Optional optionalValue = config.getOptionalValue(propertyName, String.class); + if (optionalValue.isPresent()) { + return expectedStringValue.equalsIgnoreCase(optionalValue.get()); + } else { + return enableIfMissing; + } + } + + boolean disabled(Config config) { + // cannot just negate `enabled()`, that would change the meaning of `enableIfMissing` + Optional optionalValue = config.getOptionalValue(propertyName, String.class); + if (optionalValue.isPresent()) { + return !expectedStringValue.equalsIgnoreCase(optionalValue.get()); + } else { + return enableIfMissing; + } + } + + static BuildProperty from(AnnotationInstance instance) { + String propertyName = instance.value("name").asString(); + String expectedStringValue = instance.value("stringValue").asString(); + AnnotationValue enableIfMissingValue = instance.value("enableIfMissing"); + boolean enableIfMissing = enableIfMissingValue != null && enableIfMissingValue.asBoolean(); + + return new BuildProperty(propertyName, expectedStringValue, enableIfMissing); + } + } } diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BuildTimeEnabledStereotypesBuildItem.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BuildTimeEnabledStereotypesBuildItem.java new file mode 100644 index 0000000000000..fc6b165faac28 --- /dev/null +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BuildTimeEnabledStereotypesBuildItem.java @@ -0,0 +1,53 @@ +package io.quarkus.arc.deployment; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.DotName; + +import io.quarkus.builder.item.SimpleBuildItem; + +final class BuildTimeEnabledStereotypesBuildItem extends SimpleBuildItem { + private final Map map; + + BuildTimeEnabledStereotypesBuildItem(List buildTimeEnabledStereotypes) { + Map map = new HashMap<>(); + for (BuildTimeEnabledStereotype buildTimeEnabledStereotype : buildTimeEnabledStereotypes) { + map.put(buildTimeEnabledStereotype.name, buildTimeEnabledStereotype); + } + this.map = map; + } + + boolean isStereotype(DotName name) { + return map.containsKey(name); + } + + BuildTimeEnabledStereotype getStereotype(DotName stereotypeName) { + return map.get(stereotypeName); + } + + Collection all() { + return map.values(); + } + + static final class BuildTimeEnabledStereotype { + final DotName name; + final boolean inheritable; // meta-annotated `@Inherited` + + // enablement annotations present directly _or transitively_ on this stereotype + final Map> annotations; + + BuildTimeEnabledStereotype(DotName name, boolean inheritable, Map> annotations) { + this.name = name; + this.inheritable = inheritable; + this.annotations = annotations; + } + + List getEnablementAnnotations(DotName enablementAnnotationName) { + return annotations.getOrDefault(enablementAnnotationName, List.of()); + } + } +} diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/profile/IfBuildProfileStereotypeTest.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/profile/IfBuildProfileStereotypeTest.java new file mode 100644 index 0000000000000..2a6e79e72a7a1 --- /dev/null +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/profile/IfBuildProfileStereotypeTest.java @@ -0,0 +1,195 @@ +package io.quarkus.arc.test.profile; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Set; +import java.util.stream.Collectors; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Any; +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.Produces; +import jakarta.enterprise.inject.Stereotype; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.profile.IfBuildProfile; +import io.quarkus.test.QuarkusUnitTest; + +public class IfBuildProfileStereotypeTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(DevOnly.class, InheritableDevOnly.class, TransitiveDevOnly.class, + InheritableTransitiveDevOnly.class, MyService.class, DevOnlyMyService.class, + InheritableDevOnlyMyService.class, TransitiveDevOnlyMyService.class, + InheritableTransitiveDevOnlyMyService.class, MyServiceSimple.class, + MyServiceDevOnlyDirect.class, MyServiceDevOnlyTransitive.class, + MyServiceDevOnlyOnSuperclassNotInheritable.class, + MyServiceDevOnlyOnSuperclassInheritable.class, + MyServiceDevOnlyTransitiveOnSuperclassNotInheritable.class, + MyServiceDevOnlyTransitiveOnSuperclassInheritable.class, Producers.class)); + + @Inject + @Any + Instance services; + + @Test + public void test() { + Set hello = services.stream().map(MyService::hello).collect(Collectors.toSet()); + Set expected = Set.of( + MyServiceSimple.class.getSimpleName(), + MyServiceDevOnlyOnSuperclassNotInheritable.class.getSimpleName(), + MyServiceDevOnlyTransitiveOnSuperclassNotInheritable.class.getSimpleName(), + Producers.SIMPLE); + assertEquals(expected, hello); + } + + @IfBuildProfile("dev") + @Stereotype + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface DevOnly { + } + + @IfBuildProfile("dev") + @Stereotype + @Inherited + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface InheritableDevOnly { + } + + @DevOnly + @Stereotype + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface TransitiveDevOnly { + } + + @DevOnly + @Stereotype + @Inherited + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface InheritableTransitiveDevOnly { + } + + interface MyService { + String hello(); + } + + @DevOnly + static abstract class DevOnlyMyService implements MyService { + } + + @InheritableDevOnly + static abstract class InheritableDevOnlyMyService implements MyService { + } + + @TransitiveDevOnly + static abstract class TransitiveDevOnlyMyService implements MyService { + } + + @InheritableTransitiveDevOnly + static abstract class InheritableTransitiveDevOnlyMyService implements MyService { + } + + @ApplicationScoped + static class MyServiceSimple implements MyService { + @Override + public String hello() { + return MyServiceSimple.class.getSimpleName(); + } + } + + @ApplicationScoped + @DevOnly + static class MyServiceDevOnlyDirect implements MyService { + @Override + public String hello() { + return MyServiceDevOnlyDirect.class.getSimpleName(); + } + } + + @ApplicationScoped + @TransitiveDevOnly + static class MyServiceDevOnlyTransitive implements MyService { + @Override + public String hello() { + return MyServiceDevOnlyTransitive.class.getSimpleName(); + } + } + + @ApplicationScoped + static class MyServiceDevOnlyOnSuperclassNotInheritable extends DevOnlyMyService { + @Override + public String hello() { + return MyServiceDevOnlyOnSuperclassNotInheritable.class.getSimpleName(); + } + } + + @ApplicationScoped + static class MyServiceDevOnlyOnSuperclassInheritable extends InheritableDevOnlyMyService { + @Override + public String hello() { + return MyServiceDevOnlyOnSuperclassInheritable.class.getSimpleName(); + } + } + + @ApplicationScoped + static class MyServiceDevOnlyTransitiveOnSuperclassNotInheritable extends TransitiveDevOnlyMyService { + @Override + public String hello() { + return MyServiceDevOnlyTransitiveOnSuperclassNotInheritable.class.getSimpleName(); + } + } + + @ApplicationScoped + static class MyServiceDevOnlyTransitiveOnSuperclassInheritable extends InheritableTransitiveDevOnlyMyService { + @Override + public String hello() { + return MyServiceDevOnlyTransitiveOnSuperclassInheritable.class.getSimpleName(); + } + } + + @ApplicationScoped + static class Producers { + static final String SIMPLE = "Producers.simple"; + static final String DEV_ONLY_DIRECT = "Producers.devOnlyDirect"; + static final String DEV_ONLY_TRANSITIVE = "Producers.devOnlyTransitive"; + + @Produces + MyService simple = new MyService() { + @Override + public String hello() { + return SIMPLE; + } + }; + + @Produces + @DevOnly + MyService devOnlyDirect = new MyService() { + @Override + public String hello() { + return DEV_ONLY_DIRECT; + } + }; + + @Produces + @TransitiveDevOnly + MyService devOnlyTransitive = new MyService() { + @Override + public String hello() { + return DEV_ONLY_TRANSITIVE; + } + }; + } +} diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/profile/UnlessBuildProfileStereotypeTest.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/profile/UnlessBuildProfileStereotypeTest.java new file mode 100644 index 0000000000000..ff9d8c982bc39 --- /dev/null +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/profile/UnlessBuildProfileStereotypeTest.java @@ -0,0 +1,204 @@ +package io.quarkus.arc.test.profile; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Set; +import java.util.stream.Collectors; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Any; +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.Produces; +import jakarta.enterprise.inject.Stereotype; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.profile.UnlessBuildProfile; +import io.quarkus.test.QuarkusUnitTest; + +public class UnlessBuildProfileStereotypeTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(TestNever.class, InheritableTestNever.class, TransitiveTestNever.class, + InheritableTransitiveTestNever.class, MyService.class, TestNeverMyService.class, + InheritableTestNeverMyService.class, TransitiveTestNeverMyService.class, + InheritableTransitiveTestNeverMyService.class, MyServiceSimple.class, + MyServiceTestNeverDirect.class, MyServiceTestNeverTransitive.class, + MyServiceTestNeverOnSuperclassNotInheritable.class, + MyServiceTestNeverOnSuperclassInheritable.class, + MyServiceTestNeverTransitiveOnSuperclassNotInheritable.class, + MyServiceTestNeverTransitiveOnSuperclassInheritable.class, Producers.class)); + + @Inject + @Any + Instance services; + + @Test + public void test() { + Set hello = services.stream().map(MyService::hello).collect(Collectors.toSet()); + Set expected = Set.of( + MyServiceSimple.class.getSimpleName(), + MyServiceTestNeverOnSuperclassNotInheritable.class.getSimpleName(), + MyServiceTestNeverTransitiveOnSuperclassNotInheritable.class.getSimpleName(), + Producers.SIMPLE); + assertEquals(expected, hello); + } + + @UnlessBuildProfile("test") + @Stereotype + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface TestNever { + } + + @UnlessBuildProfile("test") + @Stereotype + @Inherited + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface InheritableTestNever { + } + + @TestNever + @Stereotype + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface TransitiveTestNever { + } + + @TestNever + @Stereotype + @Inherited + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface InheritableTransitiveTestNever { + } + + interface MyService { + String hello(); + } + + @TestNever + static abstract class TestNeverMyService implements MyService { + } + + @InheritableTestNever + static abstract class InheritableTestNeverMyService implements MyService { + } + + @TransitiveTestNever + static abstract class TransitiveTestNeverMyService implements MyService { + } + + @InheritableTransitiveTestNever + static abstract class InheritableTransitiveTestNeverMyService implements MyService { + } + + @ApplicationScoped + static class MyServiceSimple implements MyService { + @Override + public String hello() { + return MyServiceSimple.class.getSimpleName(); + } + } + + @ApplicationScoped + @TestNever + static class MyServiceTestNeverDirect implements MyService { + @Override + public String hello() { + return MyServiceTestNeverDirect.class.getSimpleName(); + } + } + + @ApplicationScoped + @TransitiveTestNever + static class MyServiceTestNeverTransitive implements MyService { + @Override + public String hello() { + return MyServiceTestNeverTransitive.class.getSimpleName(); + } + } + + @ApplicationScoped + static class MyServiceTestNeverOnSuperclassNotInheritable extends TestNeverMyService { + @Override + public String hello() { + return MyServiceTestNeverOnSuperclassNotInheritable.class.getSimpleName(); + } + } + + @ApplicationScoped + static class MyServiceTestNeverOnSuperclassInheritable extends InheritableTestNeverMyService { + @Override + public String hello() { + return MyServiceTestNeverOnSuperclassInheritable.class.getSimpleName(); + } + } + + @ApplicationScoped + static class MyServiceTestNeverTransitiveOnSuperclassNotInheritable extends TransitiveTestNeverMyService { + @Override + public String hello() { + return MyServiceTestNeverTransitiveOnSuperclassNotInheritable.class.getSimpleName(); + } + } + + @ApplicationScoped + static class MyServiceTestNeverTransitiveOnSuperclassInheritable extends InheritableTransitiveTestNeverMyService { + @Override + public String hello() { + return MyServiceTestNeverTransitiveOnSuperclassInheritable.class.getSimpleName(); + } + } + + @ApplicationScoped + static class Producers { + static final String SIMPLE = "Producers.simple"; + static final String TEST_NEVER_DIRECT = "Producers.testNeverDirect"; + static final String TEST_NEVER_TRANSITIVE = "Producers.testNeverTransitive"; + + @Produces + @ApplicationScoped + MyService simple() { + return new MyService() { + @Override + public String hello() { + return SIMPLE; + } + }; + } + + @Produces + @ApplicationScoped + @TestNever + MyService testNeverDirect() { + return new MyService() { + @Override + public String hello() { + return TEST_NEVER_DIRECT; + } + }; + } + + @Produces + @ApplicationScoped + @TransitiveTestNever + MyService testNeverTransitive() { + return new MyService() { + @Override + public String hello() { + return TEST_NEVER_TRANSITIVE; + } + }; + } + } +} diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/properties/IfBuildPropertyRepeatableStereotypeTest.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/properties/IfBuildPropertyRepeatableStereotypeTest.java new file mode 100644 index 0000000000000..243d65fa32dad --- /dev/null +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/properties/IfBuildPropertyRepeatableStereotypeTest.java @@ -0,0 +1,202 @@ +package io.quarkus.arc.test.properties; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Set; +import java.util.stream.Collectors; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Any; +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.Produces; +import jakarta.enterprise.inject.Stereotype; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.properties.IfBuildProperty; +import io.quarkus.test.QuarkusUnitTest; + +public class IfBuildPropertyRepeatableStereotypeTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(NotMatchingProperty.class, InheritableNotMatchingProperty.class, + TransitiveNotMatchingProperty.class, + InheritableTransitiveNotMatchingProperty.class, MyService.class, NotMatchingPropertyMyService.class, + InheritableNotMatchingPropertyMyService.class, TransitiveNotMatchingPropertyMyService.class, + InheritableTransitiveNotMatchingPropertyMyService.class, MyServiceSimple.class, + MyServiceNotMatchingPropertyDirect.class, MyServiceNotMatchingPropertyTransitive.class, + MyServiceNotMatchingPropertyOnSuperclassNotInheritable.class, + MyServiceNotMatchingPropertyOnSuperclassInheritable.class, + MyServiceNotMatchingPropertyTransitiveOnSuperclassNotInheritable.class, + MyServiceNotMatchingPropertyTransitiveOnSuperclassInheritable.class, Producers.class)) + .overrideConfigKey("foo.bar", "quux") + .overrideConfigKey("some.prop", "val"); + + @Inject + @Any + Instance services; + + @Test + public void test() { + Set hello = services.stream().map(MyService::hello).collect(Collectors.toSet()); + Set expected = Set.of( + MyServiceSimple.class.getSimpleName(), + MyServiceNotMatchingPropertyOnSuperclassNotInheritable.class.getSimpleName(), + MyServiceNotMatchingPropertyTransitiveOnSuperclassNotInheritable.class.getSimpleName(), + Producers.SIMPLE); + assertEquals(expected, hello); + } + + @IfBuildProperty(name = "foo.bar", stringValue = "baz") + @IfBuildProperty(name = "some.prop", stringValue = "val") + @Stereotype + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface NotMatchingProperty { + } + + @IfBuildProperty(name = "foo.bar", stringValue = "quux") + @IfBuildProperty(name = "some.prop", stringValue = "none") + @Stereotype + @Inherited + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface InheritableNotMatchingProperty { + } + + @NotMatchingProperty + @Stereotype + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface TransitiveNotMatchingProperty { + } + + @NotMatchingProperty + @Stereotype + @Inherited + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface InheritableTransitiveNotMatchingProperty { + } + + interface MyService { + String hello(); + } + + @NotMatchingProperty + static abstract class NotMatchingPropertyMyService implements MyService { + } + + @InheritableNotMatchingProperty + static abstract class InheritableNotMatchingPropertyMyService implements MyService { + } + + @TransitiveNotMatchingProperty + static abstract class TransitiveNotMatchingPropertyMyService implements MyService { + } + + @InheritableTransitiveNotMatchingProperty + static abstract class InheritableTransitiveNotMatchingPropertyMyService implements MyService { + } + + @ApplicationScoped + static class MyServiceSimple implements MyService { + @Override + public String hello() { + return MyServiceSimple.class.getSimpleName(); + } + } + + @ApplicationScoped + @NotMatchingProperty + static class MyServiceNotMatchingPropertyDirect implements MyService { + @Override + public String hello() { + return MyServiceNotMatchingPropertyDirect.class.getSimpleName(); + } + } + + @ApplicationScoped + @TransitiveNotMatchingProperty + static class MyServiceNotMatchingPropertyTransitive implements MyService { + @Override + public String hello() { + return MyServiceNotMatchingPropertyTransitive.class.getSimpleName(); + } + } + + @ApplicationScoped + static class MyServiceNotMatchingPropertyOnSuperclassNotInheritable extends NotMatchingPropertyMyService { + @Override + public String hello() { + return MyServiceNotMatchingPropertyOnSuperclassNotInheritable.class.getSimpleName(); + } + } + + @ApplicationScoped + static class MyServiceNotMatchingPropertyOnSuperclassInheritable extends InheritableNotMatchingPropertyMyService { + @Override + public String hello() { + return MyServiceNotMatchingPropertyOnSuperclassInheritable.class.getSimpleName(); + } + } + + @ApplicationScoped + static class MyServiceNotMatchingPropertyTransitiveOnSuperclassNotInheritable + extends TransitiveNotMatchingPropertyMyService { + @Override + public String hello() { + return MyServiceNotMatchingPropertyTransitiveOnSuperclassNotInheritable.class.getSimpleName(); + } + } + + @ApplicationScoped + static class MyServiceNotMatchingPropertyTransitiveOnSuperclassInheritable + extends InheritableTransitiveNotMatchingPropertyMyService { + @Override + public String hello() { + return MyServiceNotMatchingPropertyTransitiveOnSuperclassInheritable.class.getSimpleName(); + } + } + + @ApplicationScoped + static class Producers { + static final String SIMPLE = "Producers.simple"; + static final String NOT_MATCHING_PROPERTY_DIRECT = "Producers.notMatchingPropertyDirect"; + static final String NOT_MATCHING_PROPERTY_TRANSITIVE = "Producers.notMatchingPropertyTransitive"; + + @Produces + MyService simple = new MyService() { + @Override + public String hello() { + return SIMPLE; + } + }; + + @Produces + @NotMatchingProperty + MyService notMatchingPropertyDirect = new MyService() { + @Override + public String hello() { + return NOT_MATCHING_PROPERTY_DIRECT; + } + }; + + @Produces + @TransitiveNotMatchingProperty + MyService notMatchingPropertyTransitive = new MyService() { + @Override + public String hello() { + return NOT_MATCHING_PROPERTY_TRANSITIVE; + } + }; + } +} diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/properties/IfBuildPropertyStereotypeTest.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/properties/IfBuildPropertyStereotypeTest.java new file mode 100644 index 0000000000000..56bbf64416233 --- /dev/null +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/properties/IfBuildPropertyStereotypeTest.java @@ -0,0 +1,199 @@ +package io.quarkus.arc.test.properties; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Set; +import java.util.stream.Collectors; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Any; +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.Produces; +import jakarta.enterprise.inject.Stereotype; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.properties.IfBuildProperty; +import io.quarkus.test.QuarkusUnitTest; + +public class IfBuildPropertyStereotypeTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(NotMatchingProperty.class, InheritableNotMatchingProperty.class, + TransitiveNotMatchingProperty.class, + InheritableTransitiveNotMatchingProperty.class, MyService.class, NotMatchingPropertyMyService.class, + InheritableNotMatchingPropertyMyService.class, TransitiveNotMatchingPropertyMyService.class, + InheritableTransitiveNotMatchingPropertyMyService.class, MyServiceSimple.class, + MyServiceNotMatchingPropertyDirect.class, MyServiceNotMatchingPropertyTransitive.class, + MyServiceNotMatchingPropertyOnSuperclassNotInheritable.class, + MyServiceNotMatchingPropertyOnSuperclassInheritable.class, + MyServiceNotMatchingPropertyTransitiveOnSuperclassNotInheritable.class, + MyServiceNotMatchingPropertyTransitiveOnSuperclassInheritable.class, Producers.class)) + .overrideConfigKey("foo.bar", "quux"); + + @Inject + @Any + Instance services; + + @Test + public void test() { + Set hello = services.stream().map(MyService::hello).collect(Collectors.toSet()); + Set expected = Set.of( + MyServiceSimple.class.getSimpleName(), + MyServiceNotMatchingPropertyOnSuperclassNotInheritable.class.getSimpleName(), + MyServiceNotMatchingPropertyTransitiveOnSuperclassNotInheritable.class.getSimpleName(), + Producers.SIMPLE); + assertEquals(expected, hello); + } + + @IfBuildProperty(name = "foo.bar", stringValue = "baz") + @Stereotype + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface NotMatchingProperty { + } + + @IfBuildProperty(name = "foo.bar", stringValue = "baz") + @Stereotype + @Inherited + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface InheritableNotMatchingProperty { + } + + @NotMatchingProperty + @Stereotype + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface TransitiveNotMatchingProperty { + } + + @NotMatchingProperty + @Stereotype + @Inherited + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface InheritableTransitiveNotMatchingProperty { + } + + interface MyService { + String hello(); + } + + @NotMatchingProperty + static abstract class NotMatchingPropertyMyService implements MyService { + } + + @InheritableNotMatchingProperty + static abstract class InheritableNotMatchingPropertyMyService implements MyService { + } + + @TransitiveNotMatchingProperty + static abstract class TransitiveNotMatchingPropertyMyService implements MyService { + } + + @InheritableTransitiveNotMatchingProperty + static abstract class InheritableTransitiveNotMatchingPropertyMyService implements MyService { + } + + @ApplicationScoped + static class MyServiceSimple implements MyService { + @Override + public String hello() { + return MyServiceSimple.class.getSimpleName(); + } + } + + @ApplicationScoped + @NotMatchingProperty + static class MyServiceNotMatchingPropertyDirect implements MyService { + @Override + public String hello() { + return MyServiceNotMatchingPropertyDirect.class.getSimpleName(); + } + } + + @ApplicationScoped + @TransitiveNotMatchingProperty + static class MyServiceNotMatchingPropertyTransitive implements MyService { + @Override + public String hello() { + return MyServiceNotMatchingPropertyTransitive.class.getSimpleName(); + } + } + + @ApplicationScoped + static class MyServiceNotMatchingPropertyOnSuperclassNotInheritable extends NotMatchingPropertyMyService { + @Override + public String hello() { + return MyServiceNotMatchingPropertyOnSuperclassNotInheritable.class.getSimpleName(); + } + } + + @ApplicationScoped + static class MyServiceNotMatchingPropertyOnSuperclassInheritable extends InheritableNotMatchingPropertyMyService { + @Override + public String hello() { + return MyServiceNotMatchingPropertyOnSuperclassInheritable.class.getSimpleName(); + } + } + + @ApplicationScoped + static class MyServiceNotMatchingPropertyTransitiveOnSuperclassNotInheritable + extends TransitiveNotMatchingPropertyMyService { + @Override + public String hello() { + return MyServiceNotMatchingPropertyTransitiveOnSuperclassNotInheritable.class.getSimpleName(); + } + } + + @ApplicationScoped + static class MyServiceNotMatchingPropertyTransitiveOnSuperclassInheritable + extends InheritableTransitiveNotMatchingPropertyMyService { + @Override + public String hello() { + return MyServiceNotMatchingPropertyTransitiveOnSuperclassInheritable.class.getSimpleName(); + } + } + + @ApplicationScoped + static class Producers { + static final String SIMPLE = "Producers.simple"; + static final String NOT_MATCHING_PROPERTY_DIRECT = "Producers.notMatchingPropertyDirect"; + static final String NOT_MATCHING_PROPERTY_TRANSITIVE = "Producers.notMatchingPropertyTransitive"; + + @Produces + MyService simple = new MyService() { + @Override + public String hello() { + return SIMPLE; + } + }; + + @Produces + @NotMatchingProperty + MyService notMatchingPropertyDirect = new MyService() { + @Override + public String hello() { + return NOT_MATCHING_PROPERTY_DIRECT; + } + }; + + @Produces + @TransitiveNotMatchingProperty + MyService notMatchingPropertyTransitive = new MyService() { + @Override + public String hello() { + return NOT_MATCHING_PROPERTY_TRANSITIVE; + } + }; + } +} diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/properties/UnlessBuildPropertyRepeatableStereotypeTest.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/properties/UnlessBuildPropertyRepeatableStereotypeTest.java new file mode 100644 index 0000000000000..30889db56e8cf --- /dev/null +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/properties/UnlessBuildPropertyRepeatableStereotypeTest.java @@ -0,0 +1,209 @@ +package io.quarkus.arc.test.properties; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Set; +import java.util.stream.Collectors; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Any; +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.Produces; +import jakarta.enterprise.inject.Stereotype; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.properties.UnlessBuildProperty; +import io.quarkus.test.QuarkusUnitTest; + +public class UnlessBuildPropertyRepeatableStereotypeTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(MatchingProperty.class, InheritableMatchingProperty.class, TransitiveMatchingProperty.class, + InheritableTransitiveMatchingProperty.class, MyService.class, MatchingPropertyMyService.class, + InheritableMatchingPropertyMyService.class, TransitiveMatchingPropertyMyService.class, + InheritableTransitiveMatchingPropertyMyService.class, MyServiceSimple.class, + MyServiceMatchingPropertyDirect.class, MyServiceMatchingPropertyTransitive.class, + MyServiceMatchingPropertyOnSuperclassNotInheritable.class, + MyServiceMatchingPropertyOnSuperclassInheritable.class, + MyServiceMatchingPropertyTransitiveOnSuperclassNotInheritable.class, + MyServiceMatchingPropertyTransitiveOnSuperclassInheritable.class, Producers.class)) + .overrideConfigKey("foo.bar", "baz") + .overrideConfigKey("some.prop", "val"); + + @Inject + @Any + Instance services; + + @Test + public void test() { + Set hello = services.stream().map(MyService::hello).collect(Collectors.toSet()); + Set expected = Set.of( + MyServiceSimple.class.getSimpleName(), + MyServiceMatchingPropertyOnSuperclassNotInheritable.class.getSimpleName(), + MyServiceMatchingPropertyTransitiveOnSuperclassNotInheritable.class.getSimpleName(), + Producers.SIMPLE); + assertEquals(expected, hello); + } + + @UnlessBuildProperty(name = "foo.bar", stringValue = "baz") + @UnlessBuildProperty(name = "some.prop", stringValue = "none") + @Stereotype + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface MatchingProperty { + } + + @UnlessBuildProperty(name = "foo.bar", stringValue = "quux") + @UnlessBuildProperty(name = "some.prop", stringValue = "val") + @Stereotype + @Inherited + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface InheritableMatchingProperty { + } + + @MatchingProperty + @Stereotype + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface TransitiveMatchingProperty { + } + + @MatchingProperty + @Stereotype + @Inherited + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface InheritableTransitiveMatchingProperty { + } + + interface MyService { + String hello(); + } + + @MatchingProperty + static abstract class MatchingPropertyMyService implements MyService { + } + + @InheritableMatchingProperty + static abstract class InheritableMatchingPropertyMyService implements MyService { + } + + @TransitiveMatchingProperty + static abstract class TransitiveMatchingPropertyMyService implements MyService { + } + + @InheritableTransitiveMatchingProperty + static abstract class InheritableTransitiveMatchingPropertyMyService implements MyService { + } + + @ApplicationScoped + static class MyServiceSimple implements MyService { + @Override + public String hello() { + return MyServiceSimple.class.getSimpleName(); + } + } + + @ApplicationScoped + @MatchingProperty + static class MyServiceMatchingPropertyDirect implements MyService { + @Override + public String hello() { + return MyServiceMatchingPropertyDirect.class.getSimpleName(); + } + } + + @ApplicationScoped + @TransitiveMatchingProperty + static class MyServiceMatchingPropertyTransitive implements MyService { + @Override + public String hello() { + return MyServiceMatchingPropertyTransitive.class.getSimpleName(); + } + } + + @ApplicationScoped + static class MyServiceMatchingPropertyOnSuperclassNotInheritable extends MatchingPropertyMyService { + @Override + public String hello() { + return MyServiceMatchingPropertyOnSuperclassNotInheritable.class.getSimpleName(); + } + } + + @ApplicationScoped + static class MyServiceMatchingPropertyOnSuperclassInheritable extends InheritableMatchingPropertyMyService { + @Override + public String hello() { + return MyServiceMatchingPropertyOnSuperclassInheritable.class.getSimpleName(); + } + } + + @ApplicationScoped + static class MyServiceMatchingPropertyTransitiveOnSuperclassNotInheritable extends TransitiveMatchingPropertyMyService { + @Override + public String hello() { + return MyServiceMatchingPropertyTransitiveOnSuperclassNotInheritable.class.getSimpleName(); + } + } + + @ApplicationScoped + static class MyServiceMatchingPropertyTransitiveOnSuperclassInheritable + extends InheritableTransitiveMatchingPropertyMyService { + @Override + public String hello() { + return MyServiceMatchingPropertyTransitiveOnSuperclassInheritable.class.getSimpleName(); + } + } + + @ApplicationScoped + static class Producers { + static final String SIMPLE = "Producers.simple"; + static final String MATCHING_PROPERTY_DIRECT = "Producers.matchingPropertyDirect"; + static final String MATCHING_PROPERTY_TRANSITIVE = "Producers.matchingPropertyTransitive"; + + @Produces + @ApplicationScoped + MyService simple() { + return new MyService() { + @Override + public String hello() { + return SIMPLE; + } + }; + } + + @Produces + @ApplicationScoped + @MatchingProperty + MyService matchingPropertyDirect() { + return new MyService() { + @Override + public String hello() { + return MATCHING_PROPERTY_DIRECT; + } + }; + } + + @Produces + @ApplicationScoped + @TransitiveMatchingProperty + MyService matchingPropertyTransitive() { + return new MyService() { + @Override + public String hello() { + return MATCHING_PROPERTY_TRANSITIVE; + } + }; + } + } +} diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/properties/UnlessBuildPropertyStereotypeTest.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/properties/UnlessBuildPropertyStereotypeTest.java new file mode 100644 index 0000000000000..3e527fc6a7fa6 --- /dev/null +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/properties/UnlessBuildPropertyStereotypeTest.java @@ -0,0 +1,206 @@ +package io.quarkus.arc.test.properties; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Set; +import java.util.stream.Collectors; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Any; +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.Produces; +import jakarta.enterprise.inject.Stereotype; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.properties.UnlessBuildProperty; +import io.quarkus.test.QuarkusUnitTest; + +public class UnlessBuildPropertyStereotypeTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(MatchingProperty.class, InheritableMatchingProperty.class, TransitiveMatchingProperty.class, + InheritableTransitiveMatchingProperty.class, MyService.class, MatchingPropertyMyService.class, + InheritableMatchingPropertyMyService.class, TransitiveMatchingPropertyMyService.class, + InheritableTransitiveMatchingPropertyMyService.class, MyServiceSimple.class, + MyServiceMatchingPropertyDirect.class, MyServiceMatchingPropertyTransitive.class, + MyServiceMatchingPropertyOnSuperclassNotInheritable.class, + MyServiceMatchingPropertyOnSuperclassInheritable.class, + MyServiceMatchingPropertyTransitiveOnSuperclassNotInheritable.class, + MyServiceMatchingPropertyTransitiveOnSuperclassInheritable.class, Producers.class)) + .overrideConfigKey("foo.bar", "baz"); + + @Inject + @Any + Instance services; + + @Test + public void test() { + Set hello = services.stream().map(MyService::hello).collect(Collectors.toSet()); + Set expected = Set.of( + MyServiceSimple.class.getSimpleName(), + MyServiceMatchingPropertyOnSuperclassNotInheritable.class.getSimpleName(), + MyServiceMatchingPropertyTransitiveOnSuperclassNotInheritable.class.getSimpleName(), + Producers.SIMPLE); + assertEquals(expected, hello); + } + + @UnlessBuildProperty(name = "foo.bar", stringValue = "baz") + @Stereotype + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface MatchingProperty { + } + + @UnlessBuildProperty(name = "foo.bar", stringValue = "baz") + @Stereotype + @Inherited + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface InheritableMatchingProperty { + } + + @MatchingProperty + @Stereotype + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface TransitiveMatchingProperty { + } + + @MatchingProperty + @Stereotype + @Inherited + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface InheritableTransitiveMatchingProperty { + } + + interface MyService { + String hello(); + } + + @MatchingProperty + static abstract class MatchingPropertyMyService implements MyService { + } + + @InheritableMatchingProperty + static abstract class InheritableMatchingPropertyMyService implements MyService { + } + + @TransitiveMatchingProperty + static abstract class TransitiveMatchingPropertyMyService implements MyService { + } + + @InheritableTransitiveMatchingProperty + static abstract class InheritableTransitiveMatchingPropertyMyService implements MyService { + } + + @ApplicationScoped + static class MyServiceSimple implements MyService { + @Override + public String hello() { + return MyServiceSimple.class.getSimpleName(); + } + } + + @ApplicationScoped + @MatchingProperty + static class MyServiceMatchingPropertyDirect implements MyService { + @Override + public String hello() { + return MyServiceMatchingPropertyDirect.class.getSimpleName(); + } + } + + @ApplicationScoped + @TransitiveMatchingProperty + static class MyServiceMatchingPropertyTransitive implements MyService { + @Override + public String hello() { + return MyServiceMatchingPropertyTransitive.class.getSimpleName(); + } + } + + @ApplicationScoped + static class MyServiceMatchingPropertyOnSuperclassNotInheritable extends MatchingPropertyMyService { + @Override + public String hello() { + return MyServiceMatchingPropertyOnSuperclassNotInheritable.class.getSimpleName(); + } + } + + @ApplicationScoped + static class MyServiceMatchingPropertyOnSuperclassInheritable extends InheritableMatchingPropertyMyService { + @Override + public String hello() { + return MyServiceMatchingPropertyOnSuperclassInheritable.class.getSimpleName(); + } + } + + @ApplicationScoped + static class MyServiceMatchingPropertyTransitiveOnSuperclassNotInheritable extends TransitiveMatchingPropertyMyService { + @Override + public String hello() { + return MyServiceMatchingPropertyTransitiveOnSuperclassNotInheritable.class.getSimpleName(); + } + } + + @ApplicationScoped + static class MyServiceMatchingPropertyTransitiveOnSuperclassInheritable + extends InheritableTransitiveMatchingPropertyMyService { + @Override + public String hello() { + return MyServiceMatchingPropertyTransitiveOnSuperclassInheritable.class.getSimpleName(); + } + } + + @ApplicationScoped + static class Producers { + static final String SIMPLE = "Producers.simple"; + static final String MATCHING_PROPERTY_DIRECT = "Producers.matchingPropertyDirect"; + static final String MATCHING_PROPERTY_TRANSITIVE = "Producers.matchingPropertyTransitive"; + + @Produces + @ApplicationScoped + MyService simple() { + return new MyService() { + @Override + public String hello() { + return SIMPLE; + } + }; + } + + @Produces + @ApplicationScoped + @MatchingProperty + MyService matchingPropertyDirect() { + return new MyService() { + @Override + public String hello() { + return MATCHING_PROPERTY_DIRECT; + } + }; + } + + @Produces + @ApplicationScoped + @TransitiveMatchingProperty + MyService matchingPropertyTransitive() { + return new MyService() { + @Override + public String hello() { + return MATCHING_PROPERTY_TRANSITIVE; + } + }; + } + } +} diff --git a/extensions/arc/runtime/src/main/java/io/quarkus/arc/properties/IfBuildProperty.java b/extensions/arc/runtime/src/main/java/io/quarkus/arc/properties/IfBuildProperty.java index 850c6e85490e7..9dac62c08ab40 100644 --- a/extensions/arc/runtime/src/main/java/io/quarkus/arc/properties/IfBuildProperty.java +++ b/extensions/arc/runtime/src/main/java/io/quarkus/arc/properties/IfBuildProperty.java @@ -11,7 +11,7 @@ * if the Quarkus build time property matches the provided value. *

* By default, the bean is not enabled when the build time property is not defined at all, but this behavior is configurable - * via the {#code enableIfMissing} property. + * via the {@code enableIfMissing} property. *

* This annotation is repeatable. A bean will only be enabled if all the conditions defined by the {@link IfBuildProperty} * annotations are satisfied. diff --git a/extensions/arc/runtime/src/main/java/io/quarkus/arc/properties/UnlessBuildProperty.java b/extensions/arc/runtime/src/main/java/io/quarkus/arc/properties/UnlessBuildProperty.java index 2999c5c1d83ee..6854eeb0ce720 100644 --- a/extensions/arc/runtime/src/main/java/io/quarkus/arc/properties/UnlessBuildProperty.java +++ b/extensions/arc/runtime/src/main/java/io/quarkus/arc/properties/UnlessBuildProperty.java @@ -11,7 +11,7 @@ * if the Quarkus build time property does not match the provided value. *

* By default, the bean is not enabled when the build time property is not defined at all, but this behavior is configurable - * via the {#code enableIfMissing} property. + * via the {@code enableIfMissing} property. *

* This annotation is repeatable. A bean will only be enabled if all the conditions defined by the * {@link UnlessBuildProperty} annotations are satisfied. diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java index f64a96a1c42f5..4e8cce6ae4c8c 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java @@ -1141,8 +1141,9 @@ private List findBeans(Collection beanDefiningAnnotations, Li beanClasses.add(beanClass); } } - } else { + } else if (!annotationStore.hasAnnotation(field, DotNames.PRODUCES)) { // Verify that non-producer fields are not annotated with stereotypes + // (vetoed producers must _not_ be checked) for (AnnotationInstance i : annotationStore.getAnnotations(field)) { if (realStereotypes.contains(i.name())) { throw new DefinitionException(