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()));
+        }
+    }
+}