From c03a3574488935d2d21d0beb0e7645d15bc6f42a Mon Sep 17 00:00:00 2001 From: essobedo Date: Sat, 20 Feb 2021 14:14:14 +0100 Subject: [PATCH] Allow to exclude JAX-RS classes with annotations Co-authored-by: Georgios Andrianakis --- docs/src/main/asciidoc/rest-json.adoc | 22 ++ docs/src/main/asciidoc/resteasy-reactive.adoc | 22 ++ .../deployment/BuildTimeEnabledProcessor.java | 65 ++++- ...tionalBeanBuildTimeConditionBuildItem.java | 40 +++ .../ResteasyReactiveCommonProcessor.java | 26 +- .../runtime/ResteasyReactiveConfig.java | 7 + .../deployment/ResteasyReactiveProcessor.java | 4 +- .../server/test/simple/BuildProfileTest.java | 240 ++++++++++++++++++ .../ResteasyServerCommonProcessor.java | 71 +++++- .../resteasy/test/root/BuildProfileTest.java | 240 ++++++++++++++++++ .../scanning/ApplicationScanningResult.java | 27 +- .../scanning/ResteasyReactiveScanner.java | 7 +- .../framework/ResteasyReactiveUnitTest.java | 3 +- 13 files changed, 746 insertions(+), 28 deletions(-) create mode 100644 extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/PreAdditionalBeanBuildTimeConditionBuildItem.java create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/simple/BuildProfileTest.java create mode 100644 extensions/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/root/BuildProfileTest.java diff --git a/docs/src/main/asciidoc/rest-json.adoc b/docs/src/main/asciidoc/rest-json.adoc index 805b216d6b003..b9b50f928ea42 100644 --- a/docs/src/main/asciidoc/rest-json.adoc +++ b/docs/src/main/asciidoc/rest-json.adoc @@ -605,6 +605,28 @@ If set to `true` (default) then a *single instance* of a resource class is creat If set to `false` then a *new instance* of the resource class is created per each request. An explicit CDI scope annotation (`@RequestScoped`, `@ApplicationScoped`, etc.) always overrides the default behavior and specifies the lifecycle of resource instances. +== Include/Exclude JAX-RS classes with build time conditions + +Quarkus enables the inclusion or exclusion of JAX-RS Resources, Providers and Features directly thanks to build time conditions in the same that it does for CDI beans. +Thus, the various JAX-RS classes can be annotated with profile conditions (`@io.quarkus.arc.profile.IfBuildProfile` or `@io.quarkus.arc.profile.UnlessBuildProfile`) and/or with property conditions (`io.quarkus.arc.properties.IfBuildProperty` or `io.quarkus.arc.properties.UnlessBuildProperty`) to indicate to Quarkus at build time under which conditions these JAX-RS classes should be included. + +In the following example, Quarkus includes the endpoint `sayHello` if and only if the build profile `app1` has been enabled. + +[source,java] +---- +@IfBuildProfile("app1") +public class ResourceForApp1Only { + + @GET + @Path("sayHello") + public String sayHello() { + return "hello"; + } +} +---- + +Please note that if a JAX-RS Application has been detected and the method `getClasses()` and/or `getSingletons()` has/have been overridden, Quarkus will ignore the build time conditions and consider only what has been defined in the JAX-RS Application. + == Conclusion Creating JSON REST services with Quarkus is easy as it relies on proven and well known technologies. diff --git a/docs/src/main/asciidoc/resteasy-reactive.adoc b/docs/src/main/asciidoc/resteasy-reactive.adoc index f97e371568a58..9623731d82108 100644 --- a/docs/src/main/asciidoc/resteasy-reactive.adoc +++ b/docs/src/main/asciidoc/resteasy-reactive.adoc @@ -1790,3 +1790,25 @@ Or plain text: < < {"name":"roquefort"} ---- + +== Include/Exclude JAX-RS classes with build time conditions + +Quarkus enables the inclusion or exclusion of JAX-RS Resources, Providers and Features directly thanks to build time conditions in the same that it does for CDI beans. +Thus, the various JAX-RS classes can be annotated with profile conditions (`@io.quarkus.arc.profile.IfBuildProfile` or `@io.quarkus.arc.profile.UnlessBuildProfile`) and/or with property conditions (`io.quarkus.arc.properties.IfBuildProperty` or `io.quarkus.arc.properties.UnlessBuildProperty`) to indicate to Quarkus at build time under which conditions these JAX-RS classes should be included. + +In the following example, Quarkus includes the endpoint `sayHello` if and only if the build profile `app1` has been enabled. + +[source,java] +---- +@IfBuildProfile("app1") +public class ResourceForApp1Only { + + @GET + @Path("sayHello") + public String sayHello() { + return "hello"; + } +} +---- + +Please note that if a JAX-RS Application has been detected and the method `getClasses()` and/or `getSingletons()` has/have been overridden, Quarkus will ignore the build time conditions and consider only what has been defined in the JAX-RS Application. 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 7c202e0a24376..7a1f8d12ccec5 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,6 +5,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.function.BiConsumer; import java.util.function.BiFunction; import org.eclipse.microprofile.config.Config; @@ -15,6 +16,7 @@ import org.jboss.jandex.AnnotationValue; import org.jboss.jandex.DotName; import org.jboss.jandex.FieldInfo; +import org.jboss.jandex.IndexView; import org.jboss.jandex.MethodInfo; import org.jboss.logging.Logger; @@ -42,7 +44,8 @@ public class BuildTimeEnabledProcessor { private static final DotName UNLESS_BUILD_PROPERTY = DotName.createSimple(UnlessBuildProperty.class.getName()); @BuildStep - void ifBuildProfile(CombinedIndexBuildItem index, BuildProducer producer) { + void ifBuildProfile(CombinedIndexBuildItem index, BuildProducer producer, + BuildProducer producerPreAdditionalBean) { Collection annotationInstances = index.getIndex().getAnnotations(IF_BUILD_PROFILE); for (AnnotationInstance instance : annotationInstances) { String profileOnInstance = instance.value().asString(); @@ -53,11 +56,13 @@ void ifBuildProfile(CombinedIndexBuildItem index, BuildProducer producer) { + void unlessBuildProfile(CombinedIndexBuildItem index, BuildProducer producer, + BuildProducer producerPreAdditionalBean) { Collection annotationInstances = index.getIndex().getAnnotations(UNLESS_BUILD_PROFILE); for (AnnotationInstance instance : annotationInstances) { String profileOnInstance = instance.value().asString(); @@ -68,6 +73,7 @@ void unlessBuildProfile(CombinedIndexBuildItem index, BuildProducer() { + @Override + public void accept(AnnotationTarget target, Boolean enabled) { + producer.produce(new BuildTimeConditionBuildItem(target, enabled)); + } + }); } @BuildStep @@ -88,13 +99,50 @@ void unlessBuildProperty(BeanArchiveIndexBuildItem index, BuildProducer() { + @Override + public void accept(AnnotationTarget target, Boolean enabled) { + producer.produce(new BuildTimeConditionBuildItem(target, enabled)); + } + }); + } + + @BuildStep + void ifBuildPropertyPreAdditionalBean(CombinedIndexBuildItem index, + BuildProducer producer) { + buildProperty(IF_BUILD_PROPERTY, 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) { + producer.produce(new PreAdditionalBeanBuildTimeConditionBuildItem(target, enabled)); + } + }); + } + + @BuildStep + void unlessBuildPropertyPreAdditionalBean(CombinedIndexBuildItem index, + BuildProducer producer) { + buildProperty(UNLESS_BUILD_PROPERTY, 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) { + producer.produce(new PreAdditionalBeanBuildTimeConditionBuildItem(target, enabled)); + } + }); } - void buildProperty(DotName annotationName, BiFunction testFun, BeanArchiveIndexBuildItem index, - BuildProducer producer) { + void buildProperty(DotName annotationName, BiFunction testFun, IndexView index, + BiConsumer producer) { Config config = ConfigProviderResolver.instance().getConfig(); - Collection annotationInstances = index.getIndex().getAnnotations(annotationName); + Collection annotationInstances = index.getAnnotations(annotationName); for (AnnotationInstance instance : annotationInstances) { String propertyName = instance.value("name").asString(); String expectedStringValue = instance.value("stringValue").asString(); @@ -123,8 +171,7 @@ void buildProperty(DotName annotationName, BiFunction t enabled = false; } } - - producer.produce(new BuildTimeConditionBuildItem(instance.target(), enabled)); + producer.accept(instance.target(), enabled); } } diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/PreAdditionalBeanBuildTimeConditionBuildItem.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/PreAdditionalBeanBuildTimeConditionBuildItem.java new file mode 100644 index 0000000000000..1a23fb7cd6221 --- /dev/null +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/PreAdditionalBeanBuildTimeConditionBuildItem.java @@ -0,0 +1,40 @@ +package io.quarkus.arc.deployment; + +import org.jboss.jandex.AnnotationTarget; + +import io.quarkus.builder.item.MultiBuildItem; + +/** + * A type of build item that is similar to {@link BuildTimeConditionBuildItem} but evaluated before + * processing the {@link AdditionalBeanBuildItem} in order to filter the beans thanks to build time conditions + * before actually adding them with a {@link AdditionalBeanBuildItem}. + * + * @see io.quarkus.arc.deployment.BuildTimeConditionBuildItem + * @see io.quarkus.arc.deployment.AdditionalBeanBuildItem + */ +public final class PreAdditionalBeanBuildTimeConditionBuildItem extends MultiBuildItem { + + private final AnnotationTarget target; + private final boolean enabled; + + public PreAdditionalBeanBuildTimeConditionBuildItem(AnnotationTarget target, boolean enabled) { + switch (target.kind()) { + case CLASS: + case METHOD: + case FIELD: + this.target = target; + break; + default: + throw new IllegalArgumentException("'target' can only be a class, a field or a method"); + } + this.enabled = enabled; + } + + public AnnotationTarget getTarget() { + return target; + } + + public boolean isEnabled() { + return enabled; + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/ResteasyReactiveCommonProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/ResteasyReactiveCommonProcessor.java index e01db0be5ed87..71dd5112cbaa9 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/ResteasyReactiveCommonProcessor.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/ResteasyReactiveCommonProcessor.java @@ -8,10 +8,12 @@ import java.util.Optional; import java.util.Set; import java.util.function.Consumer; +import java.util.stream.Collectors; import javax.ws.rs.RuntimeType; import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationTarget; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; import org.jboss.jandex.IndexView; @@ -31,12 +33,14 @@ import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem; import io.quarkus.arc.deployment.BeanArchiveIndexBuildItem; import io.quarkus.arc.deployment.BeanContainerBuildItem; +import io.quarkus.arc.deployment.PreAdditionalBeanBuildTimeConditionBuildItem; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.util.JandexUtil; import io.quarkus.resteasy.reactive.common.runtime.JaxRsSecurityConfig; +import io.quarkus.resteasy.reactive.common.runtime.ResteasyReactiveConfig; import io.quarkus.resteasy.reactive.spi.AbstractInterceptorBuildItem; import io.quarkus.resteasy.reactive.spi.ContainerRequestFilterBuildItem; import io.quarkus.resteasy.reactive.spi.ContainerResponseFilterBuildItem; @@ -71,9 +75,15 @@ void setUpDenyAllJaxRs(CombinedIndexBuildItem index, @BuildStep ApplicationResultBuildItem handleApplication(CombinedIndexBuildItem combinedIndexBuildItem, - BuildProducer reflectiveClass) { + BuildProducer reflectiveClass, + List buildTimeConditions, + ResteasyReactiveConfig config) { + // Use the "pre additional bean" build time conditions since we need to be able to filter the beans + // before actually adding them otherwise if we use normal build time conditions, we end up + // with a circular dependency ApplicationScanningResult result = ResteasyReactiveScanner - .scanForApplicationClass(combinedIndexBuildItem.getComputingIndex()); + .scanForApplicationClass(combinedIndexBuildItem.getComputingIndex(), + config.buildTimeConditionAware ? getExcludedClasses(buildTimeConditions) : Collections.emptySet()); if (result.getSelectedAppClass() != null) { reflectiveClass.produce(new ReflectiveClassBuildItem(false, false, result.getSelectedAppClass().name().toString())); } @@ -268,4 +278,16 @@ public void setupEndpoints(BeanArchiveIndexBuildItem beanArchiveIndexBuildItem, } } + /** + * @param buildTimeConditions the build time conditions from which the excluded classes are extracted. + * @return the set of classes that have been annotated with unsuccessful build time conditions. + */ + private static Set getExcludedClasses(List buildTimeConditions) { + return buildTimeConditions.stream() + .filter(item -> !item.isEnabled()) + .map(PreAdditionalBeanBuildTimeConditionBuildItem::getTarget) + .filter(target -> target.kind() == AnnotationTarget.Kind.CLASS) + .map(target -> target.asClass().toString()) + .collect(Collectors.toSet()); + } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/runtime/src/main/java/io/quarkus/resteasy/reactive/common/runtime/ResteasyReactiveConfig.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/runtime/src/main/java/io/quarkus/resteasy/reactive/common/runtime/ResteasyReactiveConfig.java index 8cd786572cc51..6e3405f60d495 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/runtime/src/main/java/io/quarkus/resteasy/reactive/common/runtime/ResteasyReactiveConfig.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/runtime/src/main/java/io/quarkus/resteasy/reactive/common/runtime/ResteasyReactiveConfig.java @@ -34,4 +34,11 @@ public class ResteasyReactiveConfig { @ConfigItem(defaultValue = "true") @Experimental("This flag has a high probability of going away in the future") public boolean defaultProduces; + + /** + * Whether or not annotations such `@IfBuildTimeProfile`, `@IfBuildTimeProperty` and friends will be taken + * into account when used on JAX-RS classes. + */ + @ConfigItem(defaultValue = "true") + public boolean buildTimeConditionAware; } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java index 9cf67a0feaed9..28a8bfc452e54 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java @@ -271,9 +271,7 @@ public void setupEndpoints(Capabilities capabilities, BeanArchiveIndexBuildItem Map pathInterfaces = result.getPathInterfaces(); ApplicationScanningResult appResult = applicationResultBuildItem.getResult(); - Set allowedClasses = appResult.getAllowedClasses(); Set singletonClasses = appResult.getSingletonClasses(); - boolean filterClasses = appResult.isFilterClasses(); Application application = appResult.getApplication(); Map existingConverters = new HashMap<>(); @@ -382,7 +380,7 @@ private boolean hasAnnotation(MethodInfo method, short paramPosition, DotName an serverEndpointIndexer = serverEndpointIndexerBuilder.build(); for (ClassInfo i : scannedResources.values()) { - if (filterClasses && !allowedClasses.contains(i.name().toString())) { + if (!appResult.keepClass(i.name().toString())) { continue; } ResourceClass endpoints = serverEndpointIndexer.createEndpoints(i); diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/simple/BuildProfileTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/simple/BuildProfileTest.java new file mode 100644 index 0000000000000..ba1a6bce17a25 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/simple/BuildProfileTest.java @@ -0,0 +1,240 @@ +package io.quarkus.resteasy.reactive.server.test.simple; + +import static io.restassured.RestAssured.when; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + +import java.io.IOException; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerResponseContext; +import javax.ws.rs.container.ContainerResponseFilter; +import javax.ws.rs.container.DynamicFeature; +import javax.ws.rs.container.ResourceInfo; +import javax.ws.rs.core.Feature; +import javax.ws.rs.core.FeatureContext; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.profile.IfBuildProfile; +import io.quarkus.arc.profile.UnlessBuildProfile; +import io.quarkus.arc.properties.IfBuildProperty; +import io.quarkus.arc.properties.UnlessBuildProperty; +import io.quarkus.test.QuarkusUnitTest; + +/** + * The integration test for the support of build time conditions on JAX-RS resource classes. + */ +class BuildProfileTest { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses( + ResourceTest1.class, ResourceTest2.class, ResponseFilter1.class, ResponseFilter2.class, + ResponseFilter3.class, ResponseFilter4.class, ResponseFilter5.class, ResponseFilter6.class, + Feature1.class, Feature2.class, DynamicFeature1.class, DynamicFeature2.class, + ExceptionMapper1.class, ExceptionMapper2.class)) + .overrideConfigKey("some.prop1", "v1") + .overrideConfigKey("some.prop2", "v2"); + + @DisplayName("Should access to ok of resource 1 and provide a response with the expected headers") + @Test + void should_call_ok_of_resource_1() { + when() + .get("/rt-1/ok") + .then() + .header("X-RF-1", notNullValue()) + .header("X-RF-2", nullValue()) + .header("X-RF-3", notNullValue()) + .header("X-RF-4", nullValue()) + .header("X-RF-5", notNullValue()) + .header("X-RF-6", nullValue()) + .body(Matchers.is("ok1")); + } + + @DisplayName("Should access to ko of resource 1 and call the expected exception mapper") + @Test + void should_call_ko_of_resource_1() { + when() + .get("/rt-1/ko") + .then() + .statusCode(Response.Status.SERVICE_UNAVAILABLE.getStatusCode()); + } + + @DisplayName("Should access to ok of resource 1 and provide a response with the expected headers") + @Test + void should_not_call_ok_of_resource_2() { + when() + .get("/rt-2/ok") + .then() + .statusCode(Response.Status.SERVICE_UNAVAILABLE.getStatusCode()); + } + + @IfBuildProfile("test") + @Path("rt-1") + public static class ResourceTest1 { + + @GET + @Path("ok") + public String ok() { + return "ok1"; + } + + @GET + @Path("ko") + public String ko() { + throw new UnsupportedOperationException(); + } + } + + @IfBuildProfile("foo") + @Path("rt-2") + public static class ResourceTest2 { + + @GET + @Path("ok") + public String ok() { + return "ok2"; + } + } + + @IfBuildProperty(name = "some.prop1", stringValue = "v1") // will be enabled because the value matches + @Provider + public static class ResponseFilter1 implements ContainerResponseFilter { + + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) + throws IOException { + responseContext.getHeaders().add("X-RF-1", "Value"); + } + } + + @IfBuildProperty(name = "some.prop1", stringValue = "v") // won't be enabled because the value doesn't match + @Provider + public static class ResponseFilter2 implements ContainerResponseFilter { + + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) + throws IOException { + responseContext.getHeaders().add("X-RF-2", "Value"); + } + } + + @IfBuildProfile("test") + @UnlessBuildProperty(name = "some.prop2", stringValue = "v1") // will be enabled because the value doesn't match + @Provider + public static class ResponseFilter3 implements ContainerResponseFilter { + + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) + throws IOException { + responseContext.getHeaders().add("X-RF-3", "Value"); + } + } + + @UnlessBuildProperty(name = "some.prop2", stringValue = "v2") // won't be enabled because the value matches + @Provider + public static class ResponseFilter4 implements ContainerResponseFilter { + + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) + throws IOException { + responseContext.getHeaders().add("X-RF-4", "Value"); + } + } + + @IfBuildProfile("test") + @Provider + public static class ResponseFilter5 implements ContainerResponseFilter { + + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) + throws IOException { + responseContext.getHeaders().add("X-RF-5", "Value"); + } + } + + @IfBuildProfile("bar") + @Provider + public static class ResponseFilter6 implements ContainerResponseFilter { + + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) + throws IOException { + responseContext.getHeaders().add("X-RF-6", "Value"); + } + } + + @IfBuildProfile("test") + @Provider + public static class Feature1 implements Feature { + + @Override + public boolean configure(FeatureContext context) { + context.register(ResponseFilter3.class); + return true; + } + } + + @UnlessBuildProfile("test") + @Provider + public static class Feature2 implements Feature { + + @Override + public boolean configure(FeatureContext context) { + context.register(ResponseFilter4.class); + return true; + } + } + + @IfBuildProfile("test") + @Provider + public static class ExceptionMapper1 implements ExceptionMapper { + + @Override + public Response toResponse(RuntimeException exception) { + return Response.status(Response.Status.SERVICE_UNAVAILABLE.getStatusCode()).build(); + } + } + + @UnlessBuildProfile("test") + @Provider + public static class ExceptionMapper2 implements ExceptionMapper { + + @Override + public Response toResponse(UnsupportedOperationException exception) { + return Response.status(Response.Status.NOT_IMPLEMENTED.getStatusCode()).build(); + } + } + + @IfBuildProfile("test") + @Provider + public static class DynamicFeature1 implements DynamicFeature { + + @Override + public void configure(ResourceInfo resourceInfo, FeatureContext context) { + context.register(ResponseFilter5.class); + } + } + + @IfBuildProfile("bar") + @Provider + public static class DynamicFeature2 implements DynamicFeature { + + @Override + public void configure(ResourceInfo resourceInfo, FeatureContext context) { + context.register(ResponseFilter6.class); + } + } +} diff --git a/extensions/resteasy-server-common/deployment/src/main/java/io/quarkus/resteasy/server/common/deployment/ResteasyServerCommonProcessor.java b/extensions/resteasy-server-common/deployment/src/main/java/io/quarkus/resteasy/server/common/deployment/ResteasyServerCommonProcessor.java index 2da65f52d5a19..f7c82dac4244f 100755 --- a/extensions/resteasy-server-common/deployment/src/main/java/io/quarkus/resteasy/server/common/deployment/ResteasyServerCommonProcessor.java +++ b/extensions/resteasy-server-common/deployment/src/main/java/io/quarkus/resteasy/server/common/deployment/ResteasyServerCommonProcessor.java @@ -48,6 +48,7 @@ import io.quarkus.arc.deployment.AutoInjectAnnotationBuildItem; import io.quarkus.arc.deployment.BeanArchiveIndexBuildItem; import io.quarkus.arc.deployment.BeanDefiningAnnotationBuildItem; +import io.quarkus.arc.deployment.BuildTimeConditionBuildItem; import io.quarkus.arc.deployment.CustomScopeAnnotationsBuildItem; import io.quarkus.arc.deployment.UnremovableBeanBuildItem; import io.quarkus.arc.deployment.UnremovableBeanBuildItem.BeanClassNameExclusion; @@ -155,11 +156,18 @@ static final class ResteasyConfig { public boolean metricsEnabled; /** - * Ignore all explict JAX-RS {@link Application} classes. + * Ignore all explicit JAX-RS {@link Application} classes. * As multiple JAX-RS applications are not supported, this can be used to effectively merge all JAX-RS applications. */ @ConfigItem(defaultValue = "false") boolean ignoreApplicationClasses; + + /** + * Whether or not annotations such `@IfBuildTimeProfile`, `@IfBuildTimeProperty` and friends will be taken + * into account when used on JAX-RS classes. + */ + @ConfigItem(defaultValue = "true") + boolean buildTimeConditionAware; } @BuildStep @@ -179,6 +187,7 @@ public void build( BuildProducer resteasyDeployment, BuildProducer unremovableBeans, BuildProducer annotationsTransformer, + List buildTimeConditions, List autoInjectAnnotations, List additionalJaxRsResourceDefiningAnnotations, List additionalJaxRsResourceMethodAnnotations, @@ -194,15 +203,22 @@ public void build( Collection applicationPaths = Collections.emptySet(); final Set allowedClasses; + final Set excludedClasses; + if (resteasyConfig.buildTimeConditionAware) { + excludedClasses = getExcludedClasses(buildTimeConditions); + } else { + excludedClasses = Collections.emptySet(); + } if (resteasyConfig.ignoreApplicationClasses) { allowedClasses = Collections.emptySet(); } else { applicationPaths = index.getAnnotations(ResteasyDotNames.APPLICATION_PATH); allowedClasses = getAllowedClasses(index); jaxrsProvidersToRegisterBuildItem = getFilteredJaxrsProvidersToRegisterBuildItem( - jaxrsProvidersToRegisterBuildItem, allowedClasses); + jaxrsProvidersToRegisterBuildItem, allowedClasses, excludedClasses); } - boolean filterClasses = !allowedClasses.isEmpty(); + + boolean filterClasses = !allowedClasses.isEmpty() || !excludedClasses.isEmpty(); // currently we only examine the first class that is annotated with @ApplicationPath so best // fail if the user code has multiple such annotations instead of surprising the user @@ -220,8 +236,7 @@ public void build( final Collection allPaths; if (filterClasses) { allPaths = paths.stream().filter( - annotationInstance -> allowedClasses - .contains(JandexUtil.getEnclosingClass(annotationInstance).name().toString())) + annotationInstance -> keepEnclosingClass(allowedClasses, excludedClasses, annotationInstance)) .collect(Collectors.toList()); } else { allPaths = new ArrayList<>(paths); @@ -859,9 +874,44 @@ private static RuntimeException createMultipleApplicationsException(Collection getExcludedClasses(List buildTimeConditions) { + return buildTimeConditions.stream() + .filter(item -> !item.isEnabled()) + .map(BuildTimeConditionBuildItem::getTarget) + .filter(target -> target.kind() == Kind.CLASS) + .map(target -> target.asClass().toString()) + .collect(Collectors.toSet()); + } + /** * @param allowedClasses the classes returned by the methods {@link Application#getClasses()} and * {@link Application#getSingletons()} to keep. + * @param excludedClasses the classes that have been annotated wih unsuccessful build time conditions and that + * need to be excluded from the list of paths. + * @param annotationInstance the annotation instance from which the enclosing class will be extracted. + * @return {@code true} if the enclosing class of the annotation is part of the allowed classes if not empty + * or if is not part of the excluded classes, {@code false} otherwise. + */ + private static boolean keepEnclosingClass(Set allowedClasses, Set excludedClasses, + AnnotationInstance annotationInstance) { + final String className = JandexUtil.getEnclosingClass(annotationInstance).toString(); + if (allowedClasses.isEmpty()) { + // No allowed classes have been set, meaning that only excluded classes have been provided. + // Keep the enclosing class only if not excluded + return !excludedClasses.contains(className); + } + return allowedClasses.contains(className); + } + + /** + * @param allowedClasses the classes returned by the methods {@link Application#getClasses()} and + * {@link Application#getSingletons()} to keep. + * @param excludedClasses the classes that have been annotated wih unsuccessful build time conditions and that + * need to be excluded from the list of providers. * @param jaxrsProvidersToRegisterBuildItem the initial {@code jaxrsProvidersToRegisterBuildItem} before being * filtered * @return an instance of {@link JaxrsProvidersToRegisterBuildItem} that has been filtered to take into account @@ -870,9 +920,10 @@ private static RuntimeException createMultipleApplicationsException(Collection allowedClasses) { + JaxrsProvidersToRegisterBuildItem jaxrsProvidersToRegisterBuildItem, Set allowedClasses, + Set excludedClasses) { - if (allowedClasses.isEmpty()) { + if (allowedClasses.isEmpty() && excludedClasses.isEmpty()) { return jaxrsProvidersToRegisterBuildItem; } Set providers = new HashSet<>(jaxrsProvidersToRegisterBuildItem.getProviders()); @@ -880,7 +931,11 @@ private static JaxrsProvidersToRegisterBuildItem getFilteredJaxrsProvidersToRegi Set annotatedProviders = new HashSet<>(jaxrsProvidersToRegisterBuildItem.getAnnotatedProviders()); providers.removeAll(annotatedProviders); contributedProviders.removeAll(annotatedProviders); - annotatedProviders.retainAll(allowedClasses); + if (allowedClasses.isEmpty()) { + annotatedProviders.removeAll(excludedClasses); + } else { + annotatedProviders.retainAll(allowedClasses); + } providers.addAll(annotatedProviders); contributedProviders.addAll(annotatedProviders); return new JaxrsProvidersToRegisterBuildItem( diff --git a/extensions/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/root/BuildProfileTest.java b/extensions/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/root/BuildProfileTest.java new file mode 100644 index 0000000000000..067427b0fcaf8 --- /dev/null +++ b/extensions/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/root/BuildProfileTest.java @@ -0,0 +1,240 @@ +package io.quarkus.resteasy.test.root; + +import static io.restassured.RestAssured.when; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + +import java.io.IOException; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerResponseContext; +import javax.ws.rs.container.ContainerResponseFilter; +import javax.ws.rs.container.DynamicFeature; +import javax.ws.rs.container.ResourceInfo; +import javax.ws.rs.core.Feature; +import javax.ws.rs.core.FeatureContext; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.profile.IfBuildProfile; +import io.quarkus.arc.profile.UnlessBuildProfile; +import io.quarkus.arc.properties.IfBuildProperty; +import io.quarkus.arc.properties.UnlessBuildProperty; +import io.quarkus.test.QuarkusUnitTest; + +/** + * The integration test for the support of build time conditions on JAX-RS resource classes. + */ +class BuildProfileTest { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses( + ResourceTest1.class, ResourceTest2.class, ResponseFilter1.class, ResponseFilter2.class, + ResponseFilter3.class, ResponseFilter4.class, ResponseFilter5.class, ResponseFilter6.class, + Feature1.class, Feature2.class, DynamicFeature1.class, DynamicFeature2.class, + ExceptionMapper1.class, ExceptionMapper2.class)) + .overrideConfigKey("some.prop1", "v1") + .overrideConfigKey("some.prop2", "v2"); + + @DisplayName("Should access to ok of resource 1 and provide a response with the expected headers") + @Test + void should_call_ok_of_resource_1() { + when() + .get("/rt-1/ok") + .then() + .header("X-RF-1", notNullValue()) + .header("X-RF-2", nullValue()) + .header("X-RF-3", notNullValue()) + .header("X-RF-4", nullValue()) + .header("X-RF-5", notNullValue()) + .header("X-RF-6", nullValue()) + .body(Matchers.is("ok1")); + } + + @DisplayName("Should access to ko of resource 1 and call the expected exception mapper") + @Test + void should_call_ko_of_resource_1() { + when() + .get("/rt-1/ko") + .then() + .statusCode(Response.Status.SERVICE_UNAVAILABLE.getStatusCode()); + } + + @DisplayName("Should access to ok of resource 1 and provide a response with the expected headers") + @Test + void should_not_call_ok_of_resource_2() { + when() + .get("/rt-2/ok") + .then() + .statusCode(Response.Status.SERVICE_UNAVAILABLE.getStatusCode()); + } + + @IfBuildProfile("test") + @Path("rt-1") + public static class ResourceTest1 { + + @GET + @Path("ok") + public String ok() { + return "ok1"; + } + + @GET + @Path("ko") + public String ko() { + throw new UnsupportedOperationException(); + } + } + + @IfBuildProfile("foo") + @Path("rt-2") + public static class ResourceTest2 { + + @GET + @Path("ok") + public String ok() { + return "ok2"; + } + } + + @IfBuildProperty(name = "some.prop1", stringValue = "v1") // will be enabled because the value matches + @Provider + public static class ResponseFilter1 implements ContainerResponseFilter { + + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) + throws IOException { + responseContext.getHeaders().add("X-RF-1", "Value"); + } + } + + @IfBuildProperty(name = "some.prop1", stringValue = "v") // won't be enabled because the value doesn't match + @Provider + public static class ResponseFilter2 implements ContainerResponseFilter { + + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) + throws IOException { + responseContext.getHeaders().add("X-RF-2", "Value"); + } + } + + @IfBuildProfile("test") + @UnlessBuildProperty(name = "some.prop2", stringValue = "v1") // will be enabled because the value doesn't match + @Provider + public static class ResponseFilter3 implements ContainerResponseFilter { + + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) + throws IOException { + responseContext.getHeaders().add("X-RF-3", "Value"); + } + } + + @UnlessBuildProperty(name = "some.prop2", stringValue = "v2") // won't be enabled because the value matches + @Provider + public static class ResponseFilter4 implements ContainerResponseFilter { + + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) + throws IOException { + responseContext.getHeaders().add("X-RF-4", "Value"); + } + } + + @IfBuildProfile("test") + @Provider + public static class ResponseFilter5 implements ContainerResponseFilter { + + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) + throws IOException { + responseContext.getHeaders().add("X-RF-5", "Value"); + } + } + + @IfBuildProfile("bar") + @Provider + public static class ResponseFilter6 implements ContainerResponseFilter { + + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) + throws IOException { + responseContext.getHeaders().add("X-RF-6", "Value"); + } + } + + @IfBuildProfile("test") + @Provider + public static class Feature1 implements Feature { + + @Override + public boolean configure(FeatureContext context) { + context.register(ResponseFilter3.class); + return true; + } + } + + @UnlessBuildProfile("test") + @Provider + public static class Feature2 implements Feature { + + @Override + public boolean configure(FeatureContext context) { + context.register(ResponseFilter4.class); + return true; + } + } + + @IfBuildProfile("test") + @Provider + public static class ExceptionMapper1 implements ExceptionMapper { + + @Override + public Response toResponse(RuntimeException exception) { + return Response.status(Response.Status.SERVICE_UNAVAILABLE.getStatusCode()).build(); + } + } + + @UnlessBuildProfile("test") + @Provider + public static class ExceptionMapper2 implements ExceptionMapper { + + @Override + public Response toResponse(UnsupportedOperationException exception) { + return Response.status(Response.Status.NOT_IMPLEMENTED.getStatusCode()).build(); + } + } + + @IfBuildProfile("test") + @Provider + public static class DynamicFeature1 implements DynamicFeature { + + @Override + public void configure(ResourceInfo resourceInfo, FeatureContext context) { + context.register(ResponseFilter5.class); + } + } + + @IfBuildProfile("bar") + @Provider + public static class DynamicFeature2 implements DynamicFeature { + + @Override + public void configure(ResourceInfo resourceInfo, FeatureContext context) { + context.register(ResponseFilter6.class); + } + } +} diff --git a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/scanning/ApplicationScanningResult.java b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/scanning/ApplicationScanningResult.java index 9aa4dc568fcd8..0761609485ffb 100644 --- a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/scanning/ApplicationScanningResult.java +++ b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/scanning/ApplicationScanningResult.java @@ -9,16 +9,19 @@ public final class ApplicationScanningResult { final Set allowedClasses; final Set singletonClasses; + final Set excludedClasses; final Set globalNameBindings; final boolean filterClasses; final Application application; final ClassInfo selectedAppClass; final boolean blocking; - public ApplicationScanningResult(Set allowedClasses, Set singletonClasses, Set globalNameBindings, - boolean filterClasses, Application application, ClassInfo selectedAppClass, boolean blocking) { + public ApplicationScanningResult(Set allowedClasses, Set singletonClasses, Set excludedClasses, + Set globalNameBindings, boolean filterClasses, Application application, + ClassInfo selectedAppClass, boolean blocking) { this.allowedClasses = allowedClasses; this.singletonClasses = singletonClasses; + this.excludedClasses = excludedClasses; this.globalNameBindings = globalNameBindings; this.filterClasses = filterClasses; this.application = application; @@ -29,6 +32,11 @@ public ApplicationScanningResult(Set allowedClasses, Set singlet public KeepProviderResult keepProvider(ClassInfo providerClass) { if (filterClasses) { // we don't care about provider annotations, they're manually registered (but for the server only) + if (allowedClasses.isEmpty()) { + // we only have only classes to exclude + return excludedClasses.contains(providerClass.name().toString()) ? KeepProviderResult.DISCARD + : KeepProviderResult.SERVER_ONLY; + } return allowedClasses.contains(providerClass.name().toString()) ? KeepProviderResult.SERVER_ONLY : KeepProviderResult.DISCARD; } @@ -36,10 +44,25 @@ public KeepProviderResult keepProvider(ClassInfo providerClass) { : KeepProviderResult.DISCARD; } + public boolean keepClass(String className) { + if (filterClasses) { + if (allowedClasses.isEmpty()) { + // we only have only classes to exclude + return !excludedClasses.contains(className); + } + return allowedClasses.contains(className); + } + return true; + } + public Set getAllowedClasses() { return allowedClasses; } + public Set getExcludedClasses() { + return excludedClasses; + } + public Set getSingletonClasses() { return singletonClasses; } diff --git a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/scanning/ResteasyReactiveScanner.java b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/scanning/ResteasyReactiveScanner.java index 7b213a3d6590d..14d0d9915d99a 100644 --- a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/scanning/ResteasyReactiveScanner.java +++ b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/scanning/ResteasyReactiveScanner.java @@ -45,13 +45,13 @@ public class ResteasyReactiveScanner { BUILTIN_HTTP_ANNOTATIONS_TO_METHOD = Collections.unmodifiableMap(BUILTIN_HTTP_ANNOTATIONS_TO_METHOD); } - public static ApplicationScanningResult scanForApplicationClass(IndexView index) { + public static ApplicationScanningResult scanForApplicationClass(IndexView index, Set excludedClasses) { Collection applications = index .getAllKnownSubclasses(ResteasyReactiveDotNames.APPLICATION); Set allowedClasses = new HashSet<>(); Set singletonClasses = new HashSet<>(); Set globalNameBindings = new HashSet<>(); - boolean filterClasses = false; + boolean filterClasses = !excludedClasses.isEmpty(); Application application = null; ClassInfo selectedAppClass = null; boolean blocking = false; @@ -93,7 +93,8 @@ public static ApplicationScanningResult scanForApplicationClass(IndexView index) if (selectedAppClass != null) { globalNameBindings = NameBindingUtil.nameBindingNames(index, selectedAppClass); } - return new ApplicationScanningResult(allowedClasses, singletonClasses, globalNameBindings, filterClasses, application, + return new ApplicationScanningResult(allowedClasses, singletonClasses, excludedClasses, globalNameBindings, + filterClasses, application, selectedAppClass, blocking); } diff --git a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/framework/ResteasyReactiveUnitTest.java b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/framework/ResteasyReactiveUnitTest.java index 2d544e8a1f9c6..b7715ed884e60 100644 --- a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/framework/ResteasyReactiveUnitTest.java +++ b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/framework/ResteasyReactiveUnitTest.java @@ -207,7 +207,8 @@ public void close() throws Throwable { } Index index = JandexUtil.createIndex(deploymentDir); - ApplicationScanningResult applicationScanningResult = ResteasyReactiveScanner.scanForApplicationClass(index); + ApplicationScanningResult applicationScanningResult = ResteasyReactiveScanner.scanForApplicationClass(index, + Collections.emptySet()); ResourceScanningResult resources = ResteasyReactiveScanner.scanResources(index); if (resources == null) { throw new RuntimeException("no JAX-RS resources found");