From 901d53f9fed13e5dda8436a47cc54916f7ae4728 Mon Sep 17 00:00:00 2001 From: essobedo <nicolas_filotto@ultimatesoftware.com> Date: Sun, 10 Jan 2021 12:04:40 +0100 Subject: [PATCH] Add partial support of JAX-RS Application in resteasy extension --- docs/src/main/asciidoc/rest-json.adoc | 6 + .../JaxrsProvidersToRegisterBuildItem.java | 9 +- .../deployment/ResteasyCommonProcessor.java | 8 +- .../resteasy/common/spi/ResteasyDotNames.java | 1 + .../server/test/simple/ApplicationTest.java | 237 ++++++++++++++++++ .../ResteasyServerCommonProcessor.java | 92 ++++++- .../resteasy/test/root/ApplicationTest.java | 237 ++++++++++++++++++ 7 files changed, 582 insertions(+), 8 deletions(-) create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/simple/ApplicationTest.java create mode 100644 extensions/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/root/ApplicationTest.java diff --git a/docs/src/main/asciidoc/rest-json.adoc b/docs/src/main/asciidoc/rest-json.adoc index 35e4f63a3a166..849d088182e35 100644 --- a/docs/src/main/asciidoc/rest-json.adoc +++ b/docs/src/main/asciidoc/rest-json.adoc @@ -583,6 +583,12 @@ If multiple JAX-RS `Application` classes are defined, the build will fail with t If multiple JAX-RS applications are defined, the property `quarkus.resteasy.ignoreApplicationClasses=true` can be used to ignore all explicit `Application` classes. This makes all resource-classes available via the application-path as defined by `quarkus.resteasy.path` (default: `/`). +=== Support limitations of JAX-RS application + +The RESTEasy extension doesn't support the method `getProperties()` of the class `javax.ws.rs.core.Application`. Moreover, it only relies on the methods `getClasses()` and `getSingletons()` to filter out the annotated resource, provider and feature classes. +It doesn't filter out the built-in resource, provider and feature classes and also the resource, provider and feature classes registered by the other extensions. +Finally the objects returned by the method `getSingletons()` are ignored, only the classes are took into account to filter out the resource, provider and feature classes, in other words the method `getSingletons()` is actually managed the same way as `getClasses()`. + === Lifecycle of Resources In Quarkus all JAX-RS resources are treated as CDI beans. diff --git a/extensions/resteasy-common/deployment/src/main/java/io/quarkus/resteasy/common/deployment/JaxrsProvidersToRegisterBuildItem.java b/extensions/resteasy-common/deployment/src/main/java/io/quarkus/resteasy/common/deployment/JaxrsProvidersToRegisterBuildItem.java index 183e4892ebdc0..75f4118001a83 100644 --- a/extensions/resteasy-common/deployment/src/main/java/io/quarkus/resteasy/common/deployment/JaxrsProvidersToRegisterBuildItem.java +++ b/extensions/resteasy-common/deployment/src/main/java/io/quarkus/resteasy/common/deployment/JaxrsProvidersToRegisterBuildItem.java @@ -8,11 +8,14 @@ public final class JaxrsProvidersToRegisterBuildItem extends SimpleBuildItem { private final Set<String> providers; private final Set<String> contributedProviders; + private final Set<String> annotatedProviders; private final boolean useBuiltIn; - public JaxrsProvidersToRegisterBuildItem(Set<String> providers, Set<String> contributedProviders, boolean useBuiltIn) { + public JaxrsProvidersToRegisterBuildItem(Set<String> providers, Set<String> contributedProviders, + Set<String> annotatedProviders, boolean useBuiltIn) { this.providers = providers; this.contributedProviders = contributedProviders; + this.annotatedProviders = annotatedProviders; this.useBuiltIn = useBuiltIn; } @@ -24,6 +27,10 @@ public Set<String> getContributedProviders() { return this.contributedProviders; } + public Set<String> getAnnotatedProviders() { + return annotatedProviders; + } + public boolean useBuiltIn() { return useBuiltIn; } diff --git a/extensions/resteasy-common/deployment/src/main/java/io/quarkus/resteasy/common/deployment/ResteasyCommonProcessor.java b/extensions/resteasy-common/deployment/src/main/java/io/quarkus/resteasy/common/deployment/ResteasyCommonProcessor.java index be6a952417e85..61d12fa821839 100644 --- a/extensions/resteasy-common/deployment/src/main/java/io/quarkus/resteasy/common/deployment/ResteasyCommonProcessor.java +++ b/extensions/resteasy-common/deployment/src/main/java/io/quarkus/resteasy/common/deployment/ResteasyCommonProcessor.java @@ -162,14 +162,15 @@ JaxrsProvidersToRegisterBuildItem setupProviders(BuildProducer<ReflectiveClassBu contributedProviders.add(contributedProviderBuildItem.getName()); } + Set<String> annotatedProviders = new HashSet<>(); for (AnnotationInstance i : indexBuildItem.getIndex().getAnnotations(ResteasyDotNames.PROVIDER)) { if (i.target().kind() == AnnotationTarget.Kind.CLASS) { - contributedProviders.add(i.target().asClass().name().toString()); + annotatedProviders.add(i.target().asClass().name().toString()); } checkProperConfigAccessInProvider(i); checkProperConstructorInProvider(i); } - + contributedProviders.addAll(annotatedProviders); Set<String> availableProviders = new HashSet<>(ServiceUtil.classNamesNamedIn(getClass().getClassLoader(), "META-INF/services/" + Providers.class.getName())); // this one is added manually in RESTEasy's ResteasyDeploymentImpl @@ -236,7 +237,8 @@ JaxrsProvidersToRegisterBuildItem setupProviders(BuildProducer<ReflectiveClassBu "org.jboss.resteasy.plugins.providers.jsonb.AbstractJsonBindingProvider")); } - return new JaxrsProvidersToRegisterBuildItem(providersToRegister, contributedProviders, useBuiltinProviders); + return new JaxrsProvidersToRegisterBuildItem( + providersToRegister, contributedProviders, annotatedProviders, useBuiltinProviders); } private String mutinySupportNeeded(CombinedIndexBuildItem indexBuildItem) { diff --git a/extensions/resteasy-common/spi/src/main/java/io/quarkus/resteasy/common/spi/ResteasyDotNames.java b/extensions/resteasy-common/spi/src/main/java/io/quarkus/resteasy/common/spi/ResteasyDotNames.java index 91db4862bffa1..15c09880d4d93 100644 --- a/extensions/resteasy-common/spi/src/main/java/io/quarkus/resteasy/common/spi/ResteasyDotNames.java +++ b/extensions/resteasy-common/spi/src/main/java/io/quarkus/resteasy/common/spi/ResteasyDotNames.java @@ -16,6 +16,7 @@ public final class ResteasyDotNames { + public static final DotName APPLICATION = DotName.createSimple("javax.ws.rs.core.Application"); public static final DotName CONSUMES = DotName.createSimple("javax.ws.rs.Consumes"); public static final DotName PRODUCES = DotName.createSimple("javax.ws.rs.Produces"); public static final DotName PROVIDER = DotName.createSimple("javax.ws.rs.ext.Provider"); diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/simple/ApplicationTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/simple/ApplicationTest.java new file mode 100644 index 0000000000000..ea65f4df68d5c --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/simple/ApplicationTest.java @@ -0,0 +1,237 @@ +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 java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.container.*; +import javax.ws.rs.core.Application; +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.test.QuarkusUnitTest; + +/** + * The integration test allowing to ensure that we can rely on {@link Application#getClasses()} to specify explicitly + * the classes to use for the application. + */ +class ApplicationTest { + + @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, AppTest.class)); + + @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()); + } + + @Path("rt-1") + public static class ResourceTest1 { + + @GET + @Path("ok") + public String ok() { + return "ok1"; + } + + @GET + @Path("ko") + public String ko() { + throw new UnsupportedOperationException(); + } + } + + @Path("rt-2") + public static class ResourceTest2 { + + @GET + @Path("ok") + public String ok() { + return "ok2"; + } + } + + @Provider + public static class ResponseFilter1 implements ContainerResponseFilter { + + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) + throws IOException { + responseContext.getHeaders().add("X-RF-1", "Value"); + } + } + + @Provider + public static class ResponseFilter2 implements ContainerResponseFilter { + + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) + throws IOException { + responseContext.getHeaders().add("X-RF-2", "Value"); + } + } + + @Provider + public static class ResponseFilter3 implements ContainerResponseFilter { + + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) + throws IOException { + responseContext.getHeaders().add("X-RF-3", "Value"); + } + } + + @Provider + public static class ResponseFilter4 implements ContainerResponseFilter { + + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) + throws IOException { + responseContext.getHeaders().add("X-RF-4", "Value"); + } + } + + @Provider + public static class ResponseFilter5 implements ContainerResponseFilter { + + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) + throws IOException { + responseContext.getHeaders().add("X-RF-5", "Value"); + } + } + + @Provider + public static class ResponseFilter6 implements ContainerResponseFilter { + + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) + throws IOException { + responseContext.getHeaders().add("X-RF-6", "Value"); + } + } + + @Provider + public static class Feature1 implements Feature { + + @Override + public boolean configure(FeatureContext context) { + context.register(ResponseFilter3.class); + return true; + } + } + + @Provider + public static class Feature2 implements Feature { + + @Override + public boolean configure(FeatureContext context) { + context.register(ResponseFilter4.class); + return true; + } + } + + @Provider + public static class ExceptionMapper1 implements ExceptionMapper<RuntimeException> { + + @Override + public Response toResponse(RuntimeException exception) { + return Response.status(Response.Status.SERVICE_UNAVAILABLE.getStatusCode()).build(); + } + } + + @Provider + public static class ExceptionMapper2 implements ExceptionMapper<UnsupportedOperationException> { + + @Override + public Response toResponse(UnsupportedOperationException exception) { + return Response.status(Response.Status.NOT_IMPLEMENTED.getStatusCode()).build(); + } + } + + @Provider + public static class DynamicFeature1 implements DynamicFeature { + + @Override + public void configure(ResourceInfo resourceInfo, FeatureContext context) { + context.register(ResponseFilter5.class); + } + } + + @Provider + public static class DynamicFeature2 implements DynamicFeature { + + @Override + public void configure(ResourceInfo resourceInfo, FeatureContext context) { + context.register(ResponseFilter6.class); + } + } + + public static class AppTest extends Application { + + @Override + public Set<Class<?>> getClasses() { + return new HashSet<>( + Arrays.asList( + ResourceTest1.class, Feature1.class, ExceptionMapper1.class)); + } + + @Override + public Set<Object> getSingletons() { + return new HashSet<>( + Arrays.asList( + new ResponseFilter1(), new DynamicFeature1())); + } + } +} 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 26c829286a2af..2da65f52d5a19 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 @@ -2,6 +2,7 @@ import static io.quarkus.runtime.annotations.ConfigPhase.BUILD_TIME; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; @@ -62,6 +63,7 @@ import io.quarkus.deployment.builditem.nativeimage.NativeImageProxyDefinitionBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveHierarchyBuildItem; +import io.quarkus.deployment.util.JandexUtil; import io.quarkus.gizmo.Gizmo; import io.quarkus.resteasy.common.deployment.JaxrsProvidersToRegisterBuildItem; import io.quarkus.resteasy.common.deployment.ResteasyCommonProcessor.ResteasyCommonConfig; @@ -191,10 +193,16 @@ public void build( IndexView index = combinedIndexBuildItem.getIndex(); Collection<AnnotationInstance> applicationPaths = Collections.emptySet(); - - if (!resteasyConfig.ignoreApplicationClasses) { + final Set<String> allowedClasses; + if (resteasyConfig.ignoreApplicationClasses) { + allowedClasses = Collections.emptySet(); + } else { applicationPaths = index.getAnnotations(ResteasyDotNames.APPLICATION_PATH); + allowedClasses = getAllowedClasses(index); + jaxrsProvidersToRegisterBuildItem = getFilteredJaxrsProvidersToRegisterBuildItem( + jaxrsProvidersToRegisterBuildItem, allowedClasses); } + boolean filterClasses = !allowedClasses.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 @@ -203,13 +211,21 @@ public void build( throw createMultipleApplicationsException(applicationPaths); } - Collection<AnnotationInstance> paths = beanArchiveIndexBuildItem.getIndex().getAnnotations(ResteasyDotNames.PATH); Set<AnnotationInstance> additionalPaths = new HashSet<>(); for (AdditionalJaxRsResourceDefiningAnnotationBuildItem annotation : additionalJaxRsResourceDefiningAnnotations) { additionalPaths.addAll(beanArchiveIndexBuildItem.getIndex().getAnnotations(annotation.getAnnotationClass())); } - Collection<AnnotationInstance> allPaths = new ArrayList<>(paths); + Collection<AnnotationInstance> paths = beanArchiveIndexBuildItem.getIndex().getAnnotations(ResteasyDotNames.PATH); + final Collection<AnnotationInstance> allPaths; + if (filterClasses) { + allPaths = paths.stream().filter( + annotationInstance -> allowedClasses + .contains(JandexUtil.getEnclosingClass(annotationInstance).name().toString())) + .collect(Collectors.toList()); + } else { + allPaths = new ArrayList<>(paths); + } allPaths.addAll(additionalPaths); if (allPaths.isEmpty()) { @@ -842,4 +858,72 @@ private static RuntimeException createMultipleApplicationsException(Collection<A return new RuntimeException("Multiple classes ( " + sb.toString() + ") have been annotated with @ApplicationPath which is currently not supported"); } + + /** + * @param allowedClasses the classes returned by the methods {@link Application#getClasses()} and + * {@link Application#getSingletons()} to keep. + * @param jaxrsProvidersToRegisterBuildItem the initial {@code jaxrsProvidersToRegisterBuildItem} before being + * filtered + * @return an instance of {@link JaxrsProvidersToRegisterBuildItem} that has been filtered to take into account + * the classes returned by the methods {@link Application#getClasses()} and {@link Application#getSingletons()} + * if at least one of those methods return a non empty {@code Set}, the provided instance of + * {@link JaxrsProvidersToRegisterBuildItem} otherwise. + */ + private static JaxrsProvidersToRegisterBuildItem getFilteredJaxrsProvidersToRegisterBuildItem( + JaxrsProvidersToRegisterBuildItem jaxrsProvidersToRegisterBuildItem, Set<String> allowedClasses) { + + if (allowedClasses.isEmpty()) { + return jaxrsProvidersToRegisterBuildItem; + } + Set<String> providers = new HashSet<>(jaxrsProvidersToRegisterBuildItem.getProviders()); + Set<String> contributedProviders = new HashSet<>(jaxrsProvidersToRegisterBuildItem.getContributedProviders()); + Set<String> annotatedProviders = new HashSet<>(jaxrsProvidersToRegisterBuildItem.getAnnotatedProviders()); + providers.removeAll(annotatedProviders); + contributedProviders.removeAll(annotatedProviders); + annotatedProviders.retainAll(allowedClasses); + providers.addAll(annotatedProviders); + contributedProviders.addAll(annotatedProviders); + return new JaxrsProvidersToRegisterBuildItem( + providers, contributedProviders, annotatedProviders, jaxrsProvidersToRegisterBuildItem.useBuiltIn()); + } + + /** + * @param index the index to use to find the existing {@link Application}. + * @return the set of classes returned by the methods {@link Application#getClasses()} and + * {@link Application#getSingletons()}. + */ + private static Set<String> getAllowedClasses(IndexView index) { + final Collection<ClassInfo> applications = index.getAllKnownSubclasses(ResteasyDotNames.APPLICATION); + final Set<String> allowedClasses = new HashSet<>(); + Application application; + ClassInfo selectedAppClass = null; + for (ClassInfo applicationClassInfo : applications) { + if (selectedAppClass != null) { + throw new RuntimeException("More than one Application class: " + applications); + } + selectedAppClass = applicationClassInfo; + // FIXME: yell if there's more than one + String applicationClass = applicationClassInfo.name().toString(); + try { + Class<?> appClass = Thread.currentThread().getContextClassLoader().loadClass(applicationClass); + application = (Application) appClass.getConstructor().newInstance(); + Set<Class<?>> classes = application.getClasses(); + if (!classes.isEmpty()) { + for (Class<?> klass : classes) { + allowedClasses.add(klass.getName()); + } + } + classes = application.getSingletons().stream().map(Object::getClass).collect(Collectors.toSet()); + if (!classes.isEmpty()) { + for (Class<?> klass : classes) { + allowedClasses.add(klass.getName()); + } + } + } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException + | InvocationTargetException e) { + throw new RuntimeException("Unable to handle class: " + applicationClass, e); + } + } + return allowedClasses; + } } diff --git a/extensions/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/root/ApplicationTest.java b/extensions/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/root/ApplicationTest.java new file mode 100644 index 0000000000000..a8613bd142b14 --- /dev/null +++ b/extensions/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/root/ApplicationTest.java @@ -0,0 +1,237 @@ +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 java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.container.*; +import javax.ws.rs.core.Application; +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.test.QuarkusUnitTest; + +/** + * The integration test allowing to ensure that we can rely on {@link Application#getClasses()} to specify explicitly + * the classes to use for the application. + */ +class ApplicationTest { + + @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, AppTest.class)); + + @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()); + } + + @Path("rt-1") + public static class ResourceTest1 { + + @GET + @Path("ok") + public String ok() { + return "ok1"; + } + + @GET + @Path("ko") + public String ko() { + throw new UnsupportedOperationException(); + } + } + + @Path("rt-2") + public static class ResourceTest2 { + + @GET + @Path("ok") + public String ok() { + return "ok2"; + } + } + + @Provider + public static class ResponseFilter1 implements ContainerResponseFilter { + + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) + throws IOException { + responseContext.getHeaders().add("X-RF-1", "Value"); + } + } + + @Provider + public static class ResponseFilter2 implements ContainerResponseFilter { + + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) + throws IOException { + responseContext.getHeaders().add("X-RF-2", "Value"); + } + } + + @Provider + public static class ResponseFilter3 implements ContainerResponseFilter { + + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) + throws IOException { + responseContext.getHeaders().add("X-RF-3", "Value"); + } + } + + @Provider + public static class ResponseFilter4 implements ContainerResponseFilter { + + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) + throws IOException { + responseContext.getHeaders().add("X-RF-4", "Value"); + } + } + + @Provider + public static class ResponseFilter5 implements ContainerResponseFilter { + + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) + throws IOException { + responseContext.getHeaders().add("X-RF-5", "Value"); + } + } + + @Provider + public static class ResponseFilter6 implements ContainerResponseFilter { + + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) + throws IOException { + responseContext.getHeaders().add("X-RF-6", "Value"); + } + } + + @Provider + public static class Feature1 implements Feature { + + @Override + public boolean configure(FeatureContext context) { + context.register(ResponseFilter3.class); + return true; + } + } + + @Provider + public static class Feature2 implements Feature { + + @Override + public boolean configure(FeatureContext context) { + context.register(ResponseFilter4.class); + return true; + } + } + + @Provider + public static class ExceptionMapper1 implements ExceptionMapper<RuntimeException> { + + @Override + public Response toResponse(RuntimeException exception) { + return Response.status(Response.Status.SERVICE_UNAVAILABLE.getStatusCode()).build(); + } + } + + @Provider + public static class ExceptionMapper2 implements ExceptionMapper<UnsupportedOperationException> { + + @Override + public Response toResponse(UnsupportedOperationException exception) { + return Response.status(Response.Status.NOT_IMPLEMENTED.getStatusCode()).build(); + } + } + + @Provider + public static class DynamicFeature1 implements DynamicFeature { + + @Override + public void configure(ResourceInfo resourceInfo, FeatureContext context) { + context.register(ResponseFilter5.class); + } + } + + @Provider + public static class DynamicFeature2 implements DynamicFeature { + + @Override + public void configure(ResourceInfo resourceInfo, FeatureContext context) { + context.register(ResponseFilter6.class); + } + } + + public static class AppTest extends Application { + + @Override + public Set<Class<?>> getClasses() { + return new HashSet<>( + Arrays.asList( + ResourceTest1.class, Feature1.class, ExceptionMapper1.class)); + } + + @Override + public Set<Object> getSingletons() { + return new HashSet<>( + Arrays.asList( + new ResponseFilter1(), new DynamicFeature1())); + } + } +}