From f2a497447c5495e90b6cd0b71877c67d7b7b7660 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20=C3=89pardaud?= <stef@epardaud.fr>
Date: Fri, 18 Nov 2022 16:59:06 +0100
Subject: [PATCH] Make sure bean params are `@Typed` correctly for CDI lookup
 in case of subtyping

Fixes #29227
---
 .../deployment/ResteasyReactiveProcessor.java |  21 +++-
 .../server/test/beanparam/BeanParamTest.java  | 104 ++++++++++++++++++
 2 files changed, 124 insertions(+), 1 deletion(-)
 create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/beanparam/BeanParamTest.java

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 d3c4f9102d540..9f7af3d31eafb 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
@@ -730,7 +730,7 @@ private FilterClassIntrospector createFilterClassIntrospector() {
         return ab.get();
     }
 
-    // We want to add @Typed to resources and providers so that they can be resolved as CDI bean using purely their
+    // We want to add @Typed to resources, beanparams and providers so that they can be resolved as CDI bean using purely their
     // class as a bean type. This removes any ambiguity that potential subclasses may have.
     @BuildStep
     public void transformEndpoints(
@@ -744,6 +744,10 @@ public void transformEndpoints(
         allResources.addAll(resourceScanningResultBuildItem.getResult().getScannedResources().keySet());
         allResources.addAll(resourceScanningResultBuildItem.getResult().getPossibleSubResources().keySet());
 
+        // all found bean params
+        Set<String> beanParams = resourceScanningResultBuildItem.getResult()
+                .getBeanParams();
+
         // discovered filters and interceptors
         Set<String> filtersAndInterceptors = new HashSet<>();
         InterceptorContainer<ReaderInterceptor> readerInterceptors = resourceInterceptorsBuildItem.getResourceInterceptors()
@@ -788,6 +792,21 @@ public void transform(TransformationContext context) {
                                 && clazz.declaredAnnotation(ResteasyReactiveDotNames.TYPED) == null) {
                             // Add @Typed(MyResource.class)
                             context.transform().add(createTypedAnnotationInstance(clazz, beanArchiveIndexBuildItem)).done();
+                            return;
+                        }
+                        // check if the class is a bean param
+                        if (beanParams.contains(clazz.name().toString())
+                                && clazz.declaredAnnotation(ResteasyReactiveDotNames.TYPED) == null) {
+                            // Stef: we could use createTypedAnnotationInstance which adds all interfaces but I don't think
+                            // we need to, for bean params, so let's keep it simple
+                            AnnotationValue[] annotationValues = new AnnotationValue[1];
+                            annotationValues[0] = AnnotationValue.createClassValue("value",
+                                    Type.create(clazz.name(), Type.Kind.CLASS));
+                            context.transform().add(AnnotationInstance.create(ResteasyReactiveDotNames.TYPED, clazz,
+                                    new AnnotationValue[] { AnnotationValue.createArrayValue("value",
+                                            annotationValues) }))
+                                    .done();
+                            return;
                         }
                     }
                 }));
diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/beanparam/BeanParamTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/beanparam/BeanParamTest.java
new file mode 100644
index 0000000000000..85a739e30b75a
--- /dev/null
+++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/beanparam/BeanParamTest.java
@@ -0,0 +1,104 @@
+package io.quarkus.resteasy.reactive.server.test.beanparam;
+
+import javax.ws.rs.BeanParam;
+import javax.ws.rs.CookieParam;
+import javax.ws.rs.FormParam;
+import javax.ws.rs.HeaderParam;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.QueryParam;
+
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.spec.JavaArchive;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.quarkus.test.QuarkusUnitTest;
+
+public class BeanParamTest {
+    @RegisterExtension
+    static final QuarkusUnitTest TEST = new QuarkusUnitTest()
+            .setArchiveProducer(() -> {
+                return ShrinkWrap.create(JavaArchive.class)
+                        .addClasses(MyBeanParamWithFieldsAndProperties.class, Top.class);
+            });
+
+    @Test
+    void shouldDeployWithoutIssues() {
+        // we only need to check that it deploys
+    }
+
+    public static class Top {
+        @PathParam("pathParam")
+        private String pathParam = "pathParam";
+
+        public String getPathParam() {
+            return pathParam;
+        }
+
+        public void setPathParam(String pathParam) {
+            this.pathParam = pathParam;
+        }
+    }
+
+    public static class MyBeanParamWithFieldsAndProperties extends Top {
+        @HeaderParam("headerParam")
+        private String headerParam = "headerParam";
+        @CookieParam("cookieParam")
+        private String cookieParam = "cookieParam";
+        @FormParam("formParam")
+        private String formParam = "formParam";
+        @QueryParam("queryParam")
+        private String queryParam = "queryParam";
+
+        // FIXME: Matrix not supported
+
+        public String getHeaderParam() {
+            return headerParam;
+        }
+
+        public void setHeaderParam(String headerParam) {
+            this.headerParam = headerParam;
+        }
+
+        public String getCookieParam() {
+            return cookieParam;
+        }
+
+        public void setCookieParam(String cookieParam) {
+            this.cookieParam = cookieParam;
+        }
+
+        public String getFormParam() {
+            return formParam;
+        }
+
+        public void setFormParam(String formParam) {
+            this.formParam = formParam;
+        }
+
+        public String getQueryParam() {
+            return queryParam;
+        }
+
+        public void setQueryParam(String queryParam) {
+            this.queryParam = queryParam;
+        }
+    }
+
+    @Path("/")
+    public static class Resource {
+        @Path("/a/{restPathDefault}/{restPath_Overridden}/{pathParam}")
+        @POST
+        public String beanParamWithFields(@BeanParam MyBeanParamWithFieldsAndProperties p) {
+            return null;
+        }
+
+        @Path("/b/{pathParam}")
+        @POST
+        public String beanParamWithFields(@BeanParam Top p) {
+            return null;
+        }
+    }
+}