From 6f1340450da26a4e28df0dafae3838cd9053cbe4 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Sun, 26 Sep 2021 22:50:30 +0300 Subject: [PATCH] Revert "Port Spring Web to RESTEasy Reactive" This partially reverts commit bf552574f5db55d571889dcd634fa5b170fcaba1 and completely reverts commit 76768cdb380bf1ea1a09f1caed44936684413d05. --- docs/src/main/asciidoc/spring-web.adoc | 8 +- extensions/spring-web/deployment/pom.xml | 10 +- .../AbstractExceptionMapperGenerator.java | 14 +- ...viceAbstractExceptionMapperGenerator.java} | 113 +++- .../web/deployment/SpringWebProcessor.java | 627 +++++++----------- ...ResponseStatusAndExceptionHandlerTest.java | 31 +- .../web/test/basic/BasicMappingTest.java | 226 ------- .../spring/web/test/basic/Greeting.java | 14 - ...reetingControllerWithNoRequestMapping.java | 13 - .../test/basic/ResponseEntityController.java | 33 - .../test/basic/ResponseStatusController.java | 26 - .../spring/web/test/basic/SomeClass.java | 20 - .../spring/web/test/basic/TestController.java | 110 --- extensions/spring-web/runtime/pom.xml | 20 +- .../runtime/ResponseContentTypeResolver.java | 2 +- .../web/runtime/ResponseEntityConverter.java | 43 +- .../web/runtime/ResponseEntityHandler.java | 17 - .../ResponseStatusExceptionMapper.java | 3 +- .../web/runtime/ResponseStatusHandler.java | 37 -- integration-tests/spring-web/pom.xml | 34 + .../java/io/quarkus/it/spring/web/Book.java | 24 + .../quarkus/it/spring/web/BookController.java | 14 + .../quarkus/it/spring/web/CustomAdvice.java | 9 +- .../java/io/quarkus/it/spring/web/Error.java | 3 + .../it/spring/web/ExceptionHandlingTest.java | 58 ++ .../it/spring/web/SpringControllerTest.java | 9 + 26 files changed, 560 insertions(+), 958 deletions(-) rename extensions/spring-web/deployment/src/main/java/io/quarkus/spring/web/deployment/{ControllerAdviceExceptionMapperGenerator.java => ControllerAdviceAbstractExceptionMapperGenerator.java} (58%) delete mode 100644 extensions/spring-web/deployment/src/test/java/io/quarkus/spring/web/test/basic/BasicMappingTest.java delete mode 100644 extensions/spring-web/deployment/src/test/java/io/quarkus/spring/web/test/basic/Greeting.java delete mode 100644 extensions/spring-web/deployment/src/test/java/io/quarkus/spring/web/test/basic/GreetingControllerWithNoRequestMapping.java delete mode 100644 extensions/spring-web/deployment/src/test/java/io/quarkus/spring/web/test/basic/ResponseEntityController.java delete mode 100644 extensions/spring-web/deployment/src/test/java/io/quarkus/spring/web/test/basic/ResponseStatusController.java delete mode 100644 extensions/spring-web/deployment/src/test/java/io/quarkus/spring/web/test/basic/SomeClass.java delete mode 100644 extensions/spring-web/deployment/src/test/java/io/quarkus/spring/web/test/basic/TestController.java delete mode 100644 extensions/spring-web/runtime/src/main/java/io/quarkus/spring/web/runtime/ResponseEntityHandler.java delete mode 100644 extensions/spring-web/runtime/src/main/java/io/quarkus/spring/web/runtime/ResponseStatusHandler.java create mode 100644 integration-tests/spring-web/src/main/java/io/quarkus/it/spring/web/Book.java create mode 100644 integration-tests/spring-web/src/main/java/io/quarkus/it/spring/web/BookController.java diff --git a/docs/src/main/asciidoc/spring-web.adoc b/docs/src/main/asciidoc/spring-web.adoc index 663dee48c4074..5d436ccf2262e 100644 --- a/docs/src/main/asciidoc/spring-web.adoc +++ b/docs/src/main/asciidoc/spring-web.adoc @@ -429,6 +429,12 @@ The following method return types are supported: * POJO classes which will be serialized via JSON * `org.springframework.http.ResponseEntity` +=== Controller method parameter types + +In addition to the method parameters that can be annotated with the appropriate Spring Web annotations from the previous table, +`javax.servlet.http.HttpServletRequest` and `javax.servlet.http.HttpServletResponse` are also supported. +For this to function however, users need to add the `quarkus-undertow` dependency. + === Exception handler method return types The following method return types are supported: @@ -443,7 +449,7 @@ Other return types mentioned in the Spring `https://docs.spring.io/spring-framew The following parameter types are supported, in arbitrary order: * An exception argument: declared as a general `Exception` or as a more specific exception. This also serves as a mapping hint if the annotation itself does not narrow the exception types through its `value()`. -* The following JAX-RS Types: `javax.ws.rs.core.Request` and `javax.ws.rs.core.UriInfo` +* Request and/or response objects (typically from the Servlet API). You may choose any specific request/response type, e.g. `ServletRequest` / `HttpServletRequest`. To use Servlet API, the `quarkus-undertow` dependency needs to be added. Other parameter types mentioned in the Spring `https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/bind/annotation/ExceptionHandler.html[ExceptionHandler javadoc]` are not supported. diff --git a/extensions/spring-web/deployment/pom.xml b/extensions/spring-web/deployment/pom.xml index 9b976c5ce45cc..17ad4a9d67ee8 100644 --- a/extensions/spring-web/deployment/pom.xml +++ b/extensions/spring-web/deployment/pom.xml @@ -24,7 +24,15 @@ io.quarkus - quarkus-resteasy-reactive-jackson-deployment + quarkus-resteasy-jackson-deployment + + + io.quarkus + quarkus-jaxrs-spi-deployment + + + io.quarkus + quarkus-resteasy-common-spi io.quarkus diff --git a/extensions/spring-web/deployment/src/main/java/io/quarkus/spring/web/deployment/AbstractExceptionMapperGenerator.java b/extensions/spring-web/deployment/src/main/java/io/quarkus/spring/web/deployment/AbstractExceptionMapperGenerator.java index a99dd0eb54e97..8c50809a7612a 100644 --- a/extensions/spring-web/deployment/src/main/java/io/quarkus/spring/web/deployment/AbstractExceptionMapperGenerator.java +++ b/extensions/spring-web/deployment/src/main/java/io/quarkus/spring/web/deployment/AbstractExceptionMapperGenerator.java @@ -5,6 +5,7 @@ import javax.ws.rs.core.Response; import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationValue; @@ -35,6 +36,8 @@ abstract class AbstractExceptionMapperGenerator { String generate() { String generatedClassName = "io.quarkus.spring.web.mappers." + exceptionDotName.withoutPackagePrefix() + "_Mapper_" + HashUtil.sha1(exceptionDotName.toString()); + String generatedSubtypeClassName = "io.quarkus.spring.web.mappers.Subtype" + exceptionDotName.withoutPackagePrefix() + + "Mapper_" + HashUtil.sha1(exceptionDotName.toString()); String exceptionClassName = exceptionDotName.toString(); try (ClassCreator cc = ClassCreator.builder() @@ -61,7 +64,15 @@ String generate() { } } - return generatedClassName; + // additionally generate a dummy subtype to get past the RESTEasy's ExceptionMapper check for synthetic classes + try (ClassCreator cc = ClassCreator.builder() + .classOutput(classOutput).className(generatedSubtypeClassName) + .superClass(generatedClassName) + .build()) { + cc.addAnnotation(Provider.class); + } + + return generatedSubtypeClassName; } protected void preGenerateMethodBody(ClassCreator cc) { @@ -82,7 +93,6 @@ protected int getHttpStatusFromAnnotation(AnnotationInstance responseStatusInsta return 500; // the default value of @ResponseStatus } - @SuppressWarnings({ "rawtypes", "unchecked" }) private int enumValueToHttpStatus(String enumValue) { try { Class httpStatusClass = Class.forName("org.springframework.http.HttpStatus"); diff --git a/extensions/spring-web/deployment/src/main/java/io/quarkus/spring/web/deployment/ControllerAdviceExceptionMapperGenerator.java b/extensions/spring-web/deployment/src/main/java/io/quarkus/spring/web/deployment/ControllerAdviceAbstractExceptionMapperGenerator.java similarity index 58% rename from extensions/spring-web/deployment/src/main/java/io/quarkus/spring/web/deployment/ControllerAdviceExceptionMapperGenerator.java rename to extensions/spring-web/deployment/src/main/java/io/quarkus/spring/web/deployment/ControllerAdviceAbstractExceptionMapperGenerator.java index e3ecd10eb07a8..346407d9ca411 100644 --- a/extensions/spring-web/deployment/src/main/java/io/quarkus/spring/web/deployment/ControllerAdviceExceptionMapperGenerator.java +++ b/extensions/spring-web/deployment/src/main/java/io/quarkus/spring/web/deployment/ControllerAdviceAbstractExceptionMapperGenerator.java @@ -1,14 +1,18 @@ package io.quarkus.spring.web.deployment; import java.lang.annotation.Annotation; +import java.lang.reflect.Modifier; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.core.Context; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Request; import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriInfo; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.DotName; @@ -18,23 +22,26 @@ import io.quarkus.arc.Arc; import io.quarkus.arc.ArcContainer; import io.quarkus.arc.InstanceHandle; +import io.quarkus.gizmo.ClassCreator; import io.quarkus.gizmo.ClassOutput; +import io.quarkus.gizmo.FieldCreator; +import io.quarkus.gizmo.FieldDescriptor; 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 ControllerAdviceExceptionMapperGenerator extends AbstractExceptionMapperGenerator { +class ControllerAdviceAbstractExceptionMapperGenerator extends AbstractExceptionMapperGenerator { private static final DotName RESPONSE_ENTITY = DotName.createSimple("org.springframework.http.ResponseEntity"); // Preferred content types order for String or primitive type responses private static final List TEXT_MEDIA_TYPES = Arrays.asList( - MediaType.TEXT_PLAIN, MediaType.APPLICATION_JSON); + MediaType.TEXT_PLAIN, MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_XML); // Preferred content types order for object type responses private static final List OBJECT_MEDIA_TYPES = Arrays.asList( - MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN); + MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_XML, MediaType.TEXT_PLAIN); private final MethodInfo controllerAdviceMethod; private final TypesUtil typesUtil; @@ -42,7 +49,11 @@ class ControllerAdviceExceptionMapperGenerator extends AbstractExceptionMapperGe private final List parameterTypes; private final String declaringClassName; - ControllerAdviceExceptionMapperGenerator(MethodInfo controllerAdviceMethod, DotName exceptionDotName, + private final Map parameterTypeToField = new HashMap<>(); + + private FieldDescriptor httpHeadersField; + + ControllerAdviceAbstractExceptionMapperGenerator(MethodInfo controllerAdviceMethod, DotName exceptionDotName, ClassOutput classOutput, TypesUtil typesUtil) { super(exceptionDotName, classOutput); this.controllerAdviceMethod = controllerAdviceMethod; @@ -53,6 +64,69 @@ class ControllerAdviceExceptionMapperGenerator extends AbstractExceptionMapperGe this.declaringClassName = controllerAdviceMethod.declaringClass().name().toString(); } + /** + * We need to go through each parameter of the method of the ControllerAdvice + * and make sure it's supported + * The javax.ws.rs.ext.ExceptionMapper only has one parameter, the exception, however + * other parameters can be obtained using @Context and therefore injected into the target method + */ + @Override + protected void preGenerateMethodBody(ClassCreator cc) { + int notAllowedParameterIndex = -1; + for (int i = 0; i < parameterTypes.size(); i++) { + Type parameterType = parameterTypes.get(i); + DotName parameterTypeDotName = parameterType.name(); + if (typesUtil.isAssignable(Exception.class, parameterTypeDotName)) { + // do nothing since this will be handled during in generateMethodBody + } else if (typesUtil.isAssignable(HttpServletRequest.class, parameterTypeDotName)) { + if (parameterTypeToField.containsKey(parameterType)) { + throw new IllegalArgumentException("Parameter type " + parameterTypes.get(notAllowedParameterIndex).name() + + " is being used multiple times in method" + controllerAdviceMethod.name() + " of class" + + controllerAdviceMethod.declaringClass().name()); + } + + // we need to generate a field that injects the HttpServletRequest into the class + FieldCreator httpRequestFieldCreator = cc.getFieldCreator("httpServletRequest", HttpServletRequest.class) + .setModifiers(Modifier.PRIVATE); + httpRequestFieldCreator.addAnnotation(Context.class); + + // stash the fieldCreator in a map indexed by the parameter type so we can retrieve it later + parameterTypeToField.put(parameterType, httpRequestFieldCreator.getFieldDescriptor()); + } else if (typesUtil.isAssignable(HttpServletResponse.class, parameterTypeDotName)) { + if (parameterTypeToField.containsKey(parameterType)) { + throw new IllegalArgumentException("Parameter type " + parameterTypes.get(notAllowedParameterIndex).name() + + " is being used multiple times in method" + controllerAdviceMethod.name() + " of class" + + controllerAdviceMethod.declaringClass().name()); + } + + // we need to generate a field that injects the HttpServletRequest into the class + FieldCreator httpRequestFieldCreator = cc.getFieldCreator("httpServletResponse", HttpServletResponse.class) + .setModifiers(Modifier.PRIVATE); + httpRequestFieldCreator.addAnnotation(Context.class); + + // stash the fieldCreator in a map indexed by the parameter type so we can retrieve it later + parameterTypeToField.put(parameterType, httpRequestFieldCreator.getFieldDescriptor()); + } else { + notAllowedParameterIndex = i; + } + } + if (notAllowedParameterIndex >= 0) { + throw new IllegalArgumentException( + "Parameter type " + parameterTypes.get(notAllowedParameterIndex).name() + " is not supported for method" + + controllerAdviceMethod.name() + " of class" + controllerAdviceMethod.declaringClass().name()); + } + + createHttpHeadersField(cc); + } + + private void createHttpHeadersField(ClassCreator classCreator) { + FieldCreator httpHeadersFieldCreator = classCreator + .getFieldCreator("httpHeaders", HttpHeaders.class) + .setModifiers(Modifier.PRIVATE); + httpHeadersFieldCreator.addAnnotation(Context.class); + httpHeadersField = httpHeadersFieldCreator.getFieldDescriptor(); + } + @Override void generateMethodBody(MethodCreator toResponse) { if (isVoidType(returnType)) { @@ -116,7 +190,7 @@ private ResultHandle getResponseContentType(MethodCreator methodCreator, List MAPPING_ANNOTATIONS = List.of(REQUEST_MAPPING, GET_MAPPING, POST_MAPPING, - PUT_MAPPING, DELETE_MAPPING, PATCH_MAPPING); - private static final DotName PATH_VARIABLE = DotName.createSimple("org.springframework.web.bind.annotation.PathVariable"); - private static final DotName REQUEST_PARAM = DotName.createSimple("org.springframework.web.bind.annotation.RequestParam"); - private static final DotName REQUEST_HEADER = DotName.createSimple("org.springframework.web.bind.annotation.RequestHeader"); - private static final DotName COOKIE_VALUE = DotName.createSimple("org.springframework.web.bind.annotation.CookieValue"); - private static final DotName MATRIX_VARIABLE = DotName - .createSimple("org.springframework.web.bind.annotation.MatrixVariable"); + + 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"); @@ -100,10 +98,8 @@ public class SpringWebProcessor { 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 = Set.of( - MODEL_AND_VIEW, VIEW, MODEL, HTTP_ENTITY); - - private static final String DEFAULT_NONE = "\n\t\t\n\t\t\n\uE000\uE001\uE002\n\t\t\t\t\n"; // from ValueConstants + private static final Set DISALLOWED_EXCEPTION_CONTROLLER_RETURN_TYPES = new HashSet<>(Arrays.asList( + MODEL_AND_VIEW, VIEW, MODEL, HTTP_ENTITY)); @BuildStep FeatureBuildItem registerFeature() { @@ -111,16 +107,31 @@ FeatureBuildItem registerFeature() { } @BuildStep - public AdditionalJaxRsResourceMethodAnnotationsBuildItem additionalJaxRsResourceMethodAnnotationsBuildItem() { + public IgnoredServletContainerInitializerBuildItem ignoreSpringServlet() { + return new IgnoredServletContainerInitializerBuildItem("org.springframework.web.SpringServletContainerInitializer"); + } - // This is useful mainly to let Hibernate Validator know that methods annotated with these annotations - // are to be validated differently, - // e.g. by yielding HTTP status 400 instead of 500 on constraint violation for a method parameter. - // The effect on RestEasy itself is negligible: it will only register the annotated methods for reflection, - // which we already do. + @BuildStep + public AdditionalJaxRsResourceDefiningAnnotationBuildItem additionalJaxRsResourceDefiningAnnotation() { + 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"), + PATH_VARIABLE, + DotName.createSimple("org.springframework.web.bind.annotation.RequestBody"), + DotName.createSimple("org.springframework.web.bind.annotation.MatrixVariable"), + DotName.createSimple("org.springframework.web.bind.annotation.RequestHeader"), + DotName.createSimple("org.springframework.web.bind.annotation.CookieValue"))); + } + @BuildStep public void ignoreReflectionHierarchy(BuildProducer ignore) { ignore.produce(new ReflectiveHierarchyIgnoreWarningBuildItem( @@ -142,320 +153,242 @@ public void beanDefiningAnnotations(BuildProducer additionalResourceClassProducer) { + public void process(BeanArchiveIndexBuildItem beanArchiveIndexBuildItem, + BuildProducer reflectiveClass, + BuildProducer initParamProducer, + BuildProducer deploymentCustomizerProducer) { - validateControllers(index.getIndex()); + validateControllers(beanArchiveIndexBuildItem); - for (AnnotationInstance restController : index.getIndex() - .getAnnotations(REST_CONTROLLER_ANNOTATION)) { - ClassInfo targetClass = restController.target().asClass(); - additionalResourceClassProducer.produce(new AdditionalResourceClassBuildItem(targetClass, - getSinglePathOfInstance(targetClass.classAnnotation(REQUEST_MAPPING), ""))); + final IndexView index = beanArchiveIndexBuildItem.getIndex(); + final Collection annotations = index.getAnnotations(REST_CONTROLLER_ANNOTATION); + if (annotations.isEmpty()) { + return; } - } - - @BuildStep - public void methodAnnotationsTransformer(BuildProducer producer) { - producer.produce(new AnnotationsTransformerBuildItem(new AnnotationsTransformer() { + final Set classNames = new HashSet<>(); + for (AnnotationInstance annotation : annotations) { + classNames.add(annotation.target().asClass().toString()); + } + // initialize the init params that will be used in case of servlet + initParamProducer.produce( + new ServletInitParamBuildItem( + ResteasyContextParameters.RESTEASY_SCANNED_RESOURCE_CLASSES_WITH_BUILDER, + SpringResourceBuilder.class.getName() + ":" + String.join(",", classNames))); + // customize the deployment that will be used in case of RESTEasy standalone + deploymentCustomizerProducer.produce(new ResteasyDeploymentCustomizerBuildItem(new Consumer() { @Override - public boolean appliesTo(AnnotationTarget.Kind kind) { - return kind == AnnotationTarget.Kind.METHOD; + public void accept(ResteasyDeployment resteasyDeployment) { + resteasyDeployment.getScannedResourceClassesWithBuilder().put(SpringResourceBuilder.class.getName(), + new ArrayList<>(classNames)); } + })); - @Override - public void transform(TransformationContext transformationContext) { - AnnotationTarget target = transformationContext.getTarget(); - if (target.kind() != AnnotationTarget.Kind.METHOD) { - return; - } - MethodInfo methodInfo = target.asMethod(); - Transformation transform = transformationContext.transform(); - DotName jaxRSMethodAnnotation = null; - String path = null; - String[] produces = null; - String[] consumes = null; - - AnnotationInstance mappingAnnotationInstance = methodInfo.annotation(REQUEST_MAPPING); - if (mappingAnnotationInstance != null) { - AnnotationValue methodValue = mappingAnnotationInstance.value("method"); - if (methodValue == null) { - jaxRSMethodAnnotation = ResteasyReactiveDotNames.GET; - } else { - String[] methods = methodValue.asEnumArray(); - if (methods.length > 1) { - throw new IllegalArgumentException( - "Usage of multiple methods using '@RequestMapping' is not allowed. Offending method is '" - + methodInfo.declaringClass().name() + "#" + methodInfo.name() + "'"); - } - DotName methodDotName = ResteasyReactiveScanner.METHOD_TO_BUILTIN_HTTP_ANNOTATIONS.get(methods[0]); - if (methodDotName == null) { - throw new IllegalArgumentException( - "Unsupported HTTP method '" + methods[0] + "' for @RequestMapping. Offending method is '" - + methodInfo.declaringClass().name() + "#" + methodInfo.name() + "'"); - } - jaxRSMethodAnnotation = methodDotName; - } - } else { - if (methodInfo.hasAnnotation(GET_MAPPING)) { - jaxRSMethodAnnotation = ResteasyReactiveDotNames.GET; - mappingAnnotationInstance = methodInfo.annotation(GET_MAPPING); - } else if (methodInfo.hasAnnotation(POST_MAPPING)) { - jaxRSMethodAnnotation = ResteasyReactiveDotNames.POST; - mappingAnnotationInstance = methodInfo.annotation(POST_MAPPING); - } else if (methodInfo.hasAnnotation(PUT_MAPPING)) { - jaxRSMethodAnnotation = ResteasyReactiveDotNames.PUT; - mappingAnnotationInstance = methodInfo.annotation(PUT_MAPPING); - } else if (methodInfo.hasAnnotation(DELETE_MAPPING)) { - jaxRSMethodAnnotation = ResteasyReactiveDotNames.DELETE; - mappingAnnotationInstance = methodInfo.annotation(DELETE_MAPPING); - } else if (methodInfo.hasAnnotation(PATCH_MAPPING)) { - jaxRSMethodAnnotation = ResteasyReactiveDotNames.PATCH; - mappingAnnotationInstance = methodInfo.annotation(PATCH_MAPPING); - } - } + reflectiveClass.produce(new ReflectiveClassBuildItem(true, false, false, SpringResourceBuilder.class.getName())); + } - if (jaxRSMethodAnnotation == null) { - return; + /** + * Make sure the controllers have the proper annotation and warn if not + */ + private void validateControllers(BeanArchiveIndexBuildItem beanArchiveIndexBuildItem) { + Set classesWithoutRestController = new HashSet<>(); + for (DotName mappingAnnotation : MAPPING_ANNOTATIONS) { + Collection annotations = beanArchiveIndexBuildItem.getIndex().getAnnotations(mappingAnnotation); + for (AnnotationInstance annotation : annotations) { + ClassInfo targetClass; + if (annotation.target().kind() == AnnotationTarget.Kind.CLASS) { + targetClass = annotation.target().asClass(); + } else if (annotation.target().kind() == AnnotationTarget.Kind.METHOD) { + targetClass = annotation.target().asMethod().declaringClass(); + } else { + continue; } - produces = getStringArrayValueOfInstance(mappingAnnotationInstance, "produces"); - consumes = getStringArrayValueOfInstance(mappingAnnotationInstance, "consumes"); - path = getSinglePathOfInstance(mappingAnnotationInstance, null); - - transform.add(jaxRSMethodAnnotation); - addStringArrayValuedAnnotation(transform, target, consumes, ResteasyReactiveDotNames.CONSUMES); - addStringArrayValuedAnnotation(transform, target, produces, ResteasyReactiveDotNames.PRODUCES); - addPathAnnotation(transform, target, path); - - for (AnnotationInstance annotation : methodInfo.annotations()) { - if (annotation.target().kind() == AnnotationTarget.Kind.METHOD_PARAMETER) { - DotName annotationName = annotation.name(); - //TODO: add Cookie and Matrix handling - if (annotationName.equals(REQUEST_PARAM) - || annotationName.equals(REQUEST_HEADER) - || annotationName.equals(COOKIE_VALUE) - || annotationName.equals(MATRIX_VARIABLE)) { - - DotName jaxRsAnnotation; - if (annotationName.equals(REQUEST_PARAM)) { - jaxRsAnnotation = REST_QUERY_PARAM; - } else if (annotationName.equals(REQUEST_HEADER)) { - jaxRsAnnotation = REST_QUERY_PARAM; - } else if (annotationName.equals(COOKIE_VALUE)) { - jaxRsAnnotation = REST_COOKIE_PARAM; - } else { - jaxRsAnnotation = REST_MATRIX_PARAM; - } - - String name = getNameOrDefaultFromParamAnnotation(annotation); - List annotationValues; - if (name == null) { - annotationValues = Collections.emptyList(); - - } else { - annotationValues = Collections.singletonList(AnnotationValue.createStringValue("value", name)); - } - transform.add(create(jaxRsAnnotation, annotation.target(), annotationValues)); - - boolean required = true; // the default value - String defaultValueStr = DEFAULT_NONE; // default value of @RequestMapping#defaultValue - AnnotationValue defaultValue = annotation.value("defaultValue"); - if (defaultValue != null) { - defaultValueStr = defaultValue.asString(); - required = false; // implicitly set according to the javadoc of @RequestMapping#defaultValue - } else { - AnnotationValue requiredValue = annotation.value("required"); - if (requiredValue != null) { - required = requiredValue.asBoolean(); - } - } - if (!required) { - transform.add(create(DEFAULT_VALUE, annotation.target(), - Collections - .singletonList(AnnotationValue.createStringValue("value", defaultValueStr)))); - } - } else if (annotationName.equals(PATH_VARIABLE)) { - String name = getNameOrDefaultFromParamAnnotation(annotation); - List annotationValues = Collections.emptyList(); - if (name != null) { - annotationValues = Collections.singletonList(AnnotationValue.createStringValue("value", name)); - } - transform.add(create(REST_PATH_PARAM, annotation.target(), annotationValues)); - } - } + if (targetClass.classAnnotation(REST_CONTROLLER_ANNOTATION) == null) { + classesWithoutRestController.add(targetClass.name()); } - - transform.done(); } + } - private String getNameOrDefaultFromParamAnnotation(AnnotationInstance annotation) { - AnnotationValue nameValue = annotation.value("name"); - if (nameValue != null) { - return nameValue.asString(); - } else { - AnnotationValue value = annotation.value(); - if (value != null) { - return value.asString(); - } - } - return null; + if (!classesWithoutRestController.isEmpty()) { + for (DotName dotName : classesWithoutRestController) { + LOGGER.warn("Class '" + dotName + + "' uses a mapping annotation but the class itself was not annotated with '@RestContoller'. The mappings will therefore be ignored."); } + } + } - private void addStringArrayValuedAnnotation(Transformation transform, AnnotationTarget target, String[] value, - DotName annotationDotName) { - if ((value != null) && value.length > 0) { - AnnotationValue[] values = new AnnotationValue[value.length]; - for (int i = 0; i < values.length; i++) { - values[i] = AnnotationValue.createStringValue("", value[i]); - } - transform.add(AnnotationInstance.create(annotationDotName, target, - new AnnotationValue[] { AnnotationValue.createArrayValue("value", values) })); - } - } + @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; + } - private void addPathAnnotation(Transformation transform, AnnotationTarget target, String path) { - if (path == null) { - return; - } - transform.add(AnnotationInstance.create(ResteasyReactiveDotNames.PATH, target, - new AnnotationValue[] { AnnotationValue.createStringValue("value", replaceSpringWebWildcards(path)) })); - } + Map nonJaxRsPaths = new HashMap<>(); + for (AnnotationInstance restControllerInstance : restControllerAnnotations) { + String basePath = "/"; + ClassInfo restControllerAnnotatedClass = restControllerInstance.target().asClass(); - private String replaceSpringWebWildcards(String methodPath) { - if (methodPath.contains("/**")) { - methodPath = methodPath.replace("/**", "{unsetPlaceHolderVar:.*}"); - } - if (methodPath.contains("/*")) { - methodPath = methodPath.replace("/*", "/{unusedPlaceHolderVar}"); + AnnotationInstance requestMappingInstance = restControllerAnnotatedClass.classAnnotation(REQUEST_MAPPING); + if (requestMappingInstance != null) { + String basePathFromAnnotation = getMappingValue(requestMappingInstance); + if (basePathFromAnnotation != null) { + basePath = basePathFromAnnotation; } - /* - * Spring Web allows the use of '?' to capture a single character. We support this by - * converting each url path using it to a JAX-RS syntax of variable followed by a regex. - * So '/car?/s?o?/info' would become '/{notusedPlaceHolderVar0:car.}/{notusedPlaceHolderVar1:s.o.}/info' - */ - String[] parts = methodPath.split("/"); - if (parts.length > 0) { - StringBuilder sb = new StringBuilder(methodPath.startsWith("/") ? "/" : ""); - for (int i = 0; i < parts.length; i++) { - String part = parts[i]; - if (part.isEmpty()) { - continue; - } - if (!sb.toString().endsWith("/")) { - sb.append("/"); - } - if ((part.startsWith("{") && part.endsWith("}")) || !part.contains("?")) { - sb.append(part); - } else { - sb.append(String.format("{notusedPlaceHolderVar%s:", i)).append(part.replace('?', '.')).append("}"); + } + 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 (methodPath.endsWith("/")) { - sb.append("/"); - } - methodPath = sb.toString(); } - return methodPath; } - })); - } - - // meant to be called with instances of MAPPING_ANNOTATIONS - private static String getSinglePathOfInstance(AnnotationInstance instance, String defaultPathValue) { - String[] paths = getPathsOfInstance(instance); - if ((paths != null) && (paths.length > 0)) { - return paths[0]; + // 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); + } } - return defaultPathValue; - } - // meant to be called with instances of MAPPING_ANNOTATIONS - private static String[] getPathsOfInstance(AnnotationInstance instance) { - if (instance == null) { - return null; - } - AnnotationValue pathValue = instance.value("path"); - if (pathValue != null) { - return pathValue.asStringArray(); - } - AnnotationValue value = instance.value(); - if (value != null) { - return value.asStringArray(); + if (!nonJaxRsPaths.isEmpty()) { + recorder.nonJaxRsClassNameToMethodPaths(nonJaxRsPaths); } - return null; } - // meant to be called with instances of MAPPING_ANNOTATIONS and a property name that contains a String array value - private static String[] getStringArrayValueOfInstance(AnnotationInstance instance, String property) { + /** + * Meant to be called with an instance of any of the MAPPING_CLASSES + */ + private String getMappingValue(AnnotationInstance instance) { if (instance == null) { return null; } - AnnotationValue pathValue = instance.value(property); - if (pathValue != null) { - return pathValue.asStringArray(); + if (instance.value() != null) { + return instance.value().asStringArray()[0]; + } else if (instance.value("path") != null) { + return instance.value("path").asStringArray()[0]; } return null; } - /** - * Make sure the controllers have the proper annotation and warn if not - */ - private void validateControllers(IndexView index) { - Set classesWithoutRestController = new HashSet<>(); - for (DotName mappingAnnotation : MAPPING_ANNOTATIONS) { - Collection annotations = index.getAnnotations(mappingAnnotation); - for (AnnotationInstance annotation : annotations) { - ClassInfo targetClass; - if (annotation.target().kind() == AnnotationTarget.Kind.CLASS) { - targetClass = annotation.target().asClass(); - } else if (annotation.target().kind() == AnnotationTarget.Kind.METHOD) { - targetClass = annotation.target().asMethod().declaringClass(); - } else { - continue; + @BuildStep + public void registerProviders(BeanArchiveIndexBuildItem beanArchiveIndexBuildItem, + BuildProducer providersProducer) throws IOException { + + //TODO only read this information once since it is exactly the same in ResteasyCommonProcessor#setupProviders + final Set availableProviders = ServiceUtil.classNamesNamedIn(getClass().getClassLoader(), + "META-INF/services/" + Providers.class.getName()); + + final MediaTypeMap categorizedReaders = new MediaTypeMap<>(); + final MediaTypeMap categorizedWriters = new MediaTypeMap<>(); + final MediaTypeMap categorizedContextResolvers = new MediaTypeMap<>(); + final Set otherProviders = new HashSet<>(); + + ResteasyCommonProcessor.categorizeProviders(availableProviders, categorizedReaders, categorizedWriters, + categorizedContextResolvers, + otherProviders); + + boolean useAllAvailable = false; + Set providersToRegister = new HashSet<>(); + + OUTER: for (DotName mappingClass : MAPPING_ANNOTATIONS) { + final Collection instances = beanArchiveIndexBuildItem.getIndex().getAnnotations(mappingClass); + for (AnnotationInstance instance : instances) { + if (collectProviders(providersToRegister, categorizedWriters, instance, "produces")) { + useAllAvailable = true; + break OUTER; + } + if (collectProviders(providersToRegister, categorizedContextResolvers, instance, "produces")) { + useAllAvailable = true; + break OUTER; } - if (targetClass.classAnnotation(REST_CONTROLLER_ANNOTATION) == null) { - classesWithoutRestController.add(targetClass.name()); + if (collectProviders(providersToRegister, categorizedReaders, instance, "consumes")) { + useAllAvailable = true; + break OUTER; } } } - if (!classesWithoutRestController.isEmpty()) { - for (DotName dotName : classesWithoutRestController) { - LOGGER.warn("Class '" + dotName - + "' uses a mapping annotation but the class itself was not annotated with '@RestController'. The mappings will therefore be ignored."); - } + if (useAllAvailable) { + providersToRegister = availableProviders; + } else { + // for Spring Web we register all the json providers by default because using "produces" in @RequestMapping + // and friends is optional + providersToRegister.addAll(categorizedWriters.getPossible(MediaType.APPLICATION_JSON_TYPE)); + // we also need to register the custom Spring related providers + providersToRegister.add(ResponseEntityFeature.class.getName()); + providersToRegister.add(ResponseStatusFeature.class.getName()); + } + + for (String provider : providersToRegister) { + providersProducer.produce(new ResteasyJaxrsProviderBuildItem(provider)); } } - @BuildStep - public void registerStandardExceptionMappers(BuildProducer producer) { - producer.produce(new ExceptionMapperBuildItem(ResponseStatusExceptionMapper.class.getName(), - ResponseStatusException.class.getName(), Priorities.USER, false)); + private boolean collectProviders(Set providersToRegister, MediaTypeMap categorizedProviders, + AnnotationInstance instance, String annotationValueName) { + final AnnotationValue producesValue = instance.value(annotationValueName); + if (producesValue != null) { + for (String value : producesValue.asStringArray()) { + MediaType mediaType = MediaType.valueOf(value); + if (MediaType.WILDCARD_TYPE.equals(mediaType)) { + // exit early if we have the wildcard type + return true; + } + providersToRegister.addAll(categorizedProviders.getPossible(mediaType)); + } + } + return false; } @BuildStep - public void exceptionHandlingSupport(CombinedIndexBuildItem index, - BuildProducer generatedClassProducer, - BuildProducer exceptionMapperProducer, - BuildProducer reflectiveClassProducer, - BuildProducer unremovableBeanProducer) { + public void generateExceptionMapperProviders(BeanArchiveIndexBuildItem beanArchiveIndexBuildItem, + BuildProducer generatedExceptionMappers, + BuildProducer providersProducer, + BuildProducer reflectiveClassProducer) { TypesUtil typesUtil = new TypesUtil(Thread.currentThread().getContextClassLoader()); - ClassOutput classOutput = new GeneratedClassGizmoAdaptor(generatedClassProducer, true); - generateMappersForResponseStatusOnException(exceptionMapperProducer, index.getIndex(), classOutput, typesUtil); - generateMappersForExceptionHandlerInControllerAdvice(exceptionMapperProducer, reflectiveClassProducer, - unremovableBeanProducer, index.getIndex(), - classOutput, + // 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); + generateMappersForExceptionHandlerInControllerAdvice(providersProducer, reflectiveClassProducer, index, classOutput, typesUtil); } - private void generateMappersForResponseStatusOnException(BuildProducer exceptionMapperProducer, + @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); @@ -468,21 +401,18 @@ private void generateMappersForResponseStatusOnException(BuildProducer exceptionMapperProducer, - BuildProducer reflectiveClassProducer, - BuildProducer unremovableBeanProducer, IndexView index, ClassOutput classOutput, + BuildProducer providersProducer, + BuildProducer reflectiveClassProducer, IndexView index, ClassOutput classOutput, TypesUtil typesUtil) { AnnotationInstance controllerAdviceInstance = getSingleControllerAdviceInstance(index); @@ -516,16 +446,12 @@ private void generateMappersForExceptionHandlerInControllerAdvice( // we need to generate one JAX-RS ExceptionMapper per Exception type Type[] handledExceptionTypes = exceptionHandlerInstance.value().asClassArray(); for (Type handledExceptionType : handledExceptionTypes) { - String name = new ControllerAdviceExceptionMapperGenerator(method, handledExceptionType.name(), + String name = new ControllerAdviceAbstractExceptionMapperGenerator(method, handledExceptionType.name(), classOutput, typesUtil).generate(); - exceptionMapperProducer.produce( - new ExceptionMapperBuildItem(name, handledExceptionType.name().toString(), Priorities.USER, false)); + providersProducer.produce(new ResteasyJaxrsProviderBuildItem(name)); } - } - // allow access to HttpHeaders from Arc.container() - unremovableBeanProducer.produce( - UnremovableBeanBuildItem.beanClassNames(ContextProducers.class.getName(), HttpHeaders.class.getName())); + } } private AnnotationInstance getSingleControllerAdviceInstance(IndexView index) { @@ -542,53 +468,4 @@ private AnnotationInstance getSingleControllerAdviceInstance(IndexView index) { return controllerAdviceInstances.iterator().next(); } - @BuildStep - public MethodScannerBuildItem responseEntitySupport() { - return new MethodScannerBuildItem(new MethodScanner() { - @Override - public List scan(MethodInfo method, ClassInfo actualEndpointClass, - Map methodContext) { - DotName returnTypeName = method.returnType().name(); - if (returnTypeName.equals(RESPONSE_ENTITY)) { - return Collections.singletonList(new FixedHandlerChainCustomizer(new ResponseEntityHandler(), - HandlerChainCustomizer.Phase.AFTER_METHOD_INVOKE)); - } - return Collections.emptyList(); - } - }); - } - - @BuildStep - public MethodScannerBuildItem responseStatusSupport() { - return new MethodScannerBuildItem(new MethodScanner() { - @Override - public List scan(MethodInfo method, ClassInfo actualEndpointClass, - Map methodContext) { - AnnotationInstance responseStatus = method.annotation(RESPONSE_STATUS); - if (responseStatus != null) { - int newStatus = Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(); // default value for @ResponseStatus - AnnotationValue codeValue = responseStatus.value("code"); - if (codeValue != null) { - newStatus = HttpStatus.valueOf(codeValue.asEnum()).value(); - } else { - AnnotationValue value = responseStatus.value(); - if (value != null) { - newStatus = HttpStatus.valueOf(value.asEnum()).value(); - } - } - - ResponseStatusHandler handler = new ResponseStatusHandler(); - handler.setNewResponseCode(newStatus); - handler.setDefaultResponseCode( - method.returnType().kind() != Type.Kind.VOID ? Response.Status.OK.getStatusCode() - : Response.Status.NO_CONTENT.getStatusCode()); - return Collections.singletonList( - new FixedHandlerChainCustomizer(handler, HandlerChainCustomizer.Phase.AFTER_RESPONSE_CREATED)); - } - - return Collections.emptyList(); - } - }); - } - } diff --git a/extensions/spring-web/deployment/src/test/java/io/quarkus/spring/web/test/ResponseStatusAndExceptionHandlerTest.java b/extensions/spring-web/deployment/src/test/java/io/quarkus/spring/web/test/ResponseStatusAndExceptionHandlerTest.java index 20c3c8a9cb104..2a0e031905721 100644 --- a/extensions/spring-web/deployment/src/test/java/io/quarkus/spring/web/test/ResponseStatusAndExceptionHandlerTest.java +++ b/extensions/spring-web/deployment/src/test/java/io/quarkus/spring/web/test/ResponseStatusAndExceptionHandlerTest.java @@ -10,6 +10,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -24,33 +25,18 @@ public class ResponseStatusAndExceptionHandlerTest { .addClasses(ExceptionController.class, RestExceptionHandler.class)); @Test - public void testRestControllerAdvice() { + public void testRootResource() { when().get("/exception").then().statusCode(400); } - @Test - public void testResponseStatusOnException() { - when().get("/exception2").then().statusCode(202); - } - @RestController + @RequestMapping("/exception") public static class ExceptionController { - public static final StackTraceElement[] EMPTY_STACK_TRACE = new StackTraceElement[0]; - - @GetMapping("/exception") + @GetMapping @ResponseStatus(HttpStatus.OK) - public String throwRuntimeException() { - RuntimeException runtimeException = new RuntimeException(); - runtimeException.setStackTrace(EMPTY_STACK_TRACE); - throw runtimeException; - } - - @GetMapping("/exception2") - public String throwMyException() { - MyException myException = new MyException(); - myException.setStackTrace(EMPTY_STACK_TRACE); - throw myException; + public String throwException() { + throw new RuntimeException(); } } @@ -62,9 +48,4 @@ public ResponseEntity handleException(Exception ex) { return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } } - - @ResponseStatus(HttpStatus.ACCEPTED) - public static class MyException extends RuntimeException { - - } } diff --git a/extensions/spring-web/deployment/src/test/java/io/quarkus/spring/web/test/basic/BasicMappingTest.java b/extensions/spring-web/deployment/src/test/java/io/quarkus/spring/web/test/basic/BasicMappingTest.java deleted file mode 100644 index 57d27a963ad19..0000000000000 --- a/extensions/spring-web/deployment/src/test/java/io/quarkus/spring/web/test/basic/BasicMappingTest.java +++ /dev/null @@ -1,226 +0,0 @@ -package io.quarkus.spring.web.test.basic; - -import static io.restassured.RestAssured.given; -import static io.restassured.RestAssured.when; -import static org.hamcrest.Matchers.is; - -import org.jboss.shrinkwrap.api.ShrinkWrap; -import org.jboss.shrinkwrap.api.spec.JavaArchive; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; - -import io.quarkus.test.QuarkusUnitTest; - -public class BasicMappingTest { - - @RegisterExtension - static QuarkusUnitTest runner = new QuarkusUnitTest() - .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) - .addClasses(SomeClass.class, Greeting.class, TestController.class, ResponseEntityController.class, - ResponseStatusController.class, GreetingControllerWithNoRequestMapping.class)); - - @Test - public void verifyGetWithQueryParam() { - when().get(TestController.CONTROLLER_PATH + "/hello?name=people") - .then() - .statusCode(200) - .contentType("text/plain") - .body(is("hello people")); - } - - @Test - public void verifyRequestMappingWithNoMethod() { - when().get(TestController.CONTROLLER_PATH + "/hello4?name=people") - .then() - .statusCode(200) - .contentType("text/plain") - .body(is("hello people")); - } - - @Test - public void verifyGetToMethodWithoutForwardSlash() { - when().get(TestController.CONTROLLER_PATH + "/yolo") - .then() - .statusCode(200) - .body(is("yolo")); - } - - @Test - public void verifyGetUsingDefaultValue() { - when().get(TestController.CONTROLLER_PATH + "/hello2") - .then() - .statusCode(200) - .body(is("hello world")); - } - - @Test - public void verifyGetUsingNonLatinChars() { - when().get(TestController.CONTROLLER_PATH + "/hello3?name=Γιώργος") - .then() - .statusCode(200) - .body(is("hello Γιώργος")); - } - - @Test - public void verifyPathWithWildcard() { - when().get(TestController.CONTROLLER_PATH + "/wildcard/whatever/world") - .then() - .statusCode(200) - .body(is("world")); - } - - @Test - public void verifyPathWithMultipleWildcards() { - when().get(TestController.CONTROLLER_PATH + "/wildcard2/something/folks/somethingelse") - .then() - .statusCode(200) - .body(is("folks")); - } - - @Test - public void verifyPathWithAntStyleWildCard() { - when().get(TestController.CONTROLLER_PATH + "/antwildcard/whatever/we/want") - .then() - .statusCode(200) - .body(is("ant")); - } - - @Test - public void verifyPathWithCharacterWildCard() { - for (char c : new char[] { 't', 'r' }) { - when().get(TestController.CONTROLLER_PATH + String.format("/ca%cs", c)) - .then() - .statusCode(200) - .body(is("single")); - } - } - - @Test - public void verifyPathWithMultipleCharacterWildCards() { - for (String path : new String[] { "/cars/shop/info", "/cart/show/info" }) { - when().get(TestController.CONTROLLER_PATH + path) - .then() - .statusCode(200) - .body(is("multiple")); - } - } - - @Test - public void verifyPathVariableTypeConversion() { - when().get(TestController.CONTROLLER_PATH + "/int/9") - .then() - .statusCode(200) - .body(is("10")); - } - - @Test - public void verifyJsonGetWithPathParamAndGettingMapping() { - when().get(TestController.CONTROLLER_PATH + "/json/dummy") - .then() - .statusCode(200) - .contentType("application/json") - .body("message", is("dummy")); - } - - @Test - public void verifyJsonOnRequestMappingGetWithPathParamAndRequestMapping() { - when().get(TestController.CONTROLLER_PATH + "/json2/dummy") - .then() - .statusCode(200) - .contentType("application/json") - .body("message", is("dummy")); - } - - @Test - public void verifyJsonPostWithPostMapping() { - given().body("{\"message\": \"hi\"}") - .contentType("application/json") - .when().post(TestController.CONTROLLER_PATH + "/json") - .then() - .statusCode(200) - .contentType("text/plain") - .body(is("hi")); - } - - @Test - public void verifyJsonPostWithRequestMapping() { - given().body("{\"message\": \"hi\"}") - .contentType("application/json") - .when().post(TestController.CONTROLLER_PATH + "/json2") - .then() - .statusCode(200) - .contentType("text/plain") - .body(is("hi")); - } - - @Test - public void verifyMultipleInputAndJsonResponse() { - given().body("{\"message\": \"hi\"}") - .contentType("application/json") - .when().put(TestController.CONTROLLER_PATH + "/json3?suffix=!") - .then() - .statusCode(200) - .contentType("application/json") - .body("message", is("hi!")); - } - - @Test - public void verifyEmptyContentResponseEntity() { - when().get(ResponseEntityController.CONTROLLER_PATH + "/noContent") - .then() - .statusCode(204); - } - - @Test - public void verifyStringContentResponseEntity() { - when().get(ResponseEntityController.CONTROLLER_PATH + "/string") - .then() - .statusCode(200) - .contentType("text/plain") - .body(is("hello world")); - } - - @Test - public void verifyJsonContentResponseEntity() { - when().get(ResponseEntityController.CONTROLLER_PATH + "/json") - .then() - .statusCode(200) - .contentType("application/json") - .body("message", is("dummy")) - .header("custom-header", "somevalue"); - } - - @Test - public void verifyJsonContentResponseEntityWithoutType() { - when().get(ResponseEntityController.CONTROLLER_PATH + "/json2") - .then() - .statusCode(200) - .contentType("application/json") - .body("message", is("dummy")); - } - - @Test - public void verifyEmptyContentResponseStatus() { - when().get(ResponseStatusController.CONTROLLER_PATH + "/noContent") - .then() - .statusCode(200); - } - - @Test - public void verifyStringResponseStatus() { - when().get(ResponseStatusController.CONTROLLER_PATH + "/string") - .then() - .statusCode(202) - .contentType("text/plain") - .body(is("accepted")); - } - - @Test - public void verifyControllerWithoutRequestMapping() { - when().get("/hello") - .then() - .statusCode(200) - .contentType("text/plain") - .body(is("hello world")); - } -} diff --git a/extensions/spring-web/deployment/src/test/java/io/quarkus/spring/web/test/basic/Greeting.java b/extensions/spring-web/deployment/src/test/java/io/quarkus/spring/web/test/basic/Greeting.java deleted file mode 100644 index 33f4352dee2f3..0000000000000 --- a/extensions/spring-web/deployment/src/test/java/io/quarkus/spring/web/test/basic/Greeting.java +++ /dev/null @@ -1,14 +0,0 @@ -package io.quarkus.spring.web.test.basic; - -public class Greeting { - - private final String message; - - public Greeting(final String message) { - this.message = message; - } - - public String getMessage() { - return message; - } -} diff --git a/extensions/spring-web/deployment/src/test/java/io/quarkus/spring/web/test/basic/GreetingControllerWithNoRequestMapping.java b/extensions/spring-web/deployment/src/test/java/io/quarkus/spring/web/test/basic/GreetingControllerWithNoRequestMapping.java deleted file mode 100644 index a533f63e9648a..0000000000000 --- a/extensions/spring-web/deployment/src/test/java/io/quarkus/spring/web/test/basic/GreetingControllerWithNoRequestMapping.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.quarkus.spring.web.test.basic; - -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -public class GreetingControllerWithNoRequestMapping { - - @GetMapping("/hello") - public String hello() { - return "hello world"; - } -} diff --git a/extensions/spring-web/deployment/src/test/java/io/quarkus/spring/web/test/basic/ResponseEntityController.java b/extensions/spring-web/deployment/src/test/java/io/quarkus/spring/web/test/basic/ResponseEntityController.java deleted file mode 100644 index de9c4faafa738..0000000000000 --- a/extensions/spring-web/deployment/src/test/java/io/quarkus/spring/web/test/basic/ResponseEntityController.java +++ /dev/null @@ -1,33 +0,0 @@ -package io.quarkus.spring.web.test.basic; - -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/" + ResponseEntityController.CONTROLLER_PATH) -public class ResponseEntityController { - - public static final String CONTROLLER_PATH = "re"; - - @GetMapping("/noContent") - public ResponseEntity noContent() { - return ResponseEntity.noContent().build(); - } - - @GetMapping(value = "/string", produces = "text/plain") - public ResponseEntity string() { - return ResponseEntity.ok("hello world"); - } - - @GetMapping(value = "/json", produces = "application/json") - public ResponseEntity jsonPlusHeaders() { - return ResponseEntity.ok().header("custom-header", "somevalue").body(new SomeClass("dummy")); - } - - @GetMapping(value = "/json2", produces = "application/json") - public ResponseEntity responseEntityWithoutType() { - return ResponseEntity.ok().body(new SomeClass("dummy")); - } -} diff --git a/extensions/spring-web/deployment/src/test/java/io/quarkus/spring/web/test/basic/ResponseStatusController.java b/extensions/spring-web/deployment/src/test/java/io/quarkus/spring/web/test/basic/ResponseStatusController.java deleted file mode 100644 index e6c5f96d27ece..0000000000000 --- a/extensions/spring-web/deployment/src/test/java/io/quarkus/spring/web/test/basic/ResponseStatusController.java +++ /dev/null @@ -1,26 +0,0 @@ -package io.quarkus.spring.web.test.basic; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/" + ResponseStatusController.CONTROLLER_PATH) -public class ResponseStatusController { - - public static final String CONTROLLER_PATH = "rs"; - - @GetMapping(produces = "text/plain", path = "/noContent") - @ResponseStatus(HttpStatus.OK) - public void noValueResponseStatus() { - - } - - @GetMapping(produces = "text/plain", path = "/string") - @ResponseStatus(HttpStatus.ACCEPTED) - public String stringWithResponseStatus() { - return "accepted"; - } -} diff --git a/extensions/spring-web/deployment/src/test/java/io/quarkus/spring/web/test/basic/SomeClass.java b/extensions/spring-web/deployment/src/test/java/io/quarkus/spring/web/test/basic/SomeClass.java deleted file mode 100644 index da33bc5c4445e..0000000000000 --- a/extensions/spring-web/deployment/src/test/java/io/quarkus/spring/web/test/basic/SomeClass.java +++ /dev/null @@ -1,20 +0,0 @@ -package io.quarkus.spring.web.test.basic; - -public class SomeClass { - private String message; - - public SomeClass() { - } - - public SomeClass(final String message) { - this.message = message; - } - - public String getMessage() { - return message; - } - - public void setMessage(String message) { - this.message = message; - } -} diff --git a/extensions/spring-web/deployment/src/test/java/io/quarkus/spring/web/test/basic/TestController.java b/extensions/spring-web/deployment/src/test/java/io/quarkus/spring/web/test/basic/TestController.java deleted file mode 100644 index bfbf41296a03b..0000000000000 --- a/extensions/spring-web/deployment/src/test/java/io/quarkus/spring/web/test/basic/TestController.java +++ /dev/null @@ -1,110 +0,0 @@ -package io.quarkus.spring.web.test.basic; - -import javax.ws.rs.core.MediaType; - -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/" + TestController.CONTROLLER_PATH) -public class TestController { - - public static final String CONTROLLER_PATH = "spring"; - - @GetMapping("/hello") - public String string(@RequestParam String name) { - return "hello " + name; - } - - @RequestMapping("/hello4") - public String requestMappingNoMethod(@RequestParam String name) { - return "hello " + name; - } - - @GetMapping("yolo") - public String yolo() { - return "yolo"; - } - - @GetMapping("/hello2") - public String stringWithDefaultParamValue(@RequestParam(name = "name", defaultValue = "world") String name) { - return "hello " + name; - } - - @GetMapping("/hello3") - public String stringWithNameValue(@RequestParam(name = "name") String name) { - return "hello " + name; - } - - @GetMapping("/wildcard/*/{name}") - public String pathWithWildcard(@PathVariable("name") String name) { - return name; - } - - @RequestMapping(value = "/wildcard2/*/{name}/*", method = RequestMethod.GET) - public String pathWithMultipleWildcards(@PathVariable("name") String name) { - return name; - } - - @GetMapping("/antwildcard/**") - public String pathWithAntStyleWildcard() { - return "ant"; - } - - @GetMapping("/ca?s") - public String pathWithCharacterWildCard() { - return "single"; - } - - @GetMapping("/car?/s?o?/info") - public String pathWithMultipleCharacterWildCards() { - return "multiple"; - } - - @GetMapping("/int/{num}") - public Integer intPathVariable(@PathVariable("num") Integer number) { - return number + 1; - } - - @GetMapping("/{msg}") - public String stringPathVariable(@PathVariable("msg") String message) { - return message; - } - - @GetMapping(path = "/json/{message}") - public SomeClass json(@PathVariable String message) { - return new SomeClass(message); - } - - @RequestMapping(method = RequestMethod.GET, path = "/json2/{message}", produces = MediaType.APPLICATION_JSON) - public SomeClass jsonFromRequestMapping(@PathVariable String message) { - return new SomeClass(message); - } - - @PostMapping(path = "/json", produces = MediaType.TEXT_PLAIN, consumes = MediaType.APPLICATION_JSON) - public String postWithJsonBody(@RequestBody SomeClass someClass) { - return someClass.getMessage(); - } - - @RequestMapping(path = "/json2", method = RequestMethod.POST, produces = MediaType.TEXT_PLAIN, consumes = MediaType.APPLICATION_JSON) - public String postWithJsonBodyFromRequestMapping(@RequestBody SomeClass someClass) { - return someClass.getMessage(); - } - - @PutMapping(path = "/json3") - public Greeting multipleInputAndJsonResponse(@RequestBody SomeClass someClass, - @RequestParam(value = "suffix") String suffix) { - return new Greeting(someClass.getMessage() + suffix); - } - - public void doNothing() { - - } -} diff --git a/extensions/spring-web/runtime/pom.xml b/extensions/spring-web/runtime/pom.xml index f1d814b3854af..68f88fa22c41e 100644 --- a/extensions/spring-web/runtime/pom.xml +++ b/extensions/spring-web/runtime/pom.xml @@ -16,7 +16,25 @@ io.quarkus - quarkus-resteasy-reactive-jackson + quarkus-resteasy-jackson + + + org.jboss.resteasy + resteasy-spring-web + + + javax.enterprise + cdi-api + + + jakarta.activation + jakarta.activation-api + + + + + jakarta.enterprise + jakarta.enterprise.cdi-api io.quarkus diff --git a/extensions/spring-web/runtime/src/main/java/io/quarkus/spring/web/runtime/ResponseContentTypeResolver.java b/extensions/spring-web/runtime/src/main/java/io/quarkus/spring/web/runtime/ResponseContentTypeResolver.java index fdcd9398d12d5..db25c1729403c 100644 --- a/extensions/spring-web/runtime/src/main/java/io/quarkus/spring/web/runtime/ResponseContentTypeResolver.java +++ b/extensions/spring-web/runtime/src/main/java/io/quarkus/spring/web/runtime/ResponseContentTypeResolver.java @@ -10,7 +10,7 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Variant; -import org.jboss.resteasy.reactive.server.core.request.ServerDrivenNegotiation; +import org.jboss.resteasy.core.request.ServerDrivenNegotiation; public final class ResponseContentTypeResolver { 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 index ec35c5bc88a7c..13e6e0f6baf4c 100644 --- 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 @@ -1,14 +1,14 @@ 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.MultivaluedMap; import javax.ws.rs.core.Response; -import org.jboss.resteasy.reactive.common.util.MultivaluedTreeMap; -import org.jboss.resteasy.reactive.server.jaxrs.ResponseBuilderImpl; +import org.jboss.resteasy.core.Headers; +import org.jboss.resteasy.specimpl.BuiltResponse; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; @@ -17,34 +17,25 @@ */ public class ResponseEntityConverter { - public static final String[] EMPTY_STRINGS_ARRAY = new String[0]; - - public static Response toResponse(ResponseEntity responseEntity) { - ResponseBuilderImpl responseBuilder = toResponseBuilder(responseEntity); - return responseBuilder.build(); + public static Response toResponse(ResponseEntity responseEntity, MediaType defaultContentType) { + return new BuiltResponse(responseEntity.getStatusCodeValue(), + addContentTypeIfMissing(toJaxRsHeaders(responseEntity.getHeaders()), defaultContentType), + responseEntity.getBody(), + new Annotation[0]); } - @SuppressWarnings("rawtypes") - public static Response toResponse(ResponseEntity responseEntity, MediaType defaultMediaType) { - ResponseBuilderImpl responseBuilder = toResponseBuilder(responseEntity); - if (!responseBuilder.getMetadata().containsKey(HttpHeaders.CONTENT_TYPE)) { - responseBuilder.header(HttpHeaders.CONTENT_TYPE, defaultMediaType.toString()); + 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 responseBuilder.build(); - } - - private static ResponseBuilderImpl toResponseBuilder(ResponseEntity responseEntity) { - ResponseBuilderImpl responseBuilder = new ResponseBuilderImpl(); - responseBuilder.status(responseEntity.getStatusCodeValue()).entity(responseEntity.getBody()); - responseBuilder.setAllHeaders(toJaxRsHeaders(responseEntity.getHeaders())); - return responseBuilder; + return jaxRsHeaders; } - private static MultivaluedMap toJaxRsHeaders(HttpHeaders springHeaders) { - var jaxRsHeaders = new MultivaluedTreeMap(); - for (Map.Entry> entry : springHeaders.entrySet()) { - jaxRsHeaders.addAll(entry.getKey(), entry.getValue().toArray(EMPTY_STRINGS_ARRAY)); + private static Headers addContentTypeIfMissing(Headers headers, MediaType contentType) { + if (!headers.containsKey(HttpHeaders.CONTENT_TYPE)) { + headers.add(HttpHeaders.CONTENT_TYPE, contentType); } - return jaxRsHeaders; + return headers; } } diff --git a/extensions/spring-web/runtime/src/main/java/io/quarkus/spring/web/runtime/ResponseEntityHandler.java b/extensions/spring-web/runtime/src/main/java/io/quarkus/spring/web/runtime/ResponseEntityHandler.java deleted file mode 100644 index f51e3a3380034..0000000000000 --- a/extensions/spring-web/runtime/src/main/java/io/quarkus/spring/web/runtime/ResponseEntityHandler.java +++ /dev/null @@ -1,17 +0,0 @@ -package io.quarkus.spring.web.runtime; - -import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; -import org.jboss.resteasy.reactive.server.spi.ServerRestHandler; -import org.springframework.http.ResponseEntity; - -public class ResponseEntityHandler implements ServerRestHandler { - - @Override - - public void handle(ResteasyReactiveRequestContext requestContext) throws Exception { - Object result = requestContext.getResult(); - if (result instanceof ResponseEntity) { - requestContext.setResult(ResponseEntityConverter.toResponse((ResponseEntity) result)); - } - } -} diff --git a/extensions/spring-web/runtime/src/main/java/io/quarkus/spring/web/runtime/ResponseStatusExceptionMapper.java b/extensions/spring-web/runtime/src/main/java/io/quarkus/spring/web/runtime/ResponseStatusExceptionMapper.java index 70dcfe9bf0dad..2a3f11e997ab5 100644 --- a/extensions/spring-web/runtime/src/main/java/io/quarkus/spring/web/runtime/ResponseStatusExceptionMapper.java +++ b/extensions/spring-web/runtime/src/main/java/io/quarkus/spring/web/runtime/ResponseStatusExceptionMapper.java @@ -7,6 +7,7 @@ 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; @@ -14,7 +15,7 @@ public class ResponseStatusExceptionMapper implements ExceptionMapperio.quarkus quarkus-spring-scheduled + + io.quarkus + quarkus-undertow + + + io.quarkus + quarkus-resteasy-jaxb + io.quarkus quarkus-hibernate-validator @@ -91,6 +99,19 @@ + + io.quarkus + quarkus-resteasy-jaxb-deployment + ${project.version} + pom + test + + + * + * + + + io.quarkus quarkus-spring-cache-deployment @@ -143,6 +164,19 @@ + + io.quarkus + quarkus-undertow-deployment + ${project.version} + pom + test + + + * + * + + + io.quarkus quarkus-smallrye-openapi-deployment diff --git a/integration-tests/spring-web/src/main/java/io/quarkus/it/spring/web/Book.java b/integration-tests/spring-web/src/main/java/io/quarkus/it/spring/web/Book.java new file mode 100644 index 0000000000000..a893ed259da9b --- /dev/null +++ b/integration-tests/spring-web/src/main/java/io/quarkus/it/spring/web/Book.java @@ -0,0 +1,24 @@ +package io.quarkus.it.spring.web; + +import javax.xml.bind.annotation.XmlRootElement; + +@XmlRootElement +public class Book { + + private String name; + + public Book() { + } + + public Book(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/integration-tests/spring-web/src/main/java/io/quarkus/it/spring/web/BookController.java b/integration-tests/spring-web/src/main/java/io/quarkus/it/spring/web/BookController.java new file mode 100644 index 0000000000000..7d2351bcb14b4 --- /dev/null +++ b/integration-tests/spring-web/src/main/java/io/quarkus/it/spring/web/BookController.java @@ -0,0 +1,14 @@ +package io.quarkus.it.spring.web; + +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class BookController { + + @GetMapping(produces = MediaType.APPLICATION_XML_VALUE, path = "/book") + public Book someBook() { + return new Book("Guns germs and steel"); + } +} diff --git a/integration-tests/spring-web/src/main/java/io/quarkus/it/spring/web/CustomAdvice.java b/integration-tests/spring-web/src/main/java/io/quarkus/it/spring/web/CustomAdvice.java index 262a4dffeef70..ce892699c5ee8 100644 --- a/integration-tests/spring-web/src/main/java/io/quarkus/it/spring/web/CustomAdvice.java +++ b/integration-tests/spring-web/src/main/java/io/quarkus/it/spring/web/CustomAdvice.java @@ -1,7 +1,6 @@ package io.quarkus.it.spring.web; -import javax.ws.rs.core.Request; -import javax.ws.rs.core.UriInfo; +import javax.servlet.http.HttpServletRequest; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -25,7 +24,7 @@ public void unannotatedException() { @ExceptionHandler(HandledResponseEntityException.class) public ResponseEntity handleResponseEntityException(HandledResponseEntityException e, - UriInfo uriInfo, Request request) { + HttpServletRequest request) { ResponseEntity.BodyBuilder bodyBuilder = ResponseEntity .status(HttpStatus.PAYMENT_REQUIRED) @@ -35,12 +34,12 @@ public ResponseEntity handleResponseEntityException(HandledResponseEntity bodyBuilder.contentType(e.getContentType()); } - return bodyBuilder.body(new Error(uriInfo.getPath() + ":" + request.getMethod() + ":" + e.getMessage())); + return bodyBuilder.body(new Error(request.getRequestURI() + ":" + e.getMessage())); } @ResponseStatus(HttpStatus.EXPECTATION_FAILED) @ExceptionHandler(HandledPojoException.class) - public Error handlePojoException(HandledPojoException e) { + public Error handlePojoExcepton(HandledPojoException e) { return new Error(e.getMessage()); } diff --git a/integration-tests/spring-web/src/main/java/io/quarkus/it/spring/web/Error.java b/integration-tests/spring-web/src/main/java/io/quarkus/it/spring/web/Error.java index 53347c918a36d..a45058691defb 100644 --- a/integration-tests/spring-web/src/main/java/io/quarkus/it/spring/web/Error.java +++ b/integration-tests/spring-web/src/main/java/io/quarkus/it/spring/web/Error.java @@ -1,5 +1,8 @@ package io.quarkus.it.spring.web; +import javax.xml.bind.annotation.XmlRootElement; + +@XmlRootElement public class Error { private String message; diff --git a/integration-tests/spring-web/src/test/java/io/quarkus/it/spring/web/ExceptionHandlingTest.java b/integration-tests/spring-web/src/test/java/io/quarkus/it/spring/web/ExceptionHandlingTest.java index 09164d08236c1..324a20716f25d 100644 --- a/integration-tests/spring-web/src/test/java/io/quarkus/it/spring/web/ExceptionHandlingTest.java +++ b/integration-tests/spring-web/src/test/java/io/quarkus/it/spring/web/ExceptionHandlingTest.java @@ -36,6 +36,15 @@ public void testHandledRuntimeException() { .statusCode(400); } + @Test + public void testHandledRuntimeExceptionAsXml() { + RestAssured.given().accept("application/xml") + .when().get("/exception/runtime").then() + .contentType("application/xml") + .body(is(emptyString())) + .statusCode(400); + } + @Test public void testHandledUnannotatedException() { RestAssured.when().get("/exception/unannotated").then() @@ -44,6 +53,15 @@ public void testHandledUnannotatedException() { .statusCode(204); } + @Test + public void testHandledUnannotatedExceptionAsXml() { + RestAssured.given().accept("application/xml") + .when().get("/exception/unannotated").then() + .contentType("application/xml") + .body(is(emptyString())) + .statusCode(204); + } + @Test public void testResponseEntityWithResponseEntityException() { RestAssured.when().get("/exception/re/re").then() @@ -74,6 +92,28 @@ public void testVoidWithResponseEntityException() { .header("custom-header", "custom-value"); } + @Test + public void testVoidWithResponseEntityExceptionAsXml() { + RestAssured.given().accept("application/xml") + .when().get("/exception/re/void").then() + .contentType("application/xml") + .body(containsString("bad state")) + .body(containsString("/exception/re/void")) + .statusCode(402) + .header("custom-header", "custom-value"); + } + + @Test + public void testVoidWithResponseEntityExceptionAsHardcodedXml() { + RestAssured.given().accept("application/json") + .when().get("/exception/re/void/xml").then() + .contentType("application/xml") + .body(containsString("bad state")) + .body(containsString("/exception/re/void/xml")) + .statusCode(402) + .header("custom-header", "custom-value"); + } + @Test public void testResponseEntityWithPojoException() { RestAssured.when().get("/exception/pojo/re").then() @@ -98,6 +138,15 @@ public void testVoidWithPojoException() { .statusCode(417); } + @Test + public void testVoidWithPojoExceptionAsXml() { + RestAssured.given().accept("application/xml") + .when().get("/exception/pojo/void").then() + .contentType("application/xml") + .body(containsString("bad state")) + .statusCode(417); + } + @Test public void testStringWithStringException() { RestAssured.when().get("/exception/string").then() @@ -106,6 +155,15 @@ public void testStringWithStringException() { .body(is("bad state")); } + @Test + public void testStringWithStringExceptionAsXml() { + RestAssured.given().accept("application/xml") + .when().get("/exception/string").then() + .statusCode(418) + .contentType("application/xml") + .body(is("bad state")); + } + @Test public void testResponseStatusException() { RestAssured.when().get("/exception/responseStatusException").then() diff --git a/integration-tests/spring-web/src/test/java/io/quarkus/it/spring/web/SpringControllerTest.java b/integration-tests/spring-web/src/test/java/io/quarkus/it/spring/web/SpringControllerTest.java index e191728d0db0b..973667bf3db86 100644 --- a/integration-tests/spring-web/src/test/java/io/quarkus/it/spring/web/SpringControllerTest.java +++ b/integration-tests/spring-web/src/test/java/io/quarkus/it/spring/web/SpringControllerTest.java @@ -49,4 +49,13 @@ public void testRestControllerWithoutRequestMapping() { RestAssured.when().get("/hello").then() .body(containsString("hello")); } + + @Test + public void testMethodReturningXmlContent() { + RestAssured.when().get("/book") + .then() + .statusCode(200) + .contentType("application/xml") + .body(containsString("steel")); + } }