From 30ccd767cec1b77f287d311cdc61bf8c3ff8d3e9 Mon Sep 17 00:00:00 2001
From: Georgios Andrianakis <geoand@gmail.com>
Date: Tue, 18 Jan 2022 17:50:58 +0200
Subject: [PATCH] Warn when Resource method returns JSON but no JSON provider
 exists

Resolves: #22970
---
 .../QuarkusServerEndpointIndexer.java         |  84 +++++++-
 .../deployment/ResteasyReactiveProcessor.java | 181 +++++++++---------
 .../common/processor/EndpointIndexer.java     |   5 +
 3 files changed, 180 insertions(+), 90 deletions(-)

diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/QuarkusServerEndpointIndexer.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/QuarkusServerEndpointIndexer.java
index 48fe51343c8e4..f74a26f9c1634 100644
--- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/QuarkusServerEndpointIndexer.java
+++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/QuarkusServerEndpointIndexer.java
@@ -7,44 +7,60 @@
 import javax.ws.rs.core.MediaType;
 
 import org.jboss.jandex.AnnotationInstance;
+import org.jboss.jandex.ClassInfo;
 import org.jboss.jandex.DotName;
 import org.jboss.jandex.IndexView;
+import org.jboss.jandex.MethodInfo;
 import org.jboss.jandex.Type;
+import org.jboss.logging.Logger;
 import org.jboss.resteasy.reactive.common.ResteasyReactiveConfig;
 import org.jboss.resteasy.reactive.common.processor.DefaultProducesHandler;
+import org.jboss.resteasy.reactive.common.processor.transformation.AnnotationStore;
+import org.jboss.resteasy.reactive.server.model.ServerResourceMethod;
 import org.jboss.resteasy.reactive.server.processor.ServerEndpointIndexer;
 import org.jboss.resteasy.reactive.server.processor.ServerIndexedParameter;
 import org.jboss.resteasy.reactive.server.spi.EndpointInvokerFactory;
 
+import io.quarkus.deployment.Capabilities;
 import io.quarkus.deployment.annotations.BuildProducer;
 import io.quarkus.deployment.builditem.BytecodeTransformerBuildItem;
 import io.quarkus.deployment.builditem.GeneratedClassBuildItem;
 import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
