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()))