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