diff --git a/docs/src/main/asciidoc/config-reference.adoc b/docs/src/main/asciidoc/config-reference.adoc index 4ee6f4d16dbff..e41a982486caf 100644 --- a/docs/src/main/asciidoc/config-reference.adoc +++ b/docs/src/main/asciidoc/config-reference.adoc @@ -364,6 +364,37 @@ complex: WARNING: A limitation of such configuration is that the types used as the generic types of the lists need to be classes and not interfaces. +=== Combining ConfigProperties with build time conditions + +Quarkus allows you to define conditions evaluated at build time (`@IfBuildProfile`, `@UnlessBuildProfile`, `@IfBuildProperty` and `@UnlessBuildProperty`) to enable or not the annotations `@ConfigProperties` and `@ConfigPrefix` which gives you a very flexible way to map your configuration. + +Let's assume that the configuration of a service is mapped thanks to a `@ConfigProperties` and you don't need this part of the configuration for your tests as it will be mocked, in that case you can define a build time condition like in the next example: + +`ServiceConfiguration.java` +[source,java] +---- +@UnlessBuildProfile("test") <1> +@ConfigProperties +public class ServiceConfiguration { + public String user; + public String password; +} +---- +<1> The annotation `@ConfigProperties` is considered if and only if the active profile is not `test`. + +`SomeBean.java` +[source,java] +---- +@ApplicationScoped +public class SomeBean { + + @Inject + Instance serviceConfiguration; <1> + +} +---- +<1> As the configuration of the service could be missing, we need to use `Instance` as type at the injection point. + [[configuration_profiles]] == Configuration Profiles diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BuildExclusionsBuildItem.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BuildExclusionsBuildItem.java new file mode 100644 index 0000000000000..c6fa6f3df25b9 --- /dev/null +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BuildExclusionsBuildItem.java @@ -0,0 +1,99 @@ +package io.quarkus.arc.deployment; + +import java.util.Set; + +import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.MethodInfo; + +import io.quarkus.builder.item.SimpleBuildItem; + +/** + * A type of build item that contains only declaring classes, methods and fields that have been annotated with + * unsuccessful build time conditions. It aims to be used to manage the exclusion of the annotations thanks to the + * build time conditions also known as {@code IfBuildProfile}, {@code UnlessBuildProfile}, {@code IfBuildProperty} and + * {@code UnlessBuildProperty} + * + * @see io.quarkus.arc.deployment.PreAdditionalBeanBuildTimeConditionBuildItem + * @see io.quarkus.arc.profile.IfBuildProfile + * @see io.quarkus.arc.profile.UnlessBuildProfile + * @see io.quarkus.arc.properties.IfBuildProperty + * @see io.quarkus.arc.properties.UnlessBuildProperty + */ +public final class BuildExclusionsBuildItem extends SimpleBuildItem { + + private final Set excludedDeclaringClasses; + private final Set excludedMethods; + private final Set excludedFields; + + public BuildExclusionsBuildItem(Set excludedDeclaringClasses, + Set excludedMethods, + Set excludedFields) { + this.excludedDeclaringClasses = excludedDeclaringClasses; + this.excludedMethods = excludedMethods; + this.excludedFields = excludedFields; + } + + public Set getExcludedDeclaringClasses() { + return excludedDeclaringClasses; + } + + public Set getExcludedMethods() { + return excludedMethods; + } + + public Set getExcludedFields() { + return excludedFields; + } + + /** + * Indicates whether the given target is excluded following the next rules: + *

+ *

    + *
  • In case of a class it will check if it is part of the excluded classes
  • + *
  • In case of a method it will check if it is part of the excluded methods and if its declaring class + * is excluded
  • + *
  • In case of a method parameter it will check if its corresponding method is part of the excluded methods + * and if its declaring class is excluded
  • + *
  • In case of a field it will check if it is part of the excluded field and if its declaring class is excluded
  • + *
  • In all other cases, it is not excluded
  • + *
+ * + * @param target the target to check. + * @return {@code true} if the target is excluded, {@code false} otherwise. + */ + public boolean isExcluded(AnnotationTarget target) { + switch (target.kind()) { + case CLASS: + return excludedDeclaringClasses.contains(targetMapper(target)); + case METHOD: + return excludedMethods.contains(targetMapper(target)) || + excludedDeclaringClasses.contains(targetMapper(target.asMethod().declaringClass())); + case METHOD_PARAMETER: + final MethodInfo method = target.asMethodParameter().method(); + return excludedMethods.contains(targetMapper(method)) || + excludedDeclaringClasses.contains(targetMapper(method.declaringClass())); + case FIELD: + return excludedFields.contains(targetMapper(target)) || + excludedDeclaringClasses.contains(targetMapper(target.asField().declaringClass())); + default: + return false; + } + } + + /** + * Converts the given target into a String unique representation. + * + * @param target the target to convert. + * @return a unique representation as a {@code String} of the target + */ + public static String targetMapper(AnnotationTarget target) { + final AnnotationTarget.Kind kind = target.kind(); + if (kind == AnnotationTarget.Kind.CLASS) { + return target.asClass().toString(); + } else if (kind == AnnotationTarget.Kind.METHOD) { + final MethodInfo method = target.asMethod(); + return String.format("%s#%s", method.declaringClass(), method); + } + return target.asField().toString(); + } +} 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 7a1f8d12ccec5..fc6ec38868991 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 @@ -1,12 +1,15 @@ package io.quarkus.arc.deployment; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; 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.stream.Collectors; import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.spi.ConfigProviderResolver; @@ -235,6 +238,26 @@ public void transform(TransformationContext ctx) { })); } + /** + * @param buildTimeConditions the build time conditions from which the excluded classes are extracted. + * @return an instance of {@link BuildExclusionsBuildItem} containing the set of classes + * that have been annotated with unsuccessful build time conditions. + */ + @BuildStep + BuildExclusionsBuildItem buildExclusions(List buildTimeConditions) { + final Map> map = buildTimeConditions.stream() + .filter(item -> !item.isEnabled()) + .map(PreAdditionalBeanBuildTimeConditionBuildItem::getTarget) + .collect( + Collectors.groupingBy( + AnnotationTarget::kind, + Collectors.mapping(BuildExclusionsBuildItem::targetMapper, Collectors.toSet()))); + return new BuildExclusionsBuildItem( + map.getOrDefault(AnnotationTarget.Kind.CLASS, Collections.emptySet()), + map.getOrDefault(AnnotationTarget.Kind.METHOD, Collections.emptySet()), + map.getOrDefault(AnnotationTarget.Kind.FIELD, Collections.emptySet())); + } + private String toUniqueString(FieldInfo field) { return field.declaringClass().name().toString() + "." + field.name(); } diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/configproperties/ConfigPropertiesBuildStep.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/configproperties/ConfigPropertiesBuildStep.java index 88ff89a1acabf..7f2245e2e822a 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/configproperties/ConfigPropertiesBuildStep.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/configproperties/ConfigPropertiesBuildStep.java @@ -15,9 +15,11 @@ import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; import org.jboss.jandex.IndexView; +import org.jboss.jandex.MethodParameterInfo; import io.quarkus.arc.config.ConfigProperties; import io.quarkus.arc.deployment.ArcConfig; +import io.quarkus.arc.deployment.BuildExclusionsBuildItem; import io.quarkus.arc.deployment.ConfigPropertyBuildItem; import io.quarkus.arc.deployment.GeneratedBeanBuildItem; import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor; @@ -38,6 +40,7 @@ public class ConfigPropertiesBuildStep { @BuildStep void produceConfigPropertiesMetadata(CombinedIndexBuildItem combinedIndex, ArcConfig arcConfig, + BuildExclusionsBuildItem exclusionsBuildItem, BuildProducer configPropertiesMetadataProducer) { IndexView index = combinedIndex.getIndex(); @@ -46,9 +49,13 @@ void produceConfigPropertiesMetadata(CombinedIndexBuildItem combinedIndex, ArcCo Map failOnMismatchingMembers = new HashMap<>(); // handle @ConfigProperties + final Set excludedDeclaringClasses = exclusionsBuildItem.getExcludedDeclaringClasses(); for (AnnotationInstance instance : index.getAnnotations(DotNames.CONFIG_PROPERTIES)) { - ClassInfo classInfo = instance.target().asClass(); - + final AnnotationTarget target = instance.target(); + if (exclusionsBuildItem.isExcluded(target)) { + continue; + } + ClassInfo classInfo = target.asClass(); ConfigProperties.NamingStrategy namingStrategy = getNamingStrategy(arcConfig, instance.value("namingStrategy")); namingStrategies.put(classInfo.name(), namingStrategy); @@ -63,12 +70,16 @@ void produceConfigPropertiesMetadata(CombinedIndexBuildItem combinedIndex, ArcCo // handle @ConfigPrefix for (AnnotationInstance instance : index.getAnnotations(DotNames.CONFIG_PREFIX)) { ClassInfo classInfo; - if (instance.target().kind() == AnnotationTarget.Kind.FIELD) { - classInfo = index.getClassByName(instance.target().asField().type().name()); - } else if (instance.target().kind() == AnnotationTarget.Kind.METHOD_PARAMETER) { - short position = instance.target().asMethodParameter().position(); - classInfo = index - .getClassByName(instance.target().asMethodParameter().method().parameters().get(position).name()); + final AnnotationTarget target = instance.target(); + if (exclusionsBuildItem.isExcluded(target)) { + continue; + } + if (target.kind() == AnnotationTarget.Kind.FIELD) { + classInfo = index.getClassByName(target.asField().type().name()); + } else if (target.kind() == AnnotationTarget.Kind.METHOD_PARAMETER) { + final MethodParameterInfo parameter = target.asMethodParameter(); + short position = parameter.position(); + classInfo = index.getClassByName(parameter.method().parameters().get(position).name()); } else { break; } diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/profile/IfBuildProfileTest.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/profile/IfBuildProfileTest.java index 92b14f7978adf..d18d52a7198e5 100644 --- a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/profile/IfBuildProfileTest.java +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/profile/IfBuildProfileTest.java @@ -17,12 +17,16 @@ import javax.interceptor.Interceptor; import javax.interceptor.InvocationContext; +import org.eclipse.microprofile.config.inject.ConfigProperty; import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; import org.jboss.shrinkwrap.api.spec.JavaArchive; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import io.quarkus.arc.DefaultBean; +import io.quarkus.arc.config.ConfigPrefix; +import io.quarkus.arc.config.ConfigProperties; import io.quarkus.arc.profile.IfBuildProfile; import io.quarkus.test.QuarkusUnitTest; @@ -32,15 +36,26 @@ public class IfBuildProfileTest { static final QuarkusUnitTest config = new QuarkusUnitTest() .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) .addClasses(Producer.class, OtherProducer.class, AnotherProducer.class, - GreetingBean.class, Hello.class, PingBean.class, PongBean.class, FooBean.class, BarBean.class, - TestInterceptor.class, ProdInterceptor.class, Logging.class)); - + TestInterceptor.class, ProdInterceptor.class, Logging.class, DummyTestBean.class, + DummyProdBean.class) + .addAsResource( + new StringAsset( + "%test.dummy.message=Hi from Test\n" + + "%test.dummy.complex.message=Hi from complex Test\n" + + "%test.dummy.complex.bis.message=Hi from complex bis Test\n"), + "application.properties")); @Inject Hello hello; @Inject Instance barBean; + @Inject + DummyTestBean dummyTest; + + @Inject + Instance dummyProd; + @Test public void testInjection() { assertFalse(TestInterceptor.INTERCEPTED.get()); @@ -52,6 +67,14 @@ public void testInjection() { assertTrue(barBean.isUnsatisfied()); assertTrue(TestInterceptor.INTERCEPTED.get()); assertFalse(ProdInterceptor.INTERCEPTED.get()); + assertEquals("Hi from Test", dummyTest.message); + assertEquals("Hi from Test", dummyTest.dummy.message); + assertEquals("Hi from complex Test", dummyTest.dummyComplex.message); + assertEquals("Hi from complex bis Test", dummyTest.dummyComplexBis.message); + assertTrue(dummyTest.dummyProd.isUnsatisfied()); + assertTrue(dummyProd.isUnsatisfied()); + assertTrue(hello.dummyAbsent().isUnsatisfied()); + assertTrue(hello.dummyAbsentBis().isUnsatisfied()); } @Test @@ -59,6 +82,53 @@ public void testSelect() { assertEquals("hello from test. Foo is: foo from test", CDI.current().select(GreetingBean.class).get().greet()); } + @ConfigProperties + public static class Dummy { + public String message; + } + + @Singleton + @IfBuildProfile("test") + static class DummyTestBean { + @ConfigProperty(name = "dummy.message") + String message; + @Inject + Dummy dummy; + @ConfigPrefix("dummy.complex") + Dummy dummyComplex; + Dummy dummyComplexBis; + @Inject + Instance dummyProd; + + @Inject + public void setDummyComplexBis(@ConfigPrefix("dummy.complex.bis") Dummy dummyComplexBis) { + this.dummyComplexBis = dummyComplexBis; + } + } + + @IfBuildProfile("prod") + @ConfigProperties(prefix = "dummy.prod") + public static class DummyProd { + public String message; + } + + @Singleton + @IfBuildProfile("prod") + static class DummyProdBean { + @ConfigProperty(name = "dummy.message") + String message; + @ConfigPrefix("dummy.absent") // Should not make it fail as excluded by the IfBuildProfile annotation + Dummy dummyAbsent; + Dummy dummyAbsentBis; + @Inject + DummyProd dummyProd; + + @Inject + public void setDummyAbsentBis(@ConfigPrefix("dummy.absent.bis") Dummy dummyAbsentBis) { // Should not make it fail as excluded by the IfBuildProfile annotation + this.dummyAbsentBis = dummyAbsentBis; + } + } + @Logging @ApplicationScoped static class Hello { @@ -91,6 +161,25 @@ String foo() { return fooBean.foo(); } + Instance dummyAbsent; + + @IfBuildProfile("prod") + @ConfigPrefix("dummy.absent.bis") + Instance dummyAbsentBis; + + @IfBuildProfile("prod") + @Inject + public void setDummyAbsent(@ConfigPrefix("dummy.absent") Instance dummyAbsent) { + this.dummyAbsent = dummyAbsent; + } + + Instance dummyAbsent() { + return dummyAbsent; + } + + Instance dummyAbsentBis() { + return dummyAbsentBis; + } } @DefaultBean diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/profile/UnlessBuildProfileTest.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/profile/UnlessBuildProfileTest.java index 9e96478b4b35b..145b7a27243b2 100644 --- a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/profile/UnlessBuildProfileTest.java +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/profile/UnlessBuildProfileTest.java @@ -17,12 +17,16 @@ import javax.interceptor.Interceptor; import javax.interceptor.InvocationContext; +import org.eclipse.microprofile.config.inject.ConfigProperty; import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; import org.jboss.shrinkwrap.api.spec.JavaArchive; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import io.quarkus.arc.DefaultBean; +import io.quarkus.arc.config.ConfigPrefix; +import io.quarkus.arc.config.ConfigProperties; import io.quarkus.arc.profile.UnlessBuildProfile; import io.quarkus.test.QuarkusUnitTest; @@ -33,14 +37,26 @@ public class UnlessBuildProfileTest { .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) .addClasses(Producer.class, AnotherProducer.class, GreetingBean.class, Hello.class, PingBean.class, PongBean.class, FooBean.class, BarBean.class, - TestInterceptor.class, ProdInterceptor.class, Logging.class)); - + TestInterceptor.class, ProdInterceptor.class, Logging.class, DummyTestBean.class, + DummyProdBean.class) + .addAsResource( + new StringAsset( + "%test.dummy.message=Hi from not prod\n" + + "%test.dummy.complex.message=Hi from complex not prod\n" + + "%test.dummy.complex.bis.message=Hi from complex bis not prod\n"), + "application.properties")); @Inject Hello hello; @Inject Instance barBean; + @Inject + DummyTestBean dummyTest; + + @Inject + Instance dummyProd; + @Test public void testInjection() { assertFalse(TestInterceptor.INTERCEPTED.get()); @@ -52,6 +68,14 @@ public void testInjection() { assertTrue(barBean.isUnsatisfied()); assertTrue(TestInterceptor.INTERCEPTED.get()); assertFalse(ProdInterceptor.INTERCEPTED.get()); + assertEquals("Hi from not prod", dummyTest.message); + assertEquals("Hi from not prod", dummyTest.dummy.message); + assertEquals("Hi from complex not prod", dummyTest.dummyComplex.message); + assertEquals("Hi from complex bis not prod", dummyTest.dummyComplexBis.message); + assertTrue(dummyTest.dummyProd.isUnsatisfied()); + assertTrue(dummyProd.isUnsatisfied()); + assertTrue(hello.dummyAbsent().isUnsatisfied()); + assertTrue(hello.dummyAbsentBis().isUnsatisfied()); } @Test @@ -59,6 +83,53 @@ public void testSelect() { assertEquals("hello from not prod. Foo is: foo from not prod", CDI.current().select(GreetingBean.class).get().greet()); } + @ConfigProperties + public static class Dummy { + public String message; + } + + @Singleton + @UnlessBuildProfile("prod") + static class DummyTestBean { + @ConfigProperty(name = "dummy.message") + String message; + @Inject + Dummy dummy; + @ConfigPrefix("dummy.complex") + Dummy dummyComplex; + Dummy dummyComplexBis; + @Inject + Instance dummyProd; + + @Inject + public void setDummyComplexBis(@ConfigPrefix("dummy.complex.bis") Dummy dummyComplexBis) { + this.dummyComplexBis = dummyComplexBis; + } + } + + @UnlessBuildProfile("test") + @ConfigProperties(prefix = "dummy.prod") + public static class DummyProd { + public String message; + } + + @Singleton + @UnlessBuildProfile("test") + static class DummyProdBean { + @ConfigProperty(name = "dummy.message") + String message; + @ConfigPrefix("dummy.absent") // Should not make it fail as excluded by the UnlessBuildProfile annotation + Dummy dummyAbsent; + Dummy dummyAbsentBis; + @Inject + DummyProd dummyProd; + + @Inject + public void setDummyAbsentBis(@ConfigPrefix("dummy.absent.bis") Dummy dummyAbsentBis) { // Should not make it fail as excluded by the UnlessBuildProfile annotation + this.dummyAbsentBis = dummyAbsentBis; + } + } + @Logging @ApplicationScoped static class Hello { @@ -91,6 +162,25 @@ String foo() { return fooBean.foo(); } + Instance dummyAbsent; + + @UnlessBuildProfile("test") + @ConfigPrefix("dummy.absent.bis") + Instance dummyAbsentBis; + + @UnlessBuildProfile("test") + @Inject + public void setDummyAbsent(@ConfigPrefix("dummy.absent") Instance dummyAbsent) { + this.dummyAbsent = dummyAbsent; + } + + Instance dummyAbsent() { + return dummyAbsent; + } + + Instance dummyAbsentBis() { + return dummyAbsentBis; + } } @DefaultBean