From 24eda12307ac190c5b31a36971ad35f1973ab2e5 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Fri, 29 Oct 2021 09:30:04 +0300 Subject: [PATCH] Decouple quarkus-spring-web from RESTEasy Classic This is prerequisite work for being able to run Spring Web endpoints on RESTEasy Reactive in addition to RESTEasy Classic. The decoupling is achieved by using conditional dependencies and moving RESTEasy specific code to a new extension --- bom/application/pom.xml | 15 + devtools/bom-descriptor-json/pom.xml | 13 + docs/pom.xml | 13 + .../spring-web/core/common-runtime/pom.xml | 42 +++ .../AbstractResponseContentTypeResolver.java} | 17 +- ...bstractResponseStatusExceptionMapper.java} | 9 +- .../spring-web/{ => core}/deployment/pom.xml | 8 +- .../AbstractExceptionMapperGenerator.java | 0 ...dviceAbstractExceptionMapperGenerator.java | 22 +- .../web/deployment/ResponseBuilder.java | 0 .../ResponseStatusOnExceptionGenerator.java | 4 +- .../web/deployment/SpringWebProcessor.java | 212 ++++++++++++ .../spring/web/deployment/TypesUtil.java | 0 .../spring/web/test/ControllerReloadTest.java | 0 ...ResponseStatusAndExceptionHandlerTest.java | 0 .../web/test/SimpleSpringController.java | 0 .../web/test/SimpleSpringControllerTest.java | 0 extensions/spring-web/core/pom.xml | 23 ++ .../spring-web/{ => core}/runtime/pom.xml | 39 +-- .../web/runtime/ResponseEntityConverter.java | 44 +++ .../runtime/SpringWebEndpointProvider.java | 0 .../resources/META-INF/quarkus-extension.yaml | 0 ...rkus.runtime.test.TestHttpEndpointProvider | 0 extensions/spring-web/pom.xml | 8 +- .../resteasy-classic/deployment/pom.xml | 69 ++++ .../SpringWebResteasyClassicProcessor.java} | 307 +++++------------- .../web/test/MissingRestControllerTest.java | 3 +- .../spring-web/resteasy-classic/pom.xml | 21 ++ .../resteasy-classic/runtime/pom.xml | 62 ++++ ...asyClassicResponseContentTypeResolver.java | 21 ++ ...yClassicResponseStatusExceptionMapper.java | 16 + .../resources/META-INF/quarkus-extension.yaml | 5 + .../web/runtime/ResponseEntityConverter.java | 41 --- integration-tests/spring-web/pom.xml | 17 + 34 files changed, 693 insertions(+), 338 deletions(-) create mode 100644 extensions/spring-web/core/common-runtime/pom.xml rename extensions/spring-web/{runtime/src/main/java/io/quarkus/spring/web/runtime/ResponseContentTypeResolver.java => core/common-runtime/src/main/java/io.quarkus.spring.web.runtime.common/AbstractResponseContentTypeResolver.java} (73%) rename extensions/spring-web/{runtime/src/main/java/io/quarkus/spring/web/runtime/ResponseStatusExceptionMapper.java => core/common-runtime/src/main/java/io.quarkus.spring.web.runtime.common/AbstractResponseStatusExceptionMapper.java} (71%) rename extensions/spring-web/{ => core}/deployment/pom.xml (92%) rename extensions/spring-web/{ => core}/deployment/src/main/java/io/quarkus/spring/web/deployment/AbstractExceptionMapperGenerator.java (100%) rename extensions/spring-web/{ => core}/deployment/src/main/java/io/quarkus/spring/web/deployment/ControllerAdviceAbstractExceptionMapperGenerator.java (92%) rename extensions/spring-web/{ => core}/deployment/src/main/java/io/quarkus/spring/web/deployment/ResponseBuilder.java (100%) rename extensions/spring-web/{ => core}/deployment/src/main/java/io/quarkus/spring/web/deployment/ResponseStatusOnExceptionGenerator.java (92%) create mode 100644 extensions/spring-web/core/deployment/src/main/java/io/quarkus/spring/web/deployment/SpringWebProcessor.java rename extensions/spring-web/{ => core}/deployment/src/main/java/io/quarkus/spring/web/deployment/TypesUtil.java (100%) rename extensions/spring-web/{ => core}/deployment/src/test/java/io/quarkus/spring/web/test/ControllerReloadTest.java (100%) rename extensions/spring-web/{ => core}/deployment/src/test/java/io/quarkus/spring/web/test/ResponseStatusAndExceptionHandlerTest.java (100%) rename extensions/spring-web/{ => core}/deployment/src/test/java/io/quarkus/spring/web/test/SimpleSpringController.java (100%) rename extensions/spring-web/{ => core}/deployment/src/test/java/io/quarkus/spring/web/test/SimpleSpringControllerTest.java (100%) create mode 100644 extensions/spring-web/core/pom.xml rename extensions/spring-web/{ => core}/runtime/pom.xml (52%) create mode 100644 extensions/spring-web/core/runtime/src/main/java/io/quarkus/spring/web/runtime/ResponseEntityConverter.java rename extensions/spring-web/{ => core}/runtime/src/main/java/io/quarkus/spring/web/runtime/SpringWebEndpointProvider.java (100%) rename extensions/spring-web/{ => core}/runtime/src/main/resources/META-INF/quarkus-extension.yaml (100%) rename extensions/spring-web/{ => core}/runtime/src/main/resources/META-INF/services/io.quarkus.runtime.test.TestHttpEndpointProvider (100%) create mode 100644 extensions/spring-web/resteasy-classic/deployment/pom.xml rename extensions/spring-web/{deployment/src/main/java/io/quarkus/spring/web/deployment/SpringWebProcessor.java => resteasy-classic/deployment/src/main/java/io/quarkus/spring/web/deployment/SpringWebResteasyClassicProcessor.java} (61%) rename extensions/spring-web/{ => resteasy-classic}/deployment/src/test/java/io/quarkus/spring/web/test/MissingRestControllerTest.java (92%) create mode 100644 extensions/spring-web/resteasy-classic/pom.xml create mode 100644 extensions/spring-web/resteasy-classic/runtime/pom.xml create mode 100644 extensions/spring-web/resteasy-classic/runtime/src/main/java/io/quarkus/spring/web/runtime/ResteasyClassicResponseContentTypeResolver.java create mode 100644 extensions/spring-web/resteasy-classic/runtime/src/main/java/io/quarkus/spring/web/runtime/ResteasyClassicResponseStatusExceptionMapper.java create mode 100644 extensions/spring-web/resteasy-classic/runtime/src/main/resources/META-INF/quarkus-extension.yaml delete mode 100644 extensions/spring-web/runtime/src/main/java/io/quarkus/spring/web/runtime/ResponseEntityConverter.java diff --git a/bom/application/pom.xml b/bom/application/pom.xml index e1aee7119e11e..02a044996b4f4 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -1788,6 +1788,11 @@ quarkus-spring-scheduled-deployment ${project.version} + + io.quarkus + quarkus-spring-web-common + ${project.version} + io.quarkus quarkus-spring-web @@ -1798,6 +1803,16 @@ quarkus-spring-web-deployment ${project.version} + + io.quarkus + quarkus-spring-web-resteasy-classic + ${project.version} + + + io.quarkus + quarkus-spring-web-resteasy-classic-deployment + ${project.version} + io.quarkus quarkus-spring-data-jpa diff --git a/devtools/bom-descriptor-json/pom.xml b/devtools/bom-descriptor-json/pom.xml index 5572c44fd07a2..7b48d90c97e05 100644 --- a/devtools/bom-descriptor-json/pom.xml +++ b/devtools/bom-descriptor-json/pom.xml @@ -2671,6 +2671,19 @@ + + io.quarkus + quarkus-spring-web-resteasy-classic + ${project.version} + pom + test + + + * + * + + + io.quarkus quarkus-swagger-ui diff --git a/docs/pom.xml b/docs/pom.xml index ecf2adf740e0f..f591ade57a3f8 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -2632,6 +2632,19 @@ + + io.quarkus + quarkus-spring-web-resteasy-classic-deployment + ${project.version} + pom + test + + + * + * + + + io.quarkus quarkus-swagger-ui-deployment diff --git a/extensions/spring-web/core/common-runtime/pom.xml b/extensions/spring-web/core/common-runtime/pom.xml new file mode 100644 index 0000000000000..11e227f56b883 --- /dev/null +++ b/extensions/spring-web/core/common-runtime/pom.xml @@ -0,0 +1,42 @@ + + + + quarkus-spring-web-parent + io.quarkus + 999-SNAPSHOT + + + 4.0.0 + + quarkus-spring-web-common + Quarkus - Spring Web - Common Runtime + + + + org.jboss.spec.javax.ws.rs + jboss-jaxrs-api_2.1_spec + + + jakarta.enterprise + jakarta.enterprise.cdi-api + + + io.quarkus + quarkus-spring-web-api + + + io.quarkus + quarkus-spring-webmvc-api + + + io.quarkus + quarkus-spring-core-api + + + io.quarkus + quarkus-spring-context-api + + + diff --git a/extensions/spring-web/runtime/src/main/java/io/quarkus/spring/web/runtime/ResponseContentTypeResolver.java b/extensions/spring-web/core/common-runtime/src/main/java/io.quarkus.spring.web.runtime.common/AbstractResponseContentTypeResolver.java similarity index 73% rename from extensions/spring-web/runtime/src/main/java/io/quarkus/spring/web/runtime/ResponseContentTypeResolver.java rename to extensions/spring-web/core/common-runtime/src/main/java/io.quarkus.spring.web.runtime.common/AbstractResponseContentTypeResolver.java index db25c1729403c..b8429a0dc7c19 100644 --- a/extensions/spring-web/runtime/src/main/java/io/quarkus/spring/web/runtime/ResponseContentTypeResolver.java +++ b/extensions/spring-web/core/common-runtime/src/main/java/io.quarkus.spring.web.runtime.common/AbstractResponseContentTypeResolver.java @@ -1,4 +1,4 @@ -package io.quarkus.spring.web.runtime; +package io.quarkus.spring.web.runtime.common; import static javax.ws.rs.core.HttpHeaders.ACCEPT; import static javax.ws.rs.core.MediaType.TEXT_PLAIN_TYPE; @@ -10,13 +10,13 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Variant; -import org.jboss.resteasy.core.request.ServerDrivenNegotiation; - -public final class ResponseContentTypeResolver { +public abstract class AbstractResponseContentTypeResolver { private static final MediaType DEFAULT_MEDIA_TYPE = TEXT_PLAIN_TYPE; - public static MediaType resolve(HttpHeaders httpHeaders, String... supportedMediaTypes) { + protected abstract Variant negotiateBestMatch(List acceptHeaders, List variants); + + public MediaType resolve(HttpHeaders httpHeaders, String... supportedMediaTypes) { Objects.requireNonNull(httpHeaders, "HttpHeaders cannot be null"); Objects.requireNonNull(supportedMediaTypes, "Supported media types array cannot be null"); @@ -33,16 +33,13 @@ public static MediaType resolve(HttpHeaders httpHeaders, String... supportedMedi return DEFAULT_MEDIA_TYPE; } - private static Variant getBestVariant(List acceptHeaders, List variants) { + private Variant getBestVariant(List acceptHeaders, List variants) { if (acceptHeaders.isEmpty()) { // done because negotiation.setAcceptHeaders(acceptHeaders) throws a NPE when passed an empty list return null; } - ServerDrivenNegotiation negotiation = new ServerDrivenNegotiation(); - negotiation.setAcceptHeaders(acceptHeaders); - - return negotiation.getBestMatch(variants); + return negotiateBestMatch(acceptHeaders, variants); } private static List getMediaTypeVariants(String... mediaTypes) { diff --git a/extensions/spring-web/runtime/src/main/java/io/quarkus/spring/web/runtime/ResponseStatusExceptionMapper.java b/extensions/spring-web/core/common-runtime/src/main/java/io.quarkus.spring.web.runtime.common/AbstractResponseStatusExceptionMapper.java similarity index 71% rename from extensions/spring-web/runtime/src/main/java/io/quarkus/spring/web/runtime/ResponseStatusExceptionMapper.java rename to extensions/spring-web/core/common-runtime/src/main/java/io.quarkus.spring.web.runtime.common/AbstractResponseStatusExceptionMapper.java index 2a3f11e997ab5..9ebb33781bf9b 100644 --- a/extensions/spring-web/runtime/src/main/java/io/quarkus/spring/web/runtime/ResponseStatusExceptionMapper.java +++ b/extensions/spring-web/core/common-runtime/src/main/java/io.quarkus.spring.web.runtime.common/AbstractResponseStatusExceptionMapper.java @@ -1,4 +1,4 @@ -package io.quarkus.spring.web.runtime; +package io.quarkus.spring.web.runtime.common; import java.util.List; import java.util.Map; @@ -7,15 +7,16 @@ import javax.ws.rs.core.Response; import javax.ws.rs.ext.ExceptionMapper; -import org.jboss.resteasy.specimpl.ResponseBuilderImpl; import org.springframework.http.HttpHeaders; import org.springframework.web.server.ResponseStatusException; -public class ResponseStatusExceptionMapper implements ExceptionMapper { +public abstract class AbstractResponseStatusExceptionMapper implements ExceptionMapper { + + protected abstract Response.ResponseBuilder createResponseBuilder(ResponseStatusException exception); @Override public Response toResponse(ResponseStatusException exception) { - Response.ResponseBuilder responseBuilder = new ResponseBuilderImpl().status(exception.getStatus().value()); + Response.ResponseBuilder responseBuilder = createResponseBuilder(exception); addHeaders(responseBuilder, exception.getResponseHeaders()); return responseBuilder.entity(exception.getMessage()) .type(MediaType.TEXT_PLAIN).build(); diff --git a/extensions/spring-web/deployment/pom.xml b/extensions/spring-web/core/deployment/pom.xml similarity index 92% rename from extensions/spring-web/deployment/pom.xml rename to extensions/spring-web/core/deployment/pom.xml index 17ad4a9d67ee8..3b92b540a16ad 100644 --- a/extensions/spring-web/deployment/pom.xml +++ b/extensions/spring-web/core/deployment/pom.xml @@ -11,7 +11,7 @@ 4.0.0 quarkus-spring-web-deployment - Quarkus - Spring - Web - Deployment + Quarkus - Spring Web - Deployment @@ -24,7 +24,8 @@ io.quarkus - quarkus-resteasy-jackson-deployment + quarkus-spring-web-resteasy-classic-deployment + true io.quarkus @@ -32,7 +33,8 @@ io.quarkus - quarkus-resteasy-common-spi + quarkus-resteasy-jackson-deployment + test io.quarkus diff --git a/extensions/spring-web/deployment/src/main/java/io/quarkus/spring/web/deployment/AbstractExceptionMapperGenerator.java b/extensions/spring-web/core/deployment/src/main/java/io/quarkus/spring/web/deployment/AbstractExceptionMapperGenerator.java similarity index 100% rename from extensions/spring-web/deployment/src/main/java/io/quarkus/spring/web/deployment/AbstractExceptionMapperGenerator.java rename to extensions/spring-web/core/deployment/src/main/java/io/quarkus/spring/web/deployment/AbstractExceptionMapperGenerator.java diff --git a/extensions/spring-web/deployment/src/main/java/io/quarkus/spring/web/deployment/ControllerAdviceAbstractExceptionMapperGenerator.java b/extensions/spring-web/core/deployment/src/main/java/io/quarkus/spring/web/deployment/ControllerAdviceAbstractExceptionMapperGenerator.java similarity index 92% rename from extensions/spring-web/deployment/src/main/java/io/quarkus/spring/web/deployment/ControllerAdviceAbstractExceptionMapperGenerator.java rename to extensions/spring-web/core/deployment/src/main/java/io/quarkus/spring/web/deployment/ControllerAdviceAbstractExceptionMapperGenerator.java index 346407d9ca411..f3c0a5ce9a113 100644 --- a/extensions/spring-web/deployment/src/main/java/io/quarkus/spring/web/deployment/ControllerAdviceAbstractExceptionMapperGenerator.java +++ b/extensions/spring-web/core/deployment/src/main/java/io/quarkus/spring/web/deployment/ControllerAdviceAbstractExceptionMapperGenerator.java @@ -29,7 +29,6 @@ import io.quarkus.gizmo.MethodCreator; import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.gizmo.ResultHandle; -import io.quarkus.spring.web.runtime.ResponseContentTypeResolver; import io.quarkus.spring.web.runtime.ResponseEntityConverter; class ControllerAdviceAbstractExceptionMapperGenerator extends AbstractExceptionMapperGenerator { @@ -53,15 +52,24 @@ class ControllerAdviceAbstractExceptionMapperGenerator extends AbstractException private FieldDescriptor httpHeadersField; + private final boolean isResteasyClassic; + ControllerAdviceAbstractExceptionMapperGenerator(MethodInfo controllerAdviceMethod, DotName exceptionDotName, - ClassOutput classOutput, TypesUtil typesUtil) { + ClassOutput classOutput, TypesUtil typesUtil, boolean isResteasyClassic) { super(exceptionDotName, classOutput); + + // TODO: remove this restriction + if (!isResteasyClassic) { + throw new IllegalStateException("Currently Spring Web can only work with RESTEasy Classic"); + } + this.controllerAdviceMethod = controllerAdviceMethod; this.typesUtil = typesUtil; this.returnType = controllerAdviceMethod.returnType(); this.parameterTypes = controllerAdviceMethod.parameters(); this.declaringClassName = controllerAdviceMethod.declaringClass().name().toString(); + this.isResteasyClassic = isResteasyClassic; } /** @@ -187,9 +195,15 @@ private ResultHandle getResponseContentType(MethodCreator methodCreator, List MAPPING_ANNOTATIONS; + + static { + MAPPING_ANNOTATIONS = Arrays.asList( + REQUEST_MAPPING, + DotName.createSimple("org.springframework.web.bind.annotation.GetMapping"), + DotName.createSimple("org.springframework.web.bind.annotation.PostMapping"), + DotName.createSimple("org.springframework.web.bind.annotation.PutMapping"), + DotName.createSimple("org.springframework.web.bind.annotation.DeleteMapping"), + DotName.createSimple("org.springframework.web.bind.annotation.PatchMapping")); + } + + private static final DotName RESPONSE_STATUS = DotName + .createSimple("org.springframework.web.bind.annotation.ResponseStatus"); + private static final DotName EXCEPTION_HANDLER = DotName + .createSimple("org.springframework.web.bind.annotation.ExceptionHandler"); + + private static final DotName REST_CONTROLLER_ADVICE = DotName + .createSimple("org.springframework.web.bind.annotation.RestControllerAdvice"); + + private static final DotName MODEL_AND_VIEW = DotName.createSimple("org.springframework.web.servlet.ModelAndView"); + private static final DotName VIEW = DotName.createSimple("org.springframework.web.servlet.View"); + private static final DotName MODEL = DotName.createSimple("org.springframework.ui.Model"); + + private static final DotName HTTP_ENTITY = DotName.createSimple("org.springframework.http.HttpEntity"); + private static final DotName RESPONSE_ENTITY = DotName.createSimple("org.springframework.http.ResponseEntity"); + + private static final Set DISALLOWED_EXCEPTION_CONTROLLER_RETURN_TYPES = new HashSet<>(Arrays.asList( + MODEL_AND_VIEW, VIEW, MODEL, HTTP_ENTITY)); + + @BuildStep + FeatureBuildItem registerFeature() { + return new FeatureBuildItem(Feature.SPRING_WEB); + } + + @BuildStep + public AdditionalJaxRsResourceMethodAnnotationsBuildItem additionalJaxRsResourceMethodAnnotationsBuildItem() { + return new AdditionalJaxRsResourceMethodAnnotationsBuildItem(MAPPING_ANNOTATIONS); + } + + @BuildStep + public void ignoreReflectionHierarchy(BuildProducer ignore) { + ignore.produce(new ReflectiveHierarchyIgnoreWarningBuildItem( + new ReflectiveHierarchyIgnoreWarningBuildItem.DotNameExclusion(RESPONSE_ENTITY))); + ignore.produce( + new ReflectiveHierarchyIgnoreWarningBuildItem(new ReflectiveHierarchyIgnoreWarningBuildItem.DotNameExclusion( + DotName.createSimple("org.springframework.util.MimeType")))); + ignore.produce( + new ReflectiveHierarchyIgnoreWarningBuildItem(new ReflectiveHierarchyIgnoreWarningBuildItem.DotNameExclusion( + DotName.createSimple("org.springframework.util.MultiValueMap")))); + } + + @BuildStep + public void beanDefiningAnnotations(BuildProducer beanDefiningAnnotations) { + beanDefiningAnnotations + .produce(new BeanDefiningAnnotationBuildItem(REST_CONTROLLER_ANNOTATION, BuiltinScope.SINGLETON.getName())); + beanDefiningAnnotations + .produce(new BeanDefiningAnnotationBuildItem(REST_CONTROLLER_ADVICE, BuiltinScope.SINGLETON.getName())); + } + + @BuildStep + public void generateExceptionMapperProviders(BeanArchiveIndexBuildItem beanArchiveIndexBuildItem, + BuildProducer generatedExceptionMappers, + BuildProducer providersProducer, + BuildProducer reflectiveClassProducer, + Capabilities capabilities) { + + boolean isResteasyClassicAvailable = capabilities.isPresent(Capability.RESTEASY_JSON_JACKSON); + boolean isResteasyReactiveAvailable = capabilities.isPresent(Capability.RESTEASY_REACTIVE_JSON_JACKSON); + + if (!isResteasyClassicAvailable && !isResteasyReactiveAvailable) { + throw new IllegalStateException( + "Spring Web can only work if 'quarkus-resteasy-jackson' or 'quarkus-resteasy-reactive-jackson' is present"); + } + + TypesUtil typesUtil = new TypesUtil(Thread.currentThread().getContextClassLoader()); + + // Look for all exception classes that are annotated with @ResponseStatus + + IndexView index = beanArchiveIndexBuildItem.getIndex(); + ClassOutput classOutput = new GeneratedClassGizmoAdaptor(generatedExceptionMappers, true); + generateMappersForResponseStatusOnException(providersProducer, index, classOutput, typesUtil, + isResteasyClassicAvailable); + generateMappersForExceptionHandlerInControllerAdvice(providersProducer, reflectiveClassProducer, index, classOutput, + typesUtil, isResteasyClassicAvailable); + } + + private void generateMappersForResponseStatusOnException(BuildProducer providersProducer, + IndexView index, ClassOutput classOutput, TypesUtil typesUtil, boolean isResteasyClassic) { + Collection responseStatusInstances = index + .getAnnotations(RESPONSE_STATUS); + + if (responseStatusInstances.isEmpty()) { + return; + } + + for (AnnotationInstance instance : responseStatusInstances) { + if (AnnotationTarget.Kind.CLASS != instance.target().kind()) { + continue; + } + if (!typesUtil.isAssignable(Exception.class, instance.target().asClass().name())) { + continue; + } + + String name = new ResponseStatusOnExceptionGenerator(instance.target().asClass(), classOutput, isResteasyClassic) + .generate(); + providersProducer.produce(new ResteasyJaxrsProviderBuildItem(name)); + } + } + + private void generateMappersForExceptionHandlerInControllerAdvice( + BuildProducer providersProducer, + BuildProducer reflectiveClassProducer, IndexView index, ClassOutput classOutput, + TypesUtil typesUtil, boolean isResteasyClassic) { + + AnnotationInstance controllerAdviceInstance = getSingleControllerAdviceInstance(index); + if (controllerAdviceInstance == null) { + return; + } + + ClassInfo controllerAdvice = controllerAdviceInstance.target().asClass(); + List methods = controllerAdvice.methods(); + for (MethodInfo method : methods) { + AnnotationInstance exceptionHandlerInstance = method.annotation(EXCEPTION_HANDLER); + if (exceptionHandlerInstance == null) { + continue; + } + + if (!Modifier.isPublic(method.flags()) || Modifier.isStatic(method.flags())) { + throw new IllegalStateException( + "@ExceptionHandler methods in @ControllerAdvice must be public instance methods"); + } + + DotName returnTypeDotName = method.returnType().name(); + if (DISALLOWED_EXCEPTION_CONTROLLER_RETURN_TYPES.contains(returnTypeDotName)) { + throw new IllegalStateException( + "@ExceptionHandler methods in @ControllerAdvice classes can only have void, ResponseEntity or POJO return types"); + } + + if (!RESPONSE_ENTITY.equals(returnTypeDotName)) { + reflectiveClassProducer.produce(new ReflectiveClassBuildItem(true, true, returnTypeDotName.toString())); + } + + // we need to generate one JAX-RS ExceptionMapper per Exception type + Type[] handledExceptionTypes = exceptionHandlerInstance.value().asClassArray(); + for (Type handledExceptionType : handledExceptionTypes) { + String name = new ControllerAdviceAbstractExceptionMapperGenerator(method, handledExceptionType.name(), + classOutput, typesUtil, isResteasyClassic).generate(); + providersProducer.produce(new ResteasyJaxrsProviderBuildItem(name)); + } + + } + } + + private AnnotationInstance getSingleControllerAdviceInstance(IndexView index) { + Collection controllerAdviceInstances = index.getAnnotations(REST_CONTROLLER_ADVICE); + + if (controllerAdviceInstances.isEmpty()) { + return null; + } + + if (controllerAdviceInstances.size() > 1) { + throw new IllegalStateException("You can only have a single class annotated with @ControllerAdvice"); + } + + return controllerAdviceInstances.iterator().next(); + } + +} diff --git a/extensions/spring-web/deployment/src/main/java/io/quarkus/spring/web/deployment/TypesUtil.java b/extensions/spring-web/core/deployment/src/main/java/io/quarkus/spring/web/deployment/TypesUtil.java similarity index 100% rename from extensions/spring-web/deployment/src/main/java/io/quarkus/spring/web/deployment/TypesUtil.java rename to extensions/spring-web/core/deployment/src/main/java/io/quarkus/spring/web/deployment/TypesUtil.java diff --git a/extensions/spring-web/deployment/src/test/java/io/quarkus/spring/web/test/ControllerReloadTest.java b/extensions/spring-web/core/deployment/src/test/java/io/quarkus/spring/web/test/ControllerReloadTest.java similarity index 100% rename from extensions/spring-web/deployment/src/test/java/io/quarkus/spring/web/test/ControllerReloadTest.java rename to extensions/spring-web/core/deployment/src/test/java/io/quarkus/spring/web/test/ControllerReloadTest.java diff --git a/extensions/spring-web/deployment/src/test/java/io/quarkus/spring/web/test/ResponseStatusAndExceptionHandlerTest.java b/extensions/spring-web/core/deployment/src/test/java/io/quarkus/spring/web/test/ResponseStatusAndExceptionHandlerTest.java similarity index 100% rename from extensions/spring-web/deployment/src/test/java/io/quarkus/spring/web/test/ResponseStatusAndExceptionHandlerTest.java rename to extensions/spring-web/core/deployment/src/test/java/io/quarkus/spring/web/test/ResponseStatusAndExceptionHandlerTest.java diff --git a/extensions/spring-web/deployment/src/test/java/io/quarkus/spring/web/test/SimpleSpringController.java b/extensions/spring-web/core/deployment/src/test/java/io/quarkus/spring/web/test/SimpleSpringController.java similarity index 100% rename from extensions/spring-web/deployment/src/test/java/io/quarkus/spring/web/test/SimpleSpringController.java rename to extensions/spring-web/core/deployment/src/test/java/io/quarkus/spring/web/test/SimpleSpringController.java diff --git a/extensions/spring-web/deployment/src/test/java/io/quarkus/spring/web/test/SimpleSpringControllerTest.java b/extensions/spring-web/core/deployment/src/test/java/io/quarkus/spring/web/test/SimpleSpringControllerTest.java similarity index 100% rename from extensions/spring-web/deployment/src/test/java/io/quarkus/spring/web/test/SimpleSpringControllerTest.java rename to extensions/spring-web/core/deployment/src/test/java/io/quarkus/spring/web/test/SimpleSpringControllerTest.java diff --git a/extensions/spring-web/core/pom.xml b/extensions/spring-web/core/pom.xml new file mode 100644 index 0000000000000..8b104bd2b4631 --- /dev/null +++ b/extensions/spring-web/core/pom.xml @@ -0,0 +1,23 @@ + + + + quarkus-spring-web-parent-aggregator + io.quarkus + 999-SNAPSHOT + ../pom.xml + + 4.0.0 + + quarkus-spring-web-parent + Quarkus - Spring Web + pom + + deployment + runtime + common-runtime + + + + diff --git a/extensions/spring-web/runtime/pom.xml b/extensions/spring-web/core/runtime/pom.xml similarity index 52% rename from extensions/spring-web/runtime/pom.xml rename to extensions/spring-web/core/runtime/pom.xml index 68f88fa22c41e..1f5a06b60b1cf 100644 --- a/extensions/spring-web/runtime/pom.xml +++ b/extensions/spring-web/core/runtime/pom.xml @@ -11,51 +11,18 @@ 4.0.0 quarkus-spring-web - Quarkus - Spring - Web - Runtime + Quarkus - Spring Web - Runtime Use Spring Web annotations to create your REST services io.quarkus - quarkus-resteasy-jackson - - - org.jboss.resteasy - resteasy-spring-web - - - javax.enterprise - cdi-api - - - jakarta.activation - jakarta.activation-api - - - - - jakarta.enterprise - jakarta.enterprise.cdi-api + quarkus-spring-web-resteasy-classic + true io.quarkus quarkus-spring-di - - io.quarkus - quarkus-spring-web-api - - - io.quarkus - quarkus-spring-webmvc-api - - - io.quarkus - quarkus-spring-core-api - - - io.quarkus - quarkus-spring-context-api - diff --git a/extensions/spring-web/core/runtime/src/main/java/io/quarkus/spring/web/runtime/ResponseEntityConverter.java b/extensions/spring-web/core/runtime/src/main/java/io/quarkus/spring/web/runtime/ResponseEntityConverter.java new file mode 100644 index 0000000000000..d96c70de6405e --- /dev/null +++ b/extensions/spring-web/core/runtime/src/main/java/io/quarkus/spring/web/runtime/ResponseEntityConverter.java @@ -0,0 +1,44 @@ +package io.quarkus.spring.web.runtime; + +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; + +import org.jboss.resteasy.specimpl.MultivaluedTreeMap; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; + +/** + * This is only used in the generated ExceptionMappers when the Spring @RestControllerAdvice method returns a ResponseEntity + */ +public class ResponseEntityConverter { + + private static final String[] EMPTY_STRINGS_ARRAY = new String[0]; + + @SuppressWarnings("rawtypes") + public static Response toResponse(ResponseEntity responseEntity, MediaType defaultMediaType) { + Response.ResponseBuilder responseBuilder = Response.status(responseEntity.getStatusCodeValue()) + .entity(responseEntity.getBody()); + MultivaluedMap jaxRsHeaders = toJaxRsHeaders(responseEntity.getHeaders()); + if (!jaxRsHeaders.containsKey(HttpHeaders.CONTENT_TYPE)) { + jaxRsHeaders.putSingle(HttpHeaders.CONTENT_TYPE, defaultMediaType.toString()); + } + for (var entry : jaxRsHeaders.entrySet()) { + var value = entry.getValue(); + if (value.size() == 1) { + responseBuilder.header(entry.getKey(), entry.getValue().get(0)); + } else { + responseBuilder.header(entry.getKey(), entry.getValue()); + } + } + return responseBuilder.build(); + } + + private static MultivaluedMap toJaxRsHeaders(HttpHeaders springHeaders) { + var jaxRsHeaders = new MultivaluedTreeMap(); + for (var entry : springHeaders.entrySet()) { + jaxRsHeaders.addAll(entry.getKey(), entry.getValue().toArray(EMPTY_STRINGS_ARRAY)); + } + return jaxRsHeaders; + } +} diff --git a/extensions/spring-web/runtime/src/main/java/io/quarkus/spring/web/runtime/SpringWebEndpointProvider.java b/extensions/spring-web/core/runtime/src/main/java/io/quarkus/spring/web/runtime/SpringWebEndpointProvider.java similarity index 100% rename from extensions/spring-web/runtime/src/main/java/io/quarkus/spring/web/runtime/SpringWebEndpointProvider.java rename to extensions/spring-web/core/runtime/src/main/java/io/quarkus/spring/web/runtime/SpringWebEndpointProvider.java diff --git a/extensions/spring-web/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/spring-web/core/runtime/src/main/resources/META-INF/quarkus-extension.yaml similarity index 100% rename from extensions/spring-web/runtime/src/main/resources/META-INF/quarkus-extension.yaml rename to extensions/spring-web/core/runtime/src/main/resources/META-INF/quarkus-extension.yaml diff --git a/extensions/spring-web/runtime/src/main/resources/META-INF/services/io.quarkus.runtime.test.TestHttpEndpointProvider b/extensions/spring-web/core/runtime/src/main/resources/META-INF/services/io.quarkus.runtime.test.TestHttpEndpointProvider similarity index 100% rename from extensions/spring-web/runtime/src/main/resources/META-INF/services/io.quarkus.runtime.test.TestHttpEndpointProvider rename to extensions/spring-web/core/runtime/src/main/resources/META-INF/services/io.quarkus.runtime.test.TestHttpEndpointProvider diff --git a/extensions/spring-web/pom.xml b/extensions/spring-web/pom.xml index b287523b966fc..4d142216408aa 100644 --- a/extensions/spring-web/pom.xml +++ b/extensions/spring-web/pom.xml @@ -10,11 +10,11 @@ 4.0.0 - quarkus-spring-web-parent - Quarkus - Spring - Web + quarkus-spring-web-parent-aggregator + Quarkus - Spring Web - Parent - Aggregator pom - deployment - runtime + core + resteasy-classic diff --git a/extensions/spring-web/resteasy-classic/deployment/pom.xml b/extensions/spring-web/resteasy-classic/deployment/pom.xml new file mode 100644 index 0000000000000..21e15377874eb --- /dev/null +++ b/extensions/spring-web/resteasy-classic/deployment/pom.xml @@ -0,0 +1,69 @@ + + + + quarkus-spring-web-resteasy-classic-parent + io.quarkus + 999-SNAPSHOT + + + 4.0.0 + + quarkus-spring-web-resteasy-classic-deployment + Quarkus - Spring Web - RESTEasy Classic - Deployment + + + + io.quarkus + quarkus-resteasy-deployment + + + io.quarkus + quarkus-resteasy-common-spi + + + + io.quarkus + quarkus-spring-web-resteasy-classic + + + + io.quarkus + quarkus-junit5-internal + test + + + io.rest-assured + rest-assured + test + + + org.assertj + assertj-core + test + + + io.quarkus + quarkus-spring-web-api + test + + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/spring-web/deployment/src/main/java/io/quarkus/spring/web/deployment/SpringWebProcessor.java b/extensions/spring-web/resteasy-classic/deployment/src/main/java/io/quarkus/spring/web/deployment/SpringWebResteasyClassicProcessor.java similarity index 61% rename from extensions/spring-web/deployment/src/main/java/io/quarkus/spring/web/deployment/SpringWebProcessor.java rename to extensions/spring-web/resteasy-classic/deployment/src/main/java/io/quarkus/spring/web/deployment/SpringWebResteasyClassicProcessor.java index c68006e551a11..d71c414135bec 100644 --- a/extensions/spring-web/deployment/src/main/java/io/quarkus/spring/web/deployment/SpringWebProcessor.java +++ b/extensions/spring-web/resteasy-classic/deployment/src/main/java/io/quarkus/spring/web/deployment/SpringWebResteasyClassicProcessor.java @@ -3,9 +3,7 @@ import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT; import java.io.IOException; -import java.lang.reflect.Modifier; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; @@ -24,7 +22,6 @@ 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.core.MediaTypeMap; import org.jboss.resteasy.plugins.server.servlet.ResteasyContextParameters; @@ -34,21 +31,12 @@ import org.jboss.resteasy.spring.web.ResponseStatusFeature; import io.quarkus.arc.deployment.BeanArchiveIndexBuildItem; -import io.quarkus.arc.deployment.BeanDefiningAnnotationBuildItem; -import io.quarkus.arc.processor.BuiltinScope; -import io.quarkus.deployment.Feature; -import io.quarkus.deployment.GeneratedClassGizmoAdaptor; import io.quarkus.deployment.IsDevelopment; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.Record; -import io.quarkus.deployment.builditem.FeatureBuildItem; -import io.quarkus.deployment.builditem.GeneratedClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; -import io.quarkus.deployment.builditem.nativeimage.ReflectiveHierarchyIgnoreWarningBuildItem; import io.quarkus.deployment.util.ServiceUtil; -import io.quarkus.gizmo.ClassOutput; -import io.quarkus.jaxrs.spi.deployment.AdditionalJaxRsResourceMethodAnnotationsBuildItem; import io.quarkus.resteasy.common.deployment.ResteasyCommonProcessor; import io.quarkus.resteasy.common.spi.ResteasyJaxrsProviderBuildItem; import io.quarkus.resteasy.runtime.ExceptionMapperRecorder; @@ -56,55 +44,28 @@ import io.quarkus.resteasy.server.common.deployment.ResteasyDeploymentCustomizerBuildItem; import io.quarkus.resteasy.server.common.spi.AdditionalJaxRsResourceDefiningAnnotationBuildItem; import io.quarkus.resteasy.server.common.spi.AdditionalJaxRsResourceMethodParamAnnotations; -import io.quarkus.spring.web.runtime.ResponseStatusExceptionMapper; +import io.quarkus.spring.web.runtime.ResteasyClassicResponseStatusExceptionMapper; import io.quarkus.undertow.deployment.IgnoredServletContainerInitializerBuildItem; import io.quarkus.undertow.deployment.ServletInitParamBuildItem; -public class SpringWebProcessor { +public class SpringWebResteasyClassicProcessor { - private static final Logger LOGGER = Logger.getLogger(SpringWebProcessor.class.getName()); - - private static final DotName REST_CONTROLLER_ANNOTATION = DotName - .createSimple("org.springframework.web.bind.annotation.RestController"); + private static final Logger LOGGER = Logger.getLogger(SpringWebResteasyClassicProcessor.class.getName()); private static final DotName REQUEST_MAPPING = DotName .createSimple("org.springframework.web.bind.annotation.RequestMapping"); - private static final DotName PATH_VARIABLE = DotName.createSimple("org.springframework.web.bind.annotation.PathVariable"); - - private static final List MAPPING_ANNOTATIONS; - - static { - MAPPING_ANNOTATIONS = Arrays.asList( - REQUEST_MAPPING, - DotName.createSimple("org.springframework.web.bind.annotation.GetMapping"), - DotName.createSimple("org.springframework.web.bind.annotation.PostMapping"), - DotName.createSimple("org.springframework.web.bind.annotation.PutMapping"), - DotName.createSimple("org.springframework.web.bind.annotation.DeleteMapping"), - DotName.createSimple("org.springframework.web.bind.annotation.PatchMapping")); - } - - private static final DotName RESPONSE_STATUS = DotName - .createSimple("org.springframework.web.bind.annotation.ResponseStatus"); - private static final DotName EXCEPTION_HANDLER = DotName - .createSimple("org.springframework.web.bind.annotation.ExceptionHandler"); - - private static final DotName REST_CONTROLLER_ADVICE = DotName - .createSimple("org.springframework.web.bind.annotation.RestControllerAdvice"); + private static final DotName GET_MAPPING = DotName.createSimple("org.springframework.web.bind.annotation.GetMapping"); + private static final DotName POST_MAPPING = DotName.createSimple("org.springframework.web.bind.annotation.PostMapping"); + private static final DotName PUT_MAPPING = DotName.createSimple("org.springframework.web.bind.annotation.PutMapping"); + private static final DotName DELETE_MAPPING = DotName.createSimple("org.springframework.web.bind.annotation.DeleteMapping"); + private static final DotName PATCH_MAPPING = DotName.createSimple("org.springframework.web.bind.annotation.PatchMapping"); + private static final List MAPPING_ANNOTATIONS = List.of(REQUEST_MAPPING, GET_MAPPING, POST_MAPPING, + PUT_MAPPING, DELETE_MAPPING, PATCH_MAPPING); - private static final DotName MODEL_AND_VIEW = DotName.createSimple("org.springframework.web.servlet.ModelAndView"); - private static final DotName VIEW = DotName.createSimple("org.springframework.web.servlet.View"); - private static final DotName MODEL = DotName.createSimple("org.springframework.ui.Model"); - - private static final DotName HTTP_ENTITY = DotName.createSimple("org.springframework.http.HttpEntity"); - private static final DotName RESPONSE_ENTITY = DotName.createSimple("org.springframework.http.ResponseEntity"); - - private static final Set DISALLOWED_EXCEPTION_CONTROLLER_RETURN_TYPES = new HashSet<>(Arrays.asList( - MODEL_AND_VIEW, VIEW, MODEL, HTTP_ENTITY)); + private static final DotName REST_CONTROLLER_ANNOTATION = DotName + .createSimple("org.springframework.web.bind.annotation.RestController"); - @BuildStep - FeatureBuildItem registerFeature() { - return new FeatureBuildItem(Feature.SPRING_WEB); - } + private static final DotName PATH_VARIABLE = DotName.createSimple("org.springframework.web.bind.annotation.PathVariable"); @BuildStep public IgnoredServletContainerInitializerBuildItem ignoreSpringServlet() { @@ -116,15 +77,10 @@ public AdditionalJaxRsResourceDefiningAnnotationBuildItem additionalJaxRsResourc return new AdditionalJaxRsResourceDefiningAnnotationBuildItem(REST_CONTROLLER_ANNOTATION); } - @BuildStep - public AdditionalJaxRsResourceMethodAnnotationsBuildItem additionalJaxRsResourceMethodAnnotationsBuildItem() { - return new AdditionalJaxRsResourceMethodAnnotationsBuildItem(MAPPING_ANNOTATIONS); - } - @BuildStep public AdditionalJaxRsResourceMethodParamAnnotations additionalJaxRsResourceMethodParamAnnotations() { return new AdditionalJaxRsResourceMethodParamAnnotations( - Arrays.asList(DotName.createSimple("org.springframework.web.bind.annotation.RequestParam"), + List.of(DotName.createSimple("org.springframework.web.bind.annotation.RequestParam"), PATH_VARIABLE, DotName.createSimple("org.springframework.web.bind.annotation.RequestBody"), DotName.createSimple("org.springframework.web.bind.annotation.MatrixVariable"), @@ -133,23 +89,9 @@ public AdditionalJaxRsResourceMethodParamAnnotations additionalJaxRsResourceMeth } @BuildStep - public void ignoreReflectionHierarchy(BuildProducer ignore) { - ignore.produce(new ReflectiveHierarchyIgnoreWarningBuildItem( - new ReflectiveHierarchyIgnoreWarningBuildItem.DotNameExclusion(RESPONSE_ENTITY))); - ignore.produce( - new ReflectiveHierarchyIgnoreWarningBuildItem(new ReflectiveHierarchyIgnoreWarningBuildItem.DotNameExclusion( - DotName.createSimple("org.springframework.util.MimeType")))); - ignore.produce( - new ReflectiveHierarchyIgnoreWarningBuildItem(new ReflectiveHierarchyIgnoreWarningBuildItem.DotNameExclusion( - DotName.createSimple("org.springframework.util.MultiValueMap")))); - } - - @BuildStep - public void beanDefiningAnnotations(BuildProducer beanDefiningAnnotations) { - beanDefiningAnnotations - .produce(new BeanDefiningAnnotationBuildItem(REST_CONTROLLER_ANNOTATION, BuiltinScope.SINGLETON.getName())); - beanDefiningAnnotations - .produce(new BeanDefiningAnnotationBuildItem(REST_CONTROLLER_ADVICE, BuiltinScope.SINGLETON.getName())); + public void registerStandardExceptionMappers(BuildProducer providersProducer) { + providersProducer + .produce(new ResteasyJaxrsProviderBuildItem(ResteasyClassicResponseStatusExceptionMapper.class.getName())); } @BuildStep @@ -219,82 +161,6 @@ private void validateControllers(BeanArchiveIndexBuildItem beanArchiveIndexBuild } } - @BuildStep(onlyIf = IsDevelopment.class) - @Record(STATIC_INIT) - public void registerWithDevModeNotFoundMapper(BeanArchiveIndexBuildItem beanArchiveIndexBuildItem, - ExceptionMapperRecorder recorder) { - IndexView index = beanArchiveIndexBuildItem.getIndex(); - Collection restControllerAnnotations = index.getAnnotations(REST_CONTROLLER_ANNOTATION); - if (restControllerAnnotations.isEmpty()) { - return; - } - - Map nonJaxRsPaths = new HashMap<>(); - for (AnnotationInstance restControllerInstance : restControllerAnnotations) { - String basePath = "/"; - ClassInfo restControllerAnnotatedClass = restControllerInstance.target().asClass(); - - AnnotationInstance requestMappingInstance = restControllerAnnotatedClass.classAnnotation(REQUEST_MAPPING); - if (requestMappingInstance != null) { - String basePathFromAnnotation = getMappingValue(requestMappingInstance); - if (basePathFromAnnotation != null) { - basePath = basePathFromAnnotation; - } - } - Map methodNameToPath = new HashMap<>(); - NonJaxRsClassMappings nonJaxRsClassMappings = new NonJaxRsClassMappings(); - nonJaxRsClassMappings.setMethodNameToPath(methodNameToPath); - nonJaxRsClassMappings.setBasePath(basePath); - - List methods = restControllerAnnotatedClass.methods(); - - // go through each of the methods and see if there are any mapping Spring annotation from which to get the path - METHOD: for (MethodInfo method : methods) { - String methodName = method.name(); - String methodPath; - // go through each of the annotations that can be used to make a method handle an http request - for (DotName mappingClass : MAPPING_ANNOTATIONS) { - AnnotationInstance mappingClassAnnotation = method.annotation(mappingClass); - if (mappingClassAnnotation != null) { - methodPath = getMappingValue(mappingClassAnnotation); - if (methodPath == null) { - methodPath = ""; // ensure that no nasty null values show up in the output - } else if (!methodPath.startsWith("/")) { - methodPath = "/" + methodPath; - } - // record the mapping of method to the http path - methodNameToPath.put(methodName, methodPath); - continue METHOD; - } - } - } - - // if there was at least one controller method, add the controller since it contains methods that handle http requests - if (!methodNameToPath.isEmpty()) { - nonJaxRsPaths.put(restControllerAnnotatedClass.name().toString(), nonJaxRsClassMappings); - } - } - - if (!nonJaxRsPaths.isEmpty()) { - recorder.nonJaxRsClassNameToMethodPaths(nonJaxRsPaths); - } - } - - /** - * Meant to be called with an instance of any of the MAPPING_CLASSES - */ - private String getMappingValue(AnnotationInstance instance) { - if (instance == null) { - return null; - } - if (instance.value() != null) { - return instance.value().asStringArray()[0]; - } else if (instance.value("path") != null) { - return instance.value("path").asStringArray()[0]; - } - return null; - } - @BuildStep public void registerProviders(BeanArchiveIndexBuildItem beanArchiveIndexBuildItem, BuildProducer providersProducer) throws IOException { @@ -366,106 +232,79 @@ private boolean collectProviders(Set providersToRegister, MediaTypeMap generatedExceptionMappers, - BuildProducer providersProducer, - BuildProducer reflectiveClassProducer) { - - TypesUtil typesUtil = new TypesUtil(Thread.currentThread().getContextClassLoader()); - - // Look for all exception classes that are annotated with @ResponseStatus - + @BuildStep(onlyIf = IsDevelopment.class) + @Record(STATIC_INIT) + public void registerWithDevModeNotFoundMapper(BeanArchiveIndexBuildItem beanArchiveIndexBuildItem, + ExceptionMapperRecorder recorder) { IndexView index = beanArchiveIndexBuildItem.getIndex(); - ClassOutput classOutput = new GeneratedClassGizmoAdaptor(generatedExceptionMappers, true); - generateMappersForResponseStatusOnException(providersProducer, index, classOutput, typesUtil); - generateMappersForExceptionHandlerInControllerAdvice(providersProducer, reflectiveClassProducer, index, classOutput, - typesUtil); - } - - @BuildStep - public void registerStandardExceptionMappers(BuildProducer providersProducer) { - providersProducer.produce(new ResteasyJaxrsProviderBuildItem(ResponseStatusExceptionMapper.class.getName())); - } - - private void generateMappersForResponseStatusOnException(BuildProducer providersProducer, - IndexView index, ClassOutput classOutput, TypesUtil typesUtil) { - Collection responseStatusInstances = index - .getAnnotations(RESPONSE_STATUS); - - if (responseStatusInstances.isEmpty()) { - return; - } - - for (AnnotationInstance instance : responseStatusInstances) { - if (AnnotationTarget.Kind.CLASS != instance.target().kind()) { - continue; - } - if (!typesUtil.isAssignable(Exception.class, instance.target().asClass().name())) { - continue; - } - - String name = new ResponseStatusOnExceptionGenerator(instance.target().asClass(), classOutput).generate(); - providersProducer.produce(new ResteasyJaxrsProviderBuildItem(name)); - } - } - - private void generateMappersForExceptionHandlerInControllerAdvice( - BuildProducer providersProducer, - BuildProducer reflectiveClassProducer, IndexView index, ClassOutput classOutput, - TypesUtil typesUtil) { - - AnnotationInstance controllerAdviceInstance = getSingleControllerAdviceInstance(index); - if (controllerAdviceInstance == null) { + Collection restControllerAnnotations = index.getAnnotations(REST_CONTROLLER_ANNOTATION); + if (restControllerAnnotations.isEmpty()) { return; } - ClassInfo controllerAdvice = controllerAdviceInstance.target().asClass(); - List methods = controllerAdvice.methods(); - for (MethodInfo method : methods) { - AnnotationInstance exceptionHandlerInstance = method.annotation(EXCEPTION_HANDLER); - if (exceptionHandlerInstance == null) { - continue; - } + Map nonJaxRsPaths = new HashMap<>(); + for (AnnotationInstance restControllerInstance : restControllerAnnotations) { + String basePath = "/"; + ClassInfo restControllerAnnotatedClass = restControllerInstance.target().asClass(); - if (!Modifier.isPublic(method.flags()) || Modifier.isStatic(method.flags())) { - throw new IllegalStateException( - "@ExceptionHandler methods in @ControllerAdvice must be public instance methods"); + AnnotationInstance requestMappingInstance = restControllerAnnotatedClass.classAnnotation(REQUEST_MAPPING); + if (requestMappingInstance != null) { + String basePathFromAnnotation = getMappingValue(requestMappingInstance); + if (basePathFromAnnotation != null) { + basePath = basePathFromAnnotation; + } } + Map methodNameToPath = new HashMap<>(); + NonJaxRsClassMappings nonJaxRsClassMappings = new NonJaxRsClassMappings(); + nonJaxRsClassMappings.setMethodNameToPath(methodNameToPath); + nonJaxRsClassMappings.setBasePath(basePath); - DotName returnTypeDotName = method.returnType().name(); - if (DISALLOWED_EXCEPTION_CONTROLLER_RETURN_TYPES.contains(returnTypeDotName)) { - throw new IllegalStateException( - "@ExceptionHandler methods in @ControllerAdvice classes can only have void, ResponseEntity or POJO return types"); - } + List methods = restControllerAnnotatedClass.methods(); - if (!RESPONSE_ENTITY.equals(returnTypeDotName)) { - reflectiveClassProducer.produce(new ReflectiveClassBuildItem(true, true, returnTypeDotName.toString())); + // go through each of the methods and see if there are any mapping Spring annotation from which to get the path + METHOD: for (MethodInfo method : methods) { + String methodName = method.name(); + String methodPath; + // go through each of the annotations that can be used to make a method handle an http request + for (DotName mappingClass : MAPPING_ANNOTATIONS) { + AnnotationInstance mappingClassAnnotation = method.annotation(mappingClass); + if (mappingClassAnnotation != null) { + methodPath = getMappingValue(mappingClassAnnotation); + if (methodPath == null) { + methodPath = ""; // ensure that no nasty null values show up in the output + } else if (!methodPath.startsWith("/")) { + methodPath = "/" + methodPath; + } + // record the mapping of method to the http path + methodNameToPath.put(methodName, methodPath); + continue METHOD; + } + } } - // we need to generate one JAX-RS ExceptionMapper per Exception type - Type[] handledExceptionTypes = exceptionHandlerInstance.value().asClassArray(); - for (Type handledExceptionType : handledExceptionTypes) { - String name = new ControllerAdviceAbstractExceptionMapperGenerator(method, handledExceptionType.name(), - classOutput, typesUtil).generate(); - providersProducer.produce(new ResteasyJaxrsProviderBuildItem(name)); + // if there was at least one controller method, add the controller since it contains methods that handle http requests + if (!methodNameToPath.isEmpty()) { + nonJaxRsPaths.put(restControllerAnnotatedClass.name().toString(), nonJaxRsClassMappings); } + } + if (!nonJaxRsPaths.isEmpty()) { + recorder.nonJaxRsClassNameToMethodPaths(nonJaxRsPaths); } } - private AnnotationInstance getSingleControllerAdviceInstance(IndexView index) { - Collection controllerAdviceInstances = index.getAnnotations(REST_CONTROLLER_ADVICE); - - if (controllerAdviceInstances.isEmpty()) { + /** + * Meant to be called with an instance of any of the MAPPING_CLASSES + */ + private String getMappingValue(AnnotationInstance instance) { + if (instance == null) { return null; } - - if (controllerAdviceInstances.size() > 1) { - throw new IllegalStateException("You can only have a single class annotated with @ControllerAdvice"); + if (instance.value() != null) { + return instance.value().asStringArray()[0]; + } else if (instance.value("path") != null) { + return instance.value("path").asStringArray()[0]; } - - return controllerAdviceInstances.iterator().next(); + return null; } - } diff --git a/extensions/spring-web/deployment/src/test/java/io/quarkus/spring/web/test/MissingRestControllerTest.java b/extensions/spring-web/resteasy-classic/deployment/src/test/java/io/quarkus/spring/web/test/MissingRestControllerTest.java similarity index 92% rename from extensions/spring-web/deployment/src/test/java/io/quarkus/spring/web/test/MissingRestControllerTest.java rename to extensions/spring-web/resteasy-classic/deployment/src/test/java/io/quarkus/spring/web/test/MissingRestControllerTest.java index 3ad617d1200a8..4d2f69ec83189 100644 --- a/extensions/spring-web/deployment/src/test/java/io/quarkus/spring/web/test/MissingRestControllerTest.java +++ b/extensions/spring-web/resteasy-classic/deployment/src/test/java/io/quarkus/spring/web/test/MissingRestControllerTest.java @@ -25,7 +25,8 @@ public class MissingRestControllerTest { .addClasses(NonAnnotatedController.class, ProperController.class)) .setApplicationName("missing-rest-controller") .setApplicationVersion("0.1-SNAPSHOT") - .setLogRecordPredicate(r -> "io.quarkus.spring.web.deployment.SpringWebProcessor".equals(r.getLoggerName())); + .setLogRecordPredicate( + r -> "io.quarkus.spring.web.deployment.SpringWebResteasyClassicProcessor".equals(r.getLoggerName())); @ProdBuildResults private ProdModeTestResults prodModeTestResults; diff --git a/extensions/spring-web/resteasy-classic/pom.xml b/extensions/spring-web/resteasy-classic/pom.xml new file mode 100644 index 0000000000000..8141e8376f124 --- /dev/null +++ b/extensions/spring-web/resteasy-classic/pom.xml @@ -0,0 +1,21 @@ + + + + quarkus-spring-web-parent-aggregator + io.quarkus + 999-SNAPSHOT + ../pom.xml + + 4.0.0 + + quarkus-spring-web-resteasy-classic-parent + Quarkus - Spring Web - RESTEasy Classic - Parent + pom + + deployment + runtime + + + diff --git a/extensions/spring-web/resteasy-classic/runtime/pom.xml b/extensions/spring-web/resteasy-classic/runtime/pom.xml new file mode 100644 index 0000000000000..e5d8688143837 --- /dev/null +++ b/extensions/spring-web/resteasy-classic/runtime/pom.xml @@ -0,0 +1,62 @@ + + + + quarkus-spring-web-resteasy-classic-parent + io.quarkus + 999-SNAPSHOT + + + 4.0.0 + + quarkus-spring-web-resteasy-classic + Quarkus - Spring Web - RESTEasy Classic - Runtime + + + + io.quarkus + quarkus-resteasy + + + org.jboss.resteasy + resteasy-spring-web + + + javax.enterprise + cdi-api + + + jakarta.activation + jakarta.activation-api + + + + + io.quarkus + quarkus-spring-web-common + + + + + + + io.quarkus + quarkus-bootstrap-maven-plugin + + + process-resources + + extension-descriptor + + + + io.quarkus:quarkus-resteasy-jackson + + + + + + + + diff --git a/extensions/spring-web/resteasy-classic/runtime/src/main/java/io/quarkus/spring/web/runtime/ResteasyClassicResponseContentTypeResolver.java b/extensions/spring-web/resteasy-classic/runtime/src/main/java/io/quarkus/spring/web/runtime/ResteasyClassicResponseContentTypeResolver.java new file mode 100644 index 0000000000000..298282d10b046 --- /dev/null +++ b/extensions/spring-web/resteasy-classic/runtime/src/main/java/io/quarkus/spring/web/runtime/ResteasyClassicResponseContentTypeResolver.java @@ -0,0 +1,21 @@ +package io.quarkus.spring.web.runtime; + +import java.util.List; + +import javax.ws.rs.core.Variant; + +import org.jboss.resteasy.core.request.ServerDrivenNegotiation; + +import io.quarkus.spring.web.runtime.common.AbstractResponseContentTypeResolver; + +@SuppressWarnings("unused") +public class ResteasyClassicResponseContentTypeResolver extends AbstractResponseContentTypeResolver { + + @Override + protected Variant negotiateBestMatch(List acceptHeaders, List variants) { + ServerDrivenNegotiation negotiation = new ServerDrivenNegotiation(); + negotiation.setAcceptHeaders(acceptHeaders); + + return negotiation.getBestMatch(variants); + } +} diff --git a/extensions/spring-web/resteasy-classic/runtime/src/main/java/io/quarkus/spring/web/runtime/ResteasyClassicResponseStatusExceptionMapper.java b/extensions/spring-web/resteasy-classic/runtime/src/main/java/io/quarkus/spring/web/runtime/ResteasyClassicResponseStatusExceptionMapper.java new file mode 100644 index 0000000000000..e0f9c53f6c715 --- /dev/null +++ b/extensions/spring-web/resteasy-classic/runtime/src/main/java/io/quarkus/spring/web/runtime/ResteasyClassicResponseStatusExceptionMapper.java @@ -0,0 +1,16 @@ +package io.quarkus.spring.web.runtime; + +import javax.ws.rs.core.Response; + +import org.jboss.resteasy.specimpl.ResponseBuilderImpl; +import org.springframework.web.server.ResponseStatusException; + +import io.quarkus.spring.web.runtime.common.AbstractResponseStatusExceptionMapper; + +public class ResteasyClassicResponseStatusExceptionMapper extends AbstractResponseStatusExceptionMapper { + + @Override + protected Response.ResponseBuilder createResponseBuilder(ResponseStatusException exception) { + return new ResponseBuilderImpl().status(exception.getStatus().value()); + } +} diff --git a/extensions/spring-web/resteasy-classic/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/spring-web/resteasy-classic/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 0000000000000..8524de86240e9 --- /dev/null +++ b/extensions/spring-web/resteasy-classic/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,5 @@ +--- +artifact: ${project.groupId}:${project.artifactId}:${project.version} +name: "Spring Web RESTEasy Classic" +metadata: + unlisted: true diff --git a/extensions/spring-web/runtime/src/main/java/io/quarkus/spring/web/runtime/ResponseEntityConverter.java b/extensions/spring-web/runtime/src/main/java/io/quarkus/spring/web/runtime/ResponseEntityConverter.java deleted file mode 100644 index 13e6e0f6baf4c..0000000000000 --- a/extensions/spring-web/runtime/src/main/java/io/quarkus/spring/web/runtime/ResponseEntityConverter.java +++ /dev/null @@ -1,41 +0,0 @@ -package io.quarkus.spring.web.runtime; - -import java.lang.annotation.Annotation; -import java.util.List; -import java.util.Map; - -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; - -import org.jboss.resteasy.core.Headers; -import org.jboss.resteasy.specimpl.BuiltResponse; -import org.springframework.http.HttpHeaders; -import org.springframework.http.ResponseEntity; - -/** - * This is only used in the generated ExceptionMappers when the Spring @RestControllerAdvice method returns a ResponseEntity - */ -public class ResponseEntityConverter { - - public static Response toResponse(ResponseEntity responseEntity, MediaType defaultContentType) { - return new BuiltResponse(responseEntity.getStatusCodeValue(), - addContentTypeIfMissing(toJaxRsHeaders(responseEntity.getHeaders()), defaultContentType), - responseEntity.getBody(), - new Annotation[0]); - } - - private static Headers toJaxRsHeaders(HttpHeaders springHeaders) { - Headers jaxRsHeaders = new Headers<>(); - for (Map.Entry> entry : springHeaders.entrySet()) { - jaxRsHeaders.addAll(entry.getKey(), entry.getValue().toArray(new Object[0])); - } - return jaxRsHeaders; - } - - private static Headers addContentTypeIfMissing(Headers headers, MediaType contentType) { - if (!headers.containsKey(HttpHeaders.CONTENT_TYPE)) { - headers.add(HttpHeaders.CONTENT_TYPE, contentType); - } - return headers; - } -} diff --git a/integration-tests/spring-web/pom.xml b/integration-tests/spring-web/pom.xml index b93ed5d7b6b5a..2409d1061bc05 100644 --- a/integration-tests/spring-web/pom.xml +++ b/integration-tests/spring-web/pom.xml @@ -38,6 +38,10 @@ io.quarkus quarkus-resteasy-jaxb + + io.quarkus + quarkus-resteasy-jackson + io.quarkus quarkus-hibernate-validator @@ -99,6 +103,19 @@ + + io.quarkus + quarkus-resteasy-jackson-deployment + ${project.version} + pom + test + + + * + * + + + io.quarkus quarkus-resteasy-jaxb-deployment