From ba68e3fbb8226ffd5bfc22db0c9f99b6e023a327 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 --- docs/src/main/asciidoc/rest-json.adoc | 22 ++ docs/src/main/asciidoc/resteasy-reactive.adoc | 22 ++ .../deployment/BuildTimeEnabledProcessor.java | 6 +- .../ResteasyReactiveCommonProcessor.java | 23 +- .../runtime/ResteasyReactiveConfig.java | 6 + .../deployment/ResteasyReactiveProcessor.java | 4 +- .../server/test/simple/BuildProfileTest.java | 240 ++++++++++++++++++ .../ResteasyServerCommonProcessor.java | 70 ++++- .../resteasy/test/root/BuildProfileTest.java | 240 ++++++++++++++++++ .../scanning/ApplicationScanningResult.java | 27 +- .../scanning/ResteasyReactiveScanner.java | 7 +- .../framework/ResteasyReactiveUnitTest.java | 3 +- 12 files changed, 648 insertions(+), 22 deletions(-) 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..c87bcfa1fce8b 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 allows to include or exclude root resource, provider and feature classes directly thanks to build time conditions like it is possible with CDI beans. +Indeed, the different 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 the 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..581472514d3e7 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 allows to include or exclude root resource, provider and feature classes directly thanks to build time conditions like it is possible with CDI beans. +Indeed, the different 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 the 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..b997388f016fb 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 @@ -72,7 +72,7 @@ void unlessBuildProfile(CombinedIndexBuildItem index, BuildProducer producer) { + void ifBuildProperty(CombinedIndexBuildItem index, BuildProducer producer) { buildProperty(IF_BUILD_PROPERTY, new BiFunction() { @Override public Boolean apply(String stringValue, String expectedStringValue) { @@ -82,7 +82,7 @@ public Boolean apply(String stringValue, String expectedStringValue) { } @BuildStep - void unlessBuildProperty(BeanArchiveIndexBuildItem index, BuildProducer producer) { + void unlessBuildProperty(CombinedIndexBuildItem index, BuildProducer producer) { buildProperty(UNLESS_BUILD_PROPERTY, new BiFunction() { @Override public Boolean apply(String stringValue, String expectedStringValue) { @@ -91,7 +91,7 @@ public Boolean apply(String stringValue, String expectedStringValue) { }, index, producer); } - void buildProperty(DotName annotationName, BiFunction testFun, BeanArchiveIndexBuildItem index, + void buildProperty(DotName annotationName, BiFunction testFun, CombinedIndexBuildItem index, BuildProducer producer) { Config config = ConfigProviderResolver.instance().getConfig(); Collection annotationInstances = index.getIndex().getAnnotations(annotationName); 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..1538c016bb0a7 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.BuildTimeConditionBuildItem; 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,12 @@ void setUpDenyAllJaxRs(CombinedIndexBuildItem index, @BuildStep ApplicationResultBuildItem handleApplication(CombinedIndexBuildItem combinedIndexBuildItem, - BuildProducer reflectiveClass) { + BuildProducer reflectiveClass, + List buildTimeConditions, + ResteasyReactiveConfig config) { 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 +275,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(BuildTimeConditionBuildItem::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..33444b9ae1051 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,10 @@ public class ResteasyReactiveConfig { @ConfigItem(defaultValue = "true") @Experimental("This flag has a high probability of going away in the future") public boolean defaultProduces; + + /** + * Allow to rely on build time conditions to enable or not the JAX-RS resource, provider and feature 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..e5ffc1a62449e 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,17 @@ 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; + + /** + * Allow to rely on build time conditions to enable or not the JAX-RS resource, provider and feature classes. + */ + @ConfigItem(defaultValue = "true") + boolean buildTimeConditionAware; } @BuildStep @@ -179,6 +186,7 @@ public void build( BuildProducer resteasyDeployment, BuildProducer unremovableBeans, BuildProducer annotationsTransformer, + List buildTimeConditions, List autoInjectAnnotations, List additionalJaxRsResourceDefiningAnnotations, List additionalJaxRsResourceMethodAnnotations, @@ -194,15 +202,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 +235,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 +873,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 +919,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 +930,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");