+import io.quarkus.resteasy.reactive.common.deployment.JsonDefaultProducersHandler;
 import io.quarkus.resteasy.reactive.server.runtime.ResteasyReactiveRecorder;
 
 public class QuarkusServerEndpointIndexer
         extends ServerEndpointIndexer {
+
+    private static final org.jboss.logging.Logger LOGGER = Logger.getLogger(QuarkusServerEndpointIndexer.class);
+
+    private final Capabilities capabilities;
     private final BuildProducer<GeneratedClassBuildItem> generatedClassBuildItemBuildProducer;
     private final BuildProducer<BytecodeTransformerBuildItem> bytecodeTransformerBuildProducer;
     private final BuildProducer<ReflectiveClassBuildItem> reflectiveClassProducer;
     private final DefaultProducesHandler defaultProducesHandler;
+    private final JsonDefaultProducersHandler jsonDefaultProducersHandler;
     private final ResteasyReactiveRecorder resteasyReactiveRecorder;
 
     private final Predicate<String> applicationClassPredicate;
 
     QuarkusServerEndpointIndexer(Builder builder) {
         super(builder);
+        this.capabilities = builder.capabilities;
         this.generatedClassBuildItemBuildProducer = builder.generatedClassBuildItemBuildProducer;
         this.bytecodeTransformerBuildProducer = builder.bytecodeTransformerBuildProducer;
         this.reflectiveClassProducer = builder.reflectiveClassProducer;
         this.defaultProducesHandler = builder.defaultProducesHandler;
         this.applicationClassPredicate = builder.applicationClassPredicate;
         this.resteasyReactiveRecorder = builder.resteasyReactiveRecorder;
+        this.jsonDefaultProducersHandler = new JsonDefaultProducersHandler();
     }
 
+    private DefaultProducesHandler.Context currentDefaultProducesContext;
+
     @Override
-    protected String[] applyAdditionalDefaults(Type nonAsyncReturnType) {
-        List<MediaType> defaultMediaTypes = defaultProducesHandler.handle(new DefaultProducesHandler.Context() {
+    protected void setupApplyDefaults(Type nonAsyncReturnType) {
+        currentDefaultProducesContext = new DefaultProducesHandler.Context() {
             @Override
             public Type nonAsyncReturnType() {
                 return nonAsyncReturnType;
@@ -59,7 +75,12 @@ public IndexView index() {
             public ResteasyReactiveConfig config() {
                 return config;
             }
-        });
+        };
+    }
+
+    @Override
+    protected String[] applyAdditionalDefaults(Type nonAsyncReturnType) {
+        List<MediaType> defaultMediaTypes = defaultProducesHandler.handle(currentDefaultProducesContext);
         if ((defaultMediaTypes != null) && !defaultMediaTypes.isEmpty()) {
             String[] result = new String[defaultMediaTypes.size()];
             for (int i = 0; i < defaultMediaTypes.size(); i++) {
@@ -80,6 +101,8 @@ protected boolean handleCustomParameter(Map<DotName, AnnotationInstance> anns, S
 
     public static final class Builder extends AbstractBuilder<Builder> {
 
+        private final Capabilities capabilities;
+
         private BuildProducer<GeneratedClassBuildItem> generatedClassBuildItemBuildProducer;
         private BuildProducer<BytecodeTransformerBuildItem> bytecodeTransformerBuildProducer;
         private BuildProducer<ReflectiveClassBuildItem> reflectiveClassProducer;
@@ -87,6 +110,10 @@ public static final class Builder extends AbstractBuilder<Builder> {
         private DefaultProducesHandler defaultProducesHandler = DefaultProducesHandler.Noop.INSTANCE;
         public Predicate<String> applicationClassPredicate;
 
+        public Builder(Capabilities capabilities) {
+            this.capabilities = capabilities;
+        }
+
         @Override
         public QuarkusServerEndpointIndexer build() {
             return new QuarkusServerEndpointIndexer(this);
@@ -125,4 +152,55 @@ public Builder setDefaultProducesHandler(DefaultProducesHandler defaultProducesH
             return this;
         }
     }
+
+    @Override
+    protected void handleAdditionalMethodProcessing(ServerResourceMethod method, ClassInfo currentClassInfo,
+            MethodInfo info, AnnotationStore annotationStore) {
+        super.handleAdditionalMethodProcessing(method, currentClassInfo, info, annotationStore);
+        warnAboutMissingJsonProviderIfNeeded(method, info);
+    }
+
+    private void warnAboutMissingJsonProviderIfNeeded(ServerResourceMethod method, MethodInfo info) {
+        if (!capabilities.isCapabilityWithPrefixMissing("io.quarkus.resteasy.reactive.json")) {
+            return;
+        }
+        if (hasJson(method) || isDefaultJson()) {
+            LOGGER.warnf("Quarkus detected the use of JSON in JAX-RS method '" + info.declaringClass().name() + "#"
+                    + info.name()
+                    + "' but no JSON extension has been added. Consider adding 'quarkus-resteasy-reactive-jackson' or 'quarkus-resteasy-reactive-jsonb'.");
+        }
+    }
+
+    private boolean isDefaultJson() {
+        List<MediaType> mediaTypes = jsonDefaultProducersHandler.handle(currentDefaultProducesContext);
+        for (MediaType mediaType : mediaTypes) {
+            if (isJson(mediaType.toString())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean hasJson(ServerResourceMethod method) {
+        return hasJson(method.getProduces()) || hasJson(method.getConsumes()) || isJson(method.getSseElementType());
+    }
+
+    private boolean hasJson(String[] types) {
+        if (types == null) {
+            return false;
+        }
+        for (String type : types) {
+            if (isJson(type)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean isJson(String type) {
+        if (type == null) {
+            return false;
+        }
+        return type.startsWith(MediaType.APPLICATION_JSON);
+    }
 }
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 60fbb378bd92f..84ed0e097d40c 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
@@ -339,7 +339,8 @@ public void setupEndpoints(BeanArchiveIndexBuildItem beanArchiveIndexBuildItem,
             ParamConverterProvidersBuildItem paramConverterProvidersBuildItem,
             List<ApplicationClassPredicateBuildItem> applicationClassPredicateBuildItems,
             List<MethodScannerBuildItem> methodScanners,
-            List<AnnotationsTransformerBuildItem> annotationTransformerBuildItems)
+            List<AnnotationsTransformerBuildItem> annotationTransformerBuildItems,
+            Capabilities capabilities)
             throws NoSuchMethodException {
 
         if (!resourceScanningResultBuildItem.isPresent()) {
@@ -390,77 +391,68 @@ public void setupEndpoints(BeanArchiveIndexBuildItem beanArchiveIndexBuildItem,
             BiConsumer<String, BiFunction<String, ClassVisitor, ClassVisitor>> transformationConsumer = (name,
                     function) -> bytecodeTransformerBuildItemBuildProducer
                             .produce(new BytecodeTransformerBuildItem(name, function));
-            QuarkusServerEndpointIndexer.Builder serverEndpointIndexerBuilder = new QuarkusServerEndpointIndexer.Builder()
-                    .addMethodScanners(
-                            methodScanners.stream().map(MethodScannerBuildItem::getMethodScanner).collect(toList()))
-                    .setIndex(index)
-                    .addContextTypes(CONTEXT_TYPES)
-                    .setFactoryCreator(new QuarkusFactoryCreator(recorder, beanContainerBuildItem.getValue()))
-                    .setEndpointInvokerFactory(new QuarkusInvokerFactory(generatedClassBuildItemBuildProducer, recorder))
-                    .setGeneratedClassBuildItemBuildProducer(generatedClassBuildItemBuildProducer)
-                    .setBytecodeTransformerBuildProducer(bytecodeTransformerBuildItemBuildProducer)
-                    .setReflectiveClassProducer(reflectiveClassBuildItemBuildProducer)
-                    .setExistingConverters(existingConverters)
-                    .setScannedResourcePaths(scannedResourcePaths)
-                    .setConfig(createRestReactiveConfig(config))
-                    .setAdditionalReaders(additionalReaders)
-                    .setHttpAnnotationToMethod(result.getHttpAnnotationToMethod())
-                    .setInjectableBeans(injectableBeans)
-                    .setAdditionalWriters(additionalWriters)
-                    .setDefaultBlocking(appResult.getBlockingDefault())
-                    .setApplicationScanningResult(appResult)
-                    .setMultipartParameterIndexerExtension(
-                            new GeneratedMultipartParamIndexerExtension(transformationConsumer, classOutput))
-                    .setMultipartReturnTypeIndexerExtension(
-                            new GeneratedHandlerMultipartReturnTypeIndexerExtension(classOutput))
-                    .setFieldInjectionIndexerExtension(
-                            new TransformedFieldInjectionIndexerExtension(transformationConsumer, false, (field) -> {
-                                initConverters.invokeStaticMethod(MethodDescriptor.ofMethod(field.getInjectedClassName(),
-                                        field.getMethodName(),
-                                        void.class, Deployment.class),
-                                        initConverters.getMethodParam(0));
-                            }))
-                    .setConverterSupplierIndexerExtension(new GeneratedConverterIndexerExtension(
-                            (name) -> new GeneratedClassGizmoAdaptor(generatedClassBuildItemBuildProducer,
-                                    applicationClassPredicate.test(name))))
-                    .setHasRuntimeConverters(!paramConverterProviders.getParamConverterProviders().isEmpty())
-                    .setClassLevelExceptionMappers(
-                            classLevelExceptionMappers.isPresent() ? classLevelExceptionMappers.get().getMappers()
-                                    : Collections.emptyMap())
-                    .setResourceMethodCallback(new Consumer<>() {
-                        @Override
-                        public void accept(EndpointIndexer.ResourceMethodCallbackData entry) {
-                            MethodInfo method = entry.getMethodInfo();
-
-                            resourceMethodEntries.add(new ResteasyReactiveResourceMethodEntriesBuildItem.Entry(
-                                    entry.getBasicResourceClassInfo(), method,
-                                    entry.getActualEndpointInfo(), entry.getResourceMethod()));
-
-                            String source = ResteasyReactiveProcessor.class.getSimpleName() + " > " + method.declaringClass()
-                                    + "[" + method + "]";
-
-                            ClassInfo classInfoWithSecurity = consumeStandardSecurityAnnotations(method,
-                                    entry.getActualEndpointInfo(), index, c -> c);
-                            if (classInfoWithSecurity != null) {
-                                reflectiveClass.produce(new ReflectiveClassBuildItem(false, true, false,
-                                        entry.getActualEndpointInfo().name().toString()));
-                            }
-
-                            reflectiveHierarchy.produce(new ReflectiveHierarchyBuildItem.Builder()
-                                    .type(method.returnType())
-                                    .index(index)
-                                    .ignoreTypePredicate(QuarkusResteasyReactiveDotNames.IGNORE_TYPE_FOR_REFLECTION_PREDICATE)
-                                    .ignoreFieldPredicate(QuarkusResteasyReactiveDotNames.IGNORE_FIELD_FOR_REFLECTION_PREDICATE)
-                                    .ignoreMethodPredicate(
-                                            QuarkusResteasyReactiveDotNames.IGNORE_METHOD_FOR_REFLECTION_PREDICATE)
-                                    .source(source)
-                                    .build());
-
-                            for (short i = 0; i < method.parameters().size(); i++) {
-                                Type parameterType = method.parameters().get(i);
-                                if (!hasAnnotation(method, i, ResteasyReactiveServerDotNames.CONTEXT)) {
+            QuarkusServerEndpointIndexer.Builder serverEndpointIndexerBuilder = new QuarkusServerEndpointIndexer.Builder(
+                    capabilities)
+                            .addMethodScanners(
+                                    methodScanners.stream().map(MethodScannerBuildItem::getMethodScanner).collect(toList()))
+                            .setIndex(index)
+                            .addContextTypes(CONTEXT_TYPES)
+                            .setFactoryCreator(new QuarkusFactoryCreator(recorder, beanContainerBuildItem.getValue()))
+                            .setEndpointInvokerFactory(
+                                    new QuarkusInvokerFactory(generatedClassBuildItemBuildProducer, recorder))
+                            .setGeneratedClassBuildItemBuildProducer(generatedClassBuildItemBuildProducer)
+                            .setBytecodeTransformerBuildProducer(bytecodeTransformerBuildItemBuildProducer)
+                            .setReflectiveClassProducer(reflectiveClassBuildItemBuildProducer)
+                            .setExistingConverters(existingConverters)
+                            .setScannedResourcePaths(scannedResourcePaths)
+                            .setConfig(createRestReactiveConfig(config))
+                            .setAdditionalReaders(additionalReaders)
+                            .setHttpAnnotationToMethod(result.getHttpAnnotationToMethod())
+                            .setInjectableBeans(injectableBeans)
+                            .setAdditionalWriters(additionalWriters)
+                            .setDefaultBlocking(appResult.getBlockingDefault())
+                            .setApplicationScanningResult(appResult)
+                            .setMultipartParameterIndexerExtension(
+                                    new GeneratedMultipartParamIndexerExtension(transformationConsumer, classOutput))
+                            .setMultipartReturnTypeIndexerExtension(
+                                    new GeneratedHandlerMultipartReturnTypeIndexerExtension(classOutput))
+                            .setFieldInjectionIndexerExtension(
+                                    new TransformedFieldInjectionIndexerExtension(transformationConsumer, false, (field) -> {
+                                        initConverters.invokeStaticMethod(
+                                                MethodDescriptor.ofMethod(field.getInjectedClassName(),
+                                                        field.getMethodName(),
+                                                        void.class, Deployment.class),
+                                                initConverters.getMethodParam(0));
+                                    }))
+                            .setConverterSupplierIndexerExtension(new GeneratedConverterIndexerExtension(
+                                    (name) -> new GeneratedClassGizmoAdaptor(generatedClassBuildItemBuildProducer,
+                                            applicationClassPredicate.test(name))))
+                            .setHasRuntimeConverters(!paramConverterProviders.getParamConverterProviders().isEmpty())
+                            .setClassLevelExceptionMappers(
+                                    classLevelExceptionMappers.isPresent() ? classLevelExceptionMappers.get().getMappers()
+                                            : Collections.emptyMap())
+                            .setResourceMethodCallback(new Consumer<>() {
+                                @Override
+                                public void accept(EndpointIndexer.ResourceMethodCallbackData entry) {
+                                    MethodInfo method = entry.getMethodInfo();
+
+                                    resourceMethodEntries.add(new ResteasyReactiveResourceMethodEntriesBuildItem.Entry(
+                                            entry.getBasicResourceClassInfo(), method,
+                                            entry.getActualEndpointInfo(), entry.getResourceMethod()));
+
+                                    String source = ResteasyReactiveProcessor.class.getSimpleName() + " > "
+                                            + method.declaringClass()
+                                            + "[" + method + "]";
+
+                                    ClassInfo classInfoWithSecurity = consumeStandardSecurityAnnotations(method,
+                                            entry.getActualEndpointInfo(), index, c -> c);
+                                    if (classInfoWithSecurity != null) {
+                                        reflectiveClass.produce(new ReflectiveClassBuildItem(false, true, false,
+                                                entry.getActualEndpointInfo().name().toString()));
+                                    }
+
                                     reflectiveHierarchy.produce(new ReflectiveHierarchyBuildItem.Builder()
-                                            .type(parameterType)
+                                            .type(method.returnType())
                                             .index(index)
                                             .ignoreTypePredicate(
                                                     QuarkusResteasyReactiveDotNames.IGNORE_TYPE_FOR_REFLECTION_PREDICATE)
@@ -470,24 +462,39 @@ public void accept(EndpointIndexer.ResourceMethodCallbackData entry) {
                                                     QuarkusResteasyReactiveDotNames.IGNORE_METHOD_FOR_REFLECTION_PREDICATE)
                                             .source(source)
                                             .build());
+
+                                    for (short i = 0; i < method.parameters().size(); i++) {
+                                        Type parameterType = method.parameters().get(i);
+                                        if (!hasAnnotation(method, i, ResteasyReactiveServerDotNames.CONTEXT)) {
+                                            reflectiveHierarchy.produce(new ReflectiveHierarchyBuildItem.Builder()
+                                                    .type(parameterType)
+                                                    .index(index)
+                                                    .ignoreTypePredicate(
+                                                            QuarkusResteasyReactiveDotNames.IGNORE_TYPE_FOR_REFLECTION_PREDICATE)
+                                                    .ignoreFieldPredicate(
+                                                            QuarkusResteasyReactiveDotNames.IGNORE_FIELD_FOR_REFLECTION_PREDICATE)
+                                                    .ignoreMethodPredicate(
+                                                            QuarkusResteasyReactiveDotNames.IGNORE_METHOD_FOR_REFLECTION_PREDICATE)
+                                                    .source(source)
+                                                    .build());
+                                        }
+                                    }
                                 }
-                            }
-                        }
-
-                        private boolean hasAnnotation(MethodInfo method, short paramPosition, DotName annotation) {
-                            for (AnnotationInstance annotationInstance : method.annotations()) {
-                                AnnotationTarget target = annotationInstance.target();
-                                if (target != null && target.kind() == AnnotationTarget.Kind.METHOD_PARAMETER
-                                        && target.asMethodParameter().position() == paramPosition
-                                        && annotationInstance.name().equals(annotation)) {
-                                    return true;
+
+                                private boolean hasAnnotation(MethodInfo method, short paramPosition, DotName annotation) {
+                                    for (AnnotationInstance annotationInstance : method.annotations()) {
+                                        AnnotationTarget target = annotationInstance.target();
+                                        if (target != null && target.kind() == AnnotationTarget.Kind.METHOD_PARAMETER
+                                                && target.asMethodParameter().position() == paramPosition
+                                                && annotationInstance.name().equals(annotation)) {
+                                            return true;
+                                        }
+                                    }
+                                    return false;
                                 }
-                            }
-                            return false;
-                        }
-                    })
-                    .setResteasyReactiveRecorder(recorder)
-                    .setApplicationClassPredicate(applicationClassPredicate);
+                            })
+                            .setResteasyReactiveRecorder(recorder)
+                            .setApplicationClassPredicate(applicationClassPredicate);
 
             if (!serverDefaultProducesHandlers.isEmpty()) {
                 List<DefaultProducesHandler> handlers = new ArrayList<>(serverDefaultProducesHandlers.size());
diff --git a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java
index 65f8400fe2d8c..e009dc1918a2b 100644
--- a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java
+++ b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java
@@ -715,6 +715,7 @@ protected abstract MethodParameter createMethodParameter(ClassInfo currentClassI
             ParameterType type, String elementType, boolean single, String signature);
 
     private String[] applyDefaultProduces(String[] produces, Type nonAsyncReturnType) {
+        setupApplyDefaults(nonAsyncReturnType);
         if (produces != null && produces.length != 0)
             return produces;
         return applyAdditionalDefaults(nonAsyncReturnType);
@@ -736,6 +737,10 @@ private String[] addDefaultCharsets(String[] produces) {
         return result.toArray(EMPTY_STRING_ARRAY);
     }
 
+    protected void setupApplyDefaults(Type nonAsyncReturnType) {
+
+    }
+
     protected String[] applyAdditionalDefaults(Type nonAsyncReturnType) {
         // FIXME: primitives
         if (STRING.equals(nonAsyncReturnType.name()))