diff --git a/docs/src/main/asciidoc/spring-web.adoc b/docs/src/main/asciidoc/spring-web.adoc index 5d436ccf2262e..663dee48c4074 100644 --- a/docs/src/main/asciidoc/spring-web.adoc +++ b/docs/src/main/asciidoc/spring-web.adoc @@ -429,12 +429,6 @@ 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: @@ -449,7 +443,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()`. -* 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. +* The following JAX-RS Types: `javax.ws.rs.core.Request` and `javax.ws.rs.core.UriInfo` 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/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/ResteasyReactiveCommonProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/ResteasyReactiveCommonProcessor.java index 41f0c5d41c517..b8346752064ab 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/ResteasyReactiveCommonProcessor.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/ResteasyReactiveCommonProcessor.java @@ -6,7 +6,9 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.Consumer; @@ -51,6 +53,7 @@ import io.quarkus.resteasy.reactive.common.runtime.JaxRsSecurityConfig; import io.quarkus.resteasy.reactive.common.runtime.ResteasyReactiveConfig; import io.quarkus.resteasy.reactive.spi.AbstractInterceptorBuildItem; +import io.quarkus.resteasy.reactive.spi.AdditionalResourceClassBuildItem; import io.quarkus.resteasy.reactive.spi.ContainerRequestFilterBuildItem; import io.quarkus.resteasy.reactive.spi.ContainerResponseFilterBuildItem; import io.quarkus.resteasy.reactive.spi.GeneratedJaxRsResourceBuildItem; @@ -238,10 +241,18 @@ JaxRsResourceIndexBuildItem resourceIndex(CombinedIndexBuildItem combinedIndex, @BuildStep void scanResources( JaxRsResourceIndexBuildItem jaxRsResourceIndexBuildItem, + List additionalResourceClassBuildItems, BuildProducer annotationsTransformerBuildItemBuildProducer, BuildProducer resourceScanningResultBuildItemBuildProducer) { - ResourceScanningResult res = ResteasyReactiveScanner.scanResources(jaxRsResourceIndexBuildItem.getIndexView()); + Map additionalResources = new HashMap<>(); + Map additionalResourcePaths = new HashMap<>(); + for (AdditionalResourceClassBuildItem bi : additionalResourceClassBuildItems) { + additionalResources.put(bi.getClassInfo().name(), bi.getClassInfo()); + additionalResourcePaths.put(bi.getClassInfo().name(), bi.getPath()); + } + ResourceScanningResult res = ResteasyReactiveScanner.scanResources(jaxRsResourceIndexBuildItem.getIndexView(), + additionalResources, additionalResourcePaths); if (res == null) { return; } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/spi-deployment/src/main/java/io/quarkus/resteasy/reactive/spi/AdditionalResourceClassBuildItem.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/spi-deployment/src/main/java/io/quarkus/resteasy/reactive/spi/AdditionalResourceClassBuildItem.java new file mode 100644 index 0000000000000..af97035ed5fe9 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/spi-deployment/src/main/java/io/quarkus/resteasy/reactive/spi/AdditionalResourceClassBuildItem.java @@ -0,0 +1,29 @@ +package io.quarkus.resteasy.reactive.spi; + +import org.jboss.jandex.ClassInfo; + +import io.quarkus.builder.item.MultiBuildItem; + +/** + * Can be used by extensions that want to make classes not annotated with JAX-RS {@code @Path} + * part of the ResourceClass scanning process. + * This will likely be used in conjunction with {@code io.quarkus.resteasy.reactive.server.spi.AnnotationsTransformerBuildItem} + */ +public final class AdditionalResourceClassBuildItem extends MultiBuildItem { + + private final ClassInfo classInfo; + private final String path; + + public AdditionalResourceClassBuildItem(ClassInfo classInfo, String path) { + this.classInfo = classInfo; + this.path = path; + } + + public ClassInfo getClassInfo() { + return classInfo; + } + + public String getPath() { + return path; + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java index 9c893b38c71e3..bdf8131b9e967 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java @@ -345,7 +345,8 @@ public void setupEndpoints(Capabilities capabilities, BeanArchiveIndexBuildItem .setGeneratedClassBuildItemBuildProducer(generatedClassBuildItemBuildProducer) .setBytecodeTransformerBuildProducer(bytecodeTransformerBuildItemBuildProducer) .setReflectiveClassProducer(reflectiveClassBuildItemBuildProducer) - .setExistingConverters(existingConverters).setScannedResourcePaths(scannedResourcePaths) + .setExistingConverters(existingConverters) + .setScannedResourcePaths(scannedResourcePaths) .setConfig(createRestReactiveConfig(config)) .setAdditionalReaders(additionalReaders) .setHttpAnnotationToMethod(result.getHttpAnnotationToMethod()) diff --git a/extensions/smallrye-openapi/deployment/pom.xml b/extensions/smallrye-openapi/deployment/pom.xml index ef1b797e26257..913b972a150ac 100644 --- a/extensions/smallrye-openapi/deployment/pom.xml +++ b/extensions/smallrye-openapi/deployment/pom.xml @@ -66,11 +66,6 @@ quarkus-resteasy-deployment test - - io.quarkus - quarkus-spring-web-deployment - test - io.quarkus quarkus-reactive-routes-deployment diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/spring/OpenApiHttpRootDefaultPathTestCase.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/spring/OpenApiHttpRootDefaultPathTestCase.java deleted file mode 100644 index 949559709e7cf..0000000000000 --- a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/spring/OpenApiHttpRootDefaultPathTestCase.java +++ /dev/null @@ -1,41 +0,0 @@ -package io.quarkus.smallrye.openapi.test.spring; - -import org.hamcrest.Matchers; -import org.jboss.shrinkwrap.api.ShrinkWrap; -import org.jboss.shrinkwrap.api.asset.StringAsset; -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; -import io.restassured.RestAssured; - -public class OpenApiHttpRootDefaultPathTestCase { - private static final String OPEN_API_PATH = "/q/openapi"; - - @RegisterExtension - static QuarkusUnitTest runner = new QuarkusUnitTest() - .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) - .addClasses(OpenApiController.class) - .addAsResource(new StringAsset("quarkus.http.root-path=/foo"), "application.properties")); - - @Test - public void testOpenApiPathAccessResource() { - RestAssured.given().header("Accept", "application/yaml") - .when().get(OPEN_API_PATH) - .then().header("Content-Type", "application/yaml;charset=UTF-8"); - RestAssured.given().queryParam("format", "YAML") - .when().get(OPEN_API_PATH) - .then().header("Content-Type", "application/yaml;charset=UTF-8"); - RestAssured.given().header("Accept", "application/json") - .when().get(OPEN_API_PATH) - .then().header("Content-Type", "application/json;charset=UTF-8"); - RestAssured.given().queryParam("format", "JSON") - .when().get(OPEN_API_PATH) - .then() - .header("Content-Type", "application/json;charset=UTF-8") - .body("openapi", Matchers.startsWith("3.0")) - .body("info.title", Matchers.equalTo("Generated API")) - .body("paths", Matchers.hasKey("/foo/resource")); - } -} \ No newline at end of file diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/spring/OpenApiStoreSchemaTestCase.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/spring/OpenApiStoreSchemaTestCase.java deleted file mode 100644 index 9e3c3eb476c37..0000000000000 --- a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/spring/OpenApiStoreSchemaTestCase.java +++ /dev/null @@ -1,36 +0,0 @@ -package io.quarkus.smallrye.openapi.test.spring; - -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; - -import org.jboss.shrinkwrap.api.ShrinkWrap; -import org.jboss.shrinkwrap.api.asset.StringAsset; -import org.jboss.shrinkwrap.api.spec.JavaArchive; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; - -import io.quarkus.test.QuarkusUnitTest; -import io.smallrye.openapi.runtime.io.Format; - -public class OpenApiStoreSchemaTestCase { - - private static String directory = "target/generated/spring/"; - private static final String OPEN_API_DOT = "openapi."; - - @RegisterExtension - static QuarkusUnitTest runner = new QuarkusUnitTest() - .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) - .addClasses(OpenApiController.class) - .addAsResource(new StringAsset("quarkus.smallrye-openapi.store-schema-directory=" + directory), - "application.properties")); - - @Test - public void testOpenApiPathAccessResource() { - Path json = Paths.get(directory, OPEN_API_DOT + Format.JSON.toString().toLowerCase()); - Assertions.assertTrue(Files.exists(json)); - Path yaml = Paths.get(directory, OPEN_API_DOT + Format.YAML.toString().toLowerCase()); - Assertions.assertTrue(Files.exists(yaml)); - } -} diff --git a/extensions/spring-web/deployment/pom.xml b/extensions/spring-web/deployment/pom.xml index 6c3a08357e71c..9b976c5ce45cc 100644 --- a/extensions/spring-web/deployment/pom.xml +++ b/extensions/spring-web/deployment/pom.xml @@ -24,11 +24,7 @@ io.quarkus - quarkus-resteasy-jackson-deployment - - - io.quarkus - quarkus-resteasy-common-spi + quarkus-resteasy-reactive-jackson-deployment 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 8c50809a7612a..a99dd0eb54e97 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,7 +5,6 @@ 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; @@ -36,8 +35,6 @@ 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() @@ -64,15 +61,7 @@ String generate() { } } - // 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; + return generatedClassName; } protected void preGenerateMethodBody(ClassCreator cc) { @@ -93,6 +82,7 @@ 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/ControllerAdviceAbstractExceptionMapperGenerator.java b/extensions/spring-web/deployment/src/main/java/io/quarkus/spring/web/deployment/ControllerAdviceExceptionMapperGenerator.java similarity index 58% rename from extensions/spring-web/deployment/src/main/java/io/quarkus/spring/web/deployment/ControllerAdviceAbstractExceptionMapperGenerator.java rename to extensions/spring-web/deployment/src/main/java/io/quarkus/spring/web/deployment/ControllerAdviceExceptionMapperGenerator.java index 346407d9ca411..e3ecd10eb07a8 100644 --- a/extensions/spring-web/deployment/src/main/java/io/quarkus/spring/web/deployment/ControllerAdviceAbstractExceptionMapperGenerator.java +++ b/extensions/spring-web/deployment/src/main/java/io/quarkus/spring/web/deployment/ControllerAdviceExceptionMapperGenerator.java @@ -1,18 +1,14 @@ 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; @@ -22,26 +18,23 @@ 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 ControllerAdviceAbstractExceptionMapperGenerator extends AbstractExceptionMapperGenerator { +class ControllerAdviceExceptionMapperGenerator 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.APPLICATION_XML, MediaType.TEXT_XML); + MediaType.TEXT_PLAIN, MediaType.APPLICATION_JSON); // Preferred content types order for object type responses private static final List OBJECT_MEDIA_TYPES = Arrays.asList( - MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_XML, MediaType.TEXT_PLAIN); + MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN); private final MethodInfo controllerAdviceMethod; private final TypesUtil typesUtil; @@ -49,11 +42,7 @@ class ControllerAdviceAbstractExceptionMapperGenerator extends AbstractException private final List parameterTypes; private final String declaringClassName; - private final Map parameterTypeToField = new HashMap<>(); - - private FieldDescriptor httpHeadersField; - - ControllerAdviceAbstractExceptionMapperGenerator(MethodInfo controllerAdviceMethod, DotName exceptionDotName, + ControllerAdviceExceptionMapperGenerator(MethodInfo controllerAdviceMethod, DotName exceptionDotName, ClassOutput classOutput, TypesUtil typesUtil) { super(exceptionDotName, classOutput); this.controllerAdviceMethod = controllerAdviceMethod; @@ -64,69 +53,6 @@ class ControllerAdviceAbstractExceptionMapperGenerator extends AbstractException 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)) { @@ -190,7 +116,7 @@ private ResultHandle getResponseContentType(MethodCreator methodCreator, List MAPPING_ANNOTATIONS; + 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); - 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 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 DotName RESPONSE_STATUS = DotName .createSimple("org.springframework.web.bind.annotation.ResponseStatus"); @@ -98,40 +99,16 @@ 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 = new HashSet<>(Arrays.asList( - MODEL_AND_VIEW, VIEW, MODEL, HTTP_ENTITY)); + 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 @BuildStep FeatureBuildItem registerFeature() { return new FeatureBuildItem(Feature.SPRING_WEB); } - @BuildStep - public IgnoredServletContainerInitializerBuildItem ignoreSpringServlet() { - return new IgnoredServletContainerInitializerBuildItem("org.springframework.web.SpringServletContainerInitializer"); - } - - @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( @@ -153,242 +130,320 @@ public void beanDefiningAnnotations(BuildProducer reflectiveClass, - BuildProducer initParamProducer, - BuildProducer deploymentCustomizerProducer) { + public void registerAdditionalResourceClasses(CombinedIndexBuildItem index, + BuildProducer additionalResourceClassProducer) { - validateControllers(beanArchiveIndexBuildItem); + validateControllers(index.getIndex()); - final IndexView index = beanArchiveIndexBuildItem.getIndex(); - final Collection annotations = index.getAnnotations(REST_CONTROLLER_ANNOTATION); - if (annotations.isEmpty()) { - return; + 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 Set classNames = new HashSet<>(); - for (AnnotationInstance annotation : annotations) { - classNames.add(annotation.target().asClass().toString()); - } + @BuildStep + public void methodAnnotationsTransformer(BuildProducer producer) { + producer.produce(new AnnotationsTransformerBuildItem(new AnnotationsTransformer() { - // 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 void accept(ResteasyDeployment resteasyDeployment) { - resteasyDeployment.getScannedResourceClassesWithBuilder().put(SpringResourceBuilder.class.getName(), - new ArrayList<>(classNames)); + public boolean appliesTo(AnnotationTarget.Kind kind) { + return kind == AnnotationTarget.Kind.METHOD; } - })); - - reflectiveClass.produce(new ReflectiveClassBuildItem(true, false, false, SpringResourceBuilder.class.getName())); - } - /** - * 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(); + @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) { + throw new IllegalArgumentException( + "Usage of '@RequestMapping' without an http method is not allowed. Offending method is '" + + methodInfo.declaringClass().name() + "#" + methodInfo.name() + "'"); + } + 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 { - continue; + 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); + } } - if (targetClass.classAnnotation(REST_CONTROLLER_ANNOTATION) == null) { - classesWithoutRestController.add(targetClass.name()); + if (jaxRSMethodAnnotation == null) { + return; } - } - } - 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."); + 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)); + } + } + } + + transform.done(); } - } - } - @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 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; + } - Map nonJaxRsPaths = new HashMap<>(); - for (AnnotationInstance restControllerInstance : restControllerAnnotations) { - String basePath = "/"; - ClassInfo restControllerAnnotatedClass = restControllerInstance.target().asClass(); + 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) })); + } + } - AnnotationInstance requestMappingInstance = restControllerAnnotatedClass.classAnnotation(REQUEST_MAPPING); - if (requestMappingInstance != null) { - String basePathFromAnnotation = getMappingValue(requestMappingInstance); - if (basePathFromAnnotation != null) { - basePath = basePathFromAnnotation; + 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 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; + + private String replaceSpringWebWildcards(String methodPath) { + if (methodPath.contains("/**")) { + methodPath = methodPath.replace("/**", "{unsetPlaceHolderVar:.*}"); + } + if (methodPath.contains("/*")) { + methodPath = methodPath.replace("/*", "/{unusedPlaceHolderVar}"); + } + /* + * 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("/"); } - // record the mapping of method to the http path - methodNameToPath.put(methodName, methodPath); - continue METHOD; + if ((part.startsWith("{") && part.endsWith("}")) || !part.contains("?")) { + sb.append(part); + } else { + sb.append(String.format("{notusedPlaceHolderVar%s:", i)).append(part.replace('?', '.')).append("}"); + } + } + if (methodPath.endsWith("/")) { + sb.append("/"); } + methodPath = sb.toString(); } + return methodPath; } - // 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); - } + })); + } + + // 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]; } + return defaultPathValue; + } - if (!nonJaxRsPaths.isEmpty()) { - recorder.nonJaxRsClassNameToMethodPaths(nonJaxRsPaths); + // 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(); + } + return null; } - /** - * Meant to be called with an instance of any of the MAPPING_CLASSES - */ - private String getMappingValue(AnnotationInstance instance) { + // 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) { 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]; + AnnotationValue pathValue = instance.value(property); + if (pathValue != null) { + return pathValue.asStringArray(); } return null; } - @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; + /** + * 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; } - if (collectProviders(providersToRegister, categorizedReaders, instance, "consumes")) { - useAllAvailable = true; - break OUTER; + if (targetClass.classAnnotation(REST_CONTROLLER_ANNOTATION) == null) { + classesWithoutRestController.add(targetClass.name()); } } } - 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)); + 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."); + } } } - 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 registerStandardExceptionMappers(BuildProducer producer) { + producer.produce(new ExceptionMapperBuildItem(ResponseStatusExceptionMapper.class.getName(), + ResponseStatusException.class.getName(), Priorities.USER, false)); } @BuildStep - public void generateExceptionMapperProviders(BeanArchiveIndexBuildItem beanArchiveIndexBuildItem, - BuildProducer generatedExceptionMappers, - BuildProducer providersProducer, - BuildProducer reflectiveClassProducer) { + public void exceptionHandlingSupport(CombinedIndexBuildItem index, + BuildProducer generatedClassProducer, + BuildProducer exceptionMapperProducer, + BuildProducer reflectiveClassProducer, + BuildProducer unremovableBeanProducer) { 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); - generateMappersForExceptionHandlerInControllerAdvice(providersProducer, reflectiveClassProducer, index, classOutput, + ClassOutput classOutput = new GeneratedClassGizmoAdaptor(generatedClassProducer, true); + generateMappersForResponseStatusOnException(exceptionMapperProducer, index.getIndex(), classOutput, typesUtil); + generateMappersForExceptionHandlerInControllerAdvice(exceptionMapperProducer, reflectiveClassProducer, + unremovableBeanProducer, index.getIndex(), + classOutput, typesUtil); } - @BuildStep - public void registerStandardExceptionMappers(BuildProducer providersProducer) { - providersProducer.produce(new ResteasyJaxrsProviderBuildItem(ResponseStatusExceptionMapper.class.getName())); - } - - private void generateMappersForResponseStatusOnException(BuildProducer providersProducer, + private void generateMappersForResponseStatusOnException(BuildProducer exceptionMapperProducer, IndexView index, ClassOutput classOutput, TypesUtil typesUtil) { Collection responseStatusInstances = index .getAnnotations(RESPONSE_STATUS); @@ -401,18 +456,21 @@ private void generateMappersForResponseStatusOnException(BuildProducer providersProducer, - BuildProducer reflectiveClassProducer, IndexView index, ClassOutput classOutput, + BuildProducer exceptionMapperProducer, + BuildProducer reflectiveClassProducer, + BuildProducer unremovableBeanProducer, IndexView index, ClassOutput classOutput, TypesUtil typesUtil) { AnnotationInstance controllerAdviceInstance = getSingleControllerAdviceInstance(index); @@ -446,12 +504,16 @@ 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 ControllerAdviceAbstractExceptionMapperGenerator(method, handledExceptionType.name(), + String name = new ControllerAdviceExceptionMapperGenerator(method, handledExceptionType.name(), classOutput, typesUtil).generate(); - providersProducer.produce(new ResteasyJaxrsProviderBuildItem(name)); + exceptionMapperProducer.produce( + new ExceptionMapperBuildItem(name, handledExceptionType.name().toString(), Priorities.USER, false)); } - } + + // allow access to HttpHeaders from Arc.container() + unremovableBeanProducer.produce( + UnremovableBeanBuildItem.beanClassNames(ContextProducers.class.getName(), HttpHeaders.class.getName())); } private AnnotationInstance getSingleControllerAdviceInstance(IndexView index) { @@ -468,4 +530,53 @@ 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 2a0e031905721..20c3c8a9cb104 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,7 +10,6 @@ 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; @@ -25,18 +24,33 @@ public class ResponseStatusAndExceptionHandlerTest { .addClasses(ExceptionController.class, RestExceptionHandler.class)); @Test - public void testRootResource() { + public void testRestControllerAdvice() { when().get("/exception").then().statusCode(400); } + @Test + public void testResponseStatusOnException() { + when().get("/exception2").then().statusCode(202); + } + @RestController - @RequestMapping("/exception") public static class ExceptionController { - @GetMapping + public static final StackTraceElement[] EMPTY_STACK_TRACE = new StackTraceElement[0]; + + @GetMapping("/exception") @ResponseStatus(HttpStatus.OK) - public String throwException() { - throw new RuntimeException(); + 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; } } @@ -48,4 +62,9 @@ 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 new file mode 100644 index 0000000000000..8b8df21ad0ba9 --- /dev/null +++ b/extensions/spring-web/deployment/src/test/java/io/quarkus/spring/web/test/basic/BasicMappingTest.java @@ -0,0 +1,217 @@ +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 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 new file mode 100644 index 0000000000000..33f4352dee2f3 --- /dev/null +++ b/extensions/spring-web/deployment/src/test/java/io/quarkus/spring/web/test/basic/Greeting.java @@ -0,0 +1,14 @@ +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 new file mode 100644 index 0000000000000..a533f63e9648a --- /dev/null +++ b/extensions/spring-web/deployment/src/test/java/io/quarkus/spring/web/test/basic/GreetingControllerWithNoRequestMapping.java @@ -0,0 +1,13 @@ +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 new file mode 100644 index 0000000000000..de9c4faafa738 --- /dev/null +++ b/extensions/spring-web/deployment/src/test/java/io/quarkus/spring/web/test/basic/ResponseEntityController.java @@ -0,0 +1,33 @@ +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 new file mode 100644 index 0000000000000..e6c5f96d27ece --- /dev/null +++ b/extensions/spring-web/deployment/src/test/java/io/quarkus/spring/web/test/basic/ResponseStatusController.java @@ -0,0 +1,26 @@ +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 new file mode 100644 index 0000000000000..da33bc5c4445e --- /dev/null +++ b/extensions/spring-web/deployment/src/test/java/io/quarkus/spring/web/test/basic/SomeClass.java @@ -0,0 +1,20 @@ +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 new file mode 100644 index 0000000000000..0ce8ee944996d --- /dev/null +++ b/extensions/spring-web/deployment/src/test/java/io/quarkus/spring/web/test/basic/TestController.java @@ -0,0 +1,105 @@ +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; + } + + @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 68f88fa22c41e..f1d814b3854af 100644 --- a/extensions/spring-web/runtime/pom.xml +++ b/extensions/spring-web/runtime/pom.xml @@ -16,25 +16,7 @@ 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-resteasy-reactive-jackson 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 db25c1729403c..fdcd9398d12d5 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.core.request.ServerDrivenNegotiation; +import org.jboss.resteasy.reactive.server.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 13e6e0f6baf4c..ec35c5bc88a7c 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.core.Headers; -import org.jboss.resteasy.specimpl.BuiltResponse; +import org.jboss.resteasy.reactive.common.util.MultivaluedTreeMap; +import org.jboss.resteasy.reactive.server.jaxrs.ResponseBuilderImpl; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; @@ -17,25 +17,34 @@ */ 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]); + public static final String[] EMPTY_STRINGS_ARRAY = new String[0]; + + public static Response toResponse(ResponseEntity responseEntity) { + ResponseBuilderImpl responseBuilder = toResponseBuilder(responseEntity); + return responseBuilder.build(); } - 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])); + @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()); } - return jaxRsHeaders; + return responseBuilder.build(); } - private static Headers addContentTypeIfMissing(Headers headers, MediaType contentType) { - if (!headers.containsKey(HttpHeaders.CONTENT_TYPE)) { - headers.add(HttpHeaders.CONTENT_TYPE, contentType); + private static ResponseBuilderImpl toResponseBuilder(ResponseEntity responseEntity) { + ResponseBuilderImpl responseBuilder = new ResponseBuilderImpl(); + responseBuilder.status(responseEntity.getStatusCodeValue()).entity(responseEntity.getBody()); + responseBuilder.setAllHeaders(toJaxRsHeaders(responseEntity.getHeaders())); + return responseBuilder; + } + + 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)); } - return headers; + return jaxRsHeaders; } } 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 new file mode 100644 index 0000000000000..f51e3a3380034 --- /dev/null +++ b/extensions/spring-web/runtime/src/main/java/io/quarkus/spring/web/runtime/ResponseEntityHandler.java @@ -0,0 +1,17 @@ +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 2a3f11e997ab5..70dcfe9bf0dad 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,7 +7,6 @@ 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; @@ -15,7 +14,7 @@ public class ResponseStatusExceptionMapper implements ExceptionMapper createEndpoints(ClassInfo currentClassInfo, continue; } seenMethods.add(descriptor); - String methodPath = readStringValue(info.annotation(PATH)); + String methodPath = readStringValue(getAnnotationStore().getAnnotation(info, PATH)); if (methodPath != null) { if (!methodPath.startsWith("/")) { methodPath = "/" + methodPath; @@ -772,12 +772,11 @@ private static String[] extractProducesConsumesValues(AnnotationInstance annotat } - public static String readStringValue(AnnotationInstance annotationInstance) { - String classProduces = null; + private static String readStringValue(AnnotationInstance annotationInstance) { if (annotationInstance != null) { - classProduces = annotationInstance.value().asString(); + return annotationInstance.value().asString(); } - return classProduces; + return null; } protected static String toClassName(Type indexType, ClassInfo currentClass, ClassInfo actualEndpointClass, diff --git a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/scanning/ResteasyReactiveScanner.java b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/scanning/ResteasyReactiveScanner.java index 5464cc8dfbf07..0534765b532fa 100644 --- a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/scanning/ResteasyReactiveScanner.java +++ b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/scanning/ResteasyReactiveScanner.java @@ -37,17 +37,27 @@ public class ResteasyReactiveScanner { public static final Map BUILTIN_HTTP_ANNOTATIONS_TO_METHOD; + public static final Map METHOD_TO_BUILTIN_HTTP_ANNOTATIONS; static { Map map = new HashMap<>(); + Map reverseMap = new HashMap<>(); map.put(GET, "GET"); + reverseMap.put("GET", GET); map.put(POST, "POST"); + reverseMap.put("POST", POST); map.put(HEAD, "HEAD"); + reverseMap.put("HEAD", HEAD); map.put(PUT, "PUT"); + reverseMap.put("PUT", PUT); map.put(DELETE, "DELETE"); + reverseMap.put("DELETE", DELETE); map.put(PATCH, "PATCH"); + reverseMap.put("PATCH", PATCH); map.put(OPTIONS, "OPTIONS"); + reverseMap.put("OPTIONS", OPTIONS); BUILTIN_HTTP_ANNOTATIONS_TO_METHOD = Collections.unmodifiableMap(map); + METHOD_TO_BUILTIN_HTTP_ANNOTATIONS = Collections.unmodifiableMap(reverseMap); } public static ApplicationScanningResult scanForApplicationClass(IndexView index, Set excludedClasses) { @@ -122,13 +132,13 @@ private static boolean appClassHasInject(ClassInfo appClass) { } public static ResourceScanningResult scanResources( - IndexView index) { + IndexView index, Map additionalResources, Map additionalResourcePaths) { Collection paths = index.getAnnotations(ResteasyReactiveDotNames.PATH); Collection allPaths = new ArrayList<>(paths); - Map scannedResources = new HashMap<>(); - Map scannedResourcePaths = new HashMap<>(); + Map scannedResources = new HashMap<>(additionalResources); + Map scannedResourcePaths = new HashMap<>(additionalResourcePaths); Map possibleSubResources = new HashMap<>(); Map pathInterfaces = new HashMap<>(); Map resourcesThatNeedCustomProducer = new HashMap<>(); diff --git a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/framework/ResteasyReactiveUnitTest.java b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/framework/ResteasyReactiveUnitTest.java index e78f958a1bf5b..6a256f762278a 100644 --- a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/framework/ResteasyReactiveUnitTest.java +++ b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/framework/ResteasyReactiveUnitTest.java @@ -214,7 +214,8 @@ public void close() throws Throwable { Index index = JandexUtil.createIndex(deploymentDir); ApplicationScanningResult applicationScanningResult = ResteasyReactiveScanner.scanForApplicationClass(index, Collections.emptySet()); - ResourceScanningResult resources = ResteasyReactiveScanner.scanResources(index); + ResourceScanningResult resources = ResteasyReactiveScanner.scanResources(index, Collections.emptyMap(), + Collections.emptyMap()); if (resources == null) { throw new RuntimeException("no JAX-RS resources found"); } diff --git a/integration-tests/spring-web/pom.xml b/integration-tests/spring-web/pom.xml index b32cf82d1dd84..a74e24dd890c8 100644 --- a/integration-tests/spring-web/pom.xml +++ b/integration-tests/spring-web/pom.xml @@ -32,24 +32,25 @@ io.quarkus - quarkus-undertow + quarkus-hibernate-validator io.quarkus - quarkus-resteasy-jaxb + quarkus-elytron-security-properties-file io.quarkus - quarkus-hibernate-validator + quarkus-smallrye-openapi + io.quarkus - quarkus-elytron-security-properties-file + quarkus-junit5 + test - io.quarkus - quarkus-junit5 + quarkus-junit5-internal test @@ -90,19 +91,6 @@ - - io.quarkus - quarkus-resteasy-jaxb-deployment - ${project.version} - pom - test - - - * - * - - - io.quarkus quarkus-spring-cache-deployment @@ -157,7 +145,7 @@ io.quarkus - quarkus-undertow-deployment + quarkus-smallrye-openapi-deployment ${project.version} pom test @@ -199,6 +187,16 @@ + + prod-mode + test + + test + + + **/*PMT.java + + 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 deleted file mode 100644 index a893ed259da9b..0000000000000 --- a/integration-tests/spring-web/src/main/java/io/quarkus/it/spring/web/Book.java +++ /dev/null @@ -1,24 +0,0 @@ -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 deleted file mode 100644 index 7d2351bcb14b4..0000000000000 --- a/integration-tests/spring-web/src/main/java/io/quarkus/it/spring/web/BookController.java +++ /dev/null @@ -1,14 +0,0 @@ -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 ce892699c5ee8..262a4dffeef70 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,6 +1,7 @@ package io.quarkus.it.spring.web; -import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.core.Request; +import javax.ws.rs.core.UriInfo; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -24,7 +25,7 @@ public void unannotatedException() { @ExceptionHandler(HandledResponseEntityException.class) public ResponseEntity handleResponseEntityException(HandledResponseEntityException e, - HttpServletRequest request) { + UriInfo uriInfo, Request request) { ResponseEntity.BodyBuilder bodyBuilder = ResponseEntity .status(HttpStatus.PAYMENT_REQUIRED) @@ -34,12 +35,12 @@ public ResponseEntity handleResponseEntityException(HandledResponseEntity bodyBuilder.contentType(e.getContentType()); } - return bodyBuilder.body(new Error(request.getRequestURI() + ":" + e.getMessage())); + return bodyBuilder.body(new Error(uriInfo.getPath() + ":" + request.getMethod() + ":" + e.getMessage())); } @ResponseStatus(HttpStatus.EXPECTATION_FAILED) @ExceptionHandler(HandledPojoException.class) - public Error handlePojoExcepton(HandledPojoException e) { + public Error handlePojoException(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 a45058691defb..53347c918a36d 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,8 +1,5 @@ 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 324a20716f25d..09164d08236c1 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,15 +36,6 @@ 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() @@ -53,15 +44,6 @@ 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() @@ -92,28 +74,6 @@ 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() @@ -138,15 +98,6 @@ 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() @@ -155,15 +106,6 @@ 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 973667bf3db86..e191728d0db0b 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,13 +49,4 @@ 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")); - } } diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/spring/OpenApiController.java b/integration-tests/spring-web/src/test/java/io/quarkus/it/spring/web/openapi/OpenApiController.java similarity index 92% rename from extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/spring/OpenApiController.java rename to integration-tests/spring-web/src/test/java/io/quarkus/it/spring/web/openapi/OpenApiController.java index 0227ea958fbbe..9a0d9add87c4d 100644 --- a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/spring/OpenApiController.java +++ b/integration-tests/spring-web/src/test/java/io/quarkus/it/spring/web/openapi/OpenApiController.java @@ -1,4 +1,4 @@ -package io.quarkus.smallrye.openapi.test.spring; +package io.quarkus.it.spring.web.openapi; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -23,4 +23,4 @@ public enum Query { QUERY_PARAM_1, QUERY_PARAM_2; } -} \ No newline at end of file +} diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/spring/OpenApiDefaultPathTestCase.java b/integration-tests/spring-web/src/test/java/io/quarkus/it/spring/web/openapi/OpenApiDefaultPathPMT.java similarity index 78% rename from extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/spring/OpenApiDefaultPathTestCase.java rename to integration-tests/spring-web/src/test/java/io/quarkus/it/spring/web/openapi/OpenApiDefaultPathPMT.java index bbe8fbe95f06f..40c3137555f4b 100644 --- a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/spring/OpenApiDefaultPathTestCase.java +++ b/integration-tests/spring-web/src/test/java/io/quarkus/it/spring/web/openapi/OpenApiDefaultPathPMT.java @@ -1,4 +1,4 @@ -package io.quarkus.smallrye.openapi.test.spring; +package io.quarkus.it.spring.web.openapi; import org.hamcrest.Matchers; import org.jboss.shrinkwrap.api.ShrinkWrap; @@ -6,16 +6,19 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.QuarkusProdModeTest; import io.restassured.RestAssured; -public class OpenApiDefaultPathTestCase { +public class OpenApiDefaultPathPMT { private static final String OPEN_API_PATH = "/q/openapi"; @RegisterExtension - static QuarkusUnitTest runner = new QuarkusUnitTest() + static QuarkusProdModeTest runner = new QuarkusProdModeTest() .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) - .addClasses(OpenApiController.class)); + .addClasses(OpenApiController.class) + .addAsResource("test-roles.properties") + .addAsResource("test-users.properties")) + .setRun(true); @Test public void testOpenApiPathAccessResource() { @@ -36,4 +39,4 @@ public void testOpenApiPathAccessResource() { .body("info.title", Matchers.equalTo("Generated API")) .body("paths", Matchers.hasKey("/resource")); } -} \ No newline at end of file +} diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/spring/OpenApiPathWithSegmentsTestCase.java b/integration-tests/spring-web/src/test/java/io/quarkus/it/spring/web/openapi/OpenApiPathWithSegmentsPMT.java similarity index 74% rename from extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/spring/OpenApiPathWithSegmentsTestCase.java rename to integration-tests/spring-web/src/test/java/io/quarkus/it/spring/web/openapi/OpenApiPathWithSegmentsPMT.java index 0229070b87efd..813e0793e5c15 100644 --- a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/spring/OpenApiPathWithSegmentsTestCase.java +++ b/integration-tests/spring-web/src/test/java/io/quarkus/it/spring/web/openapi/OpenApiPathWithSegmentsPMT.java @@ -1,23 +1,24 @@ -package io.quarkus.smallrye.openapi.test.spring; +package io.quarkus.it.spring.web.openapi; import org.jboss.shrinkwrap.api.ShrinkWrap; -import org.jboss.shrinkwrap.api.asset.StringAsset; 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; +import io.quarkus.test.QuarkusProdModeTest; import io.restassured.RestAssured; -public class OpenApiPathWithSegmentsTestCase { +public class OpenApiPathWithSegmentsPMT { private static final String OPEN_API_PATH = "/path/with/segments"; @RegisterExtension - static QuarkusUnitTest runner = new QuarkusUnitTest() + static QuarkusProdModeTest runner = new QuarkusProdModeTest() .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) .addClasses(OpenApiController.class) - .addAsResource(new StringAsset("quarkus.smallrye-openapi.path=" + OPEN_API_PATH), - "application.properties")); + .addAsResource("test-roles.properties") + .addAsResource("test-users.properties")) + .overrideConfigKey("quarkus.smallrye-openapi.path", OPEN_API_PATH) + .setRun(true); @Test public void testOpenApiPathAccessResource() { @@ -34,4 +35,4 @@ public void testOpenApiPathAccessResource() { .when().get(OPEN_API_PATH) .then().header("Content-Type", "application/json;charset=UTF-8"); } -} \ No newline at end of file +} diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/spring/OpenApiPathWithoutSegmentsTestCase.java b/integration-tests/spring-web/src/test/java/io/quarkus/it/spring/web/openapi/OpenApiPathWithoutSegmentsPMT.java similarity index 75% rename from extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/spring/OpenApiPathWithoutSegmentsTestCase.java rename to integration-tests/spring-web/src/test/java/io/quarkus/it/spring/web/openapi/OpenApiPathWithoutSegmentsPMT.java index d9094818eb9a0..3675ea5a21d6e 100644 --- a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/spring/OpenApiPathWithoutSegmentsTestCase.java +++ b/integration-tests/spring-web/src/test/java/io/quarkus/it/spring/web/openapi/OpenApiPathWithoutSegmentsPMT.java @@ -1,23 +1,24 @@ -package io.quarkus.smallrye.openapi.test.spring; +package io.quarkus.it.spring.web.openapi; import org.jboss.shrinkwrap.api.ShrinkWrap; -import org.jboss.shrinkwrap.api.asset.StringAsset; 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; +import io.quarkus.test.QuarkusProdModeTest; import io.restassured.RestAssured; -public class OpenApiPathWithoutSegmentsTestCase { +public class OpenApiPathWithoutSegmentsPMT { private static final String OPEN_API_PATH = "path-without-segments"; @RegisterExtension - static QuarkusUnitTest runner = new QuarkusUnitTest() + static QuarkusProdModeTest runner = new QuarkusProdModeTest() .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) .addClasses(OpenApiController.class) - .addAsResource(new StringAsset("quarkus.smallrye-openapi.path=" + OPEN_API_PATH), - "application.properties")); + .addAsResource("test-roles.properties") + .addAsResource("test-users.properties")) + .overrideConfigKey("quarkus.smallrye-openapi.path", OPEN_API_PATH) + .setRun(true); @Test public void testOpenApiPathAccessResource() { @@ -34,4 +35,4 @@ public void testOpenApiPathAccessResource() { .when().get("/q/" + OPEN_API_PATH) .then().header("Content-Type", "application/json;charset=UTF-8"); } -} \ No newline at end of file +} diff --git a/integration-tests/spring-web/src/test/java/io/quarkus/it/spring/web/openapi/OpenApiStoreSchemaPMT.java b/integration-tests/spring-web/src/test/java/io/quarkus/it/spring/web/openapi/OpenApiStoreSchemaPMT.java new file mode 100644 index 0000000000000..f4861f8a02a25 --- /dev/null +++ b/integration-tests/spring-web/src/test/java/io/quarkus/it/spring/web/openapi/OpenApiStoreSchemaPMT.java @@ -0,0 +1,43 @@ +package io.quarkus.it.spring.web.openapi; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.ProdBuildResults; +import io.quarkus.test.ProdModeTestResults; +import io.quarkus.test.QuarkusProdModeTest; +import io.smallrye.openapi.runtime.io.Format; + +public class OpenApiStoreSchemaPMT { + + private static final String directory = "target/generated/spring/"; + private static final String OPEN_API_DOT = "openapi."; + + @RegisterExtension + static QuarkusProdModeTest runner = new QuarkusProdModeTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(OpenApiController.class) + .addAsResource("test-roles.properties") + .addAsResource("test-users.properties")) + .overrideConfigKey("quarkus.smallrye-openapi.store-schema-directory", directory) + .setRun(true); + + @ProdBuildResults + private ProdModeTestResults prodModeTestResults; + + @Test + public void testOpenApiPathAccessResource() { + Path outputDir = prodModeTestResults.getBuildDir().getParent(); + Path json = outputDir.resolve(Paths.get(directory, OPEN_API_DOT + Format.JSON.toString().toLowerCase())); + Assertions.assertTrue(Files.exists(json)); + Path yaml = outputDir.resolve(Paths.get(directory, OPEN_API_DOT + Format.YAML.toString().toLowerCase())); + Assertions.assertTrue(Files.exists(yaml)); + } +} diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/spring/OpenApiWithConfigTestCase.java b/integration-tests/spring-web/src/test/java/io/quarkus/it/spring/web/openapi/OpenApiWithConfigPMT.java similarity index 72% rename from extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/spring/OpenApiWithConfigTestCase.java rename to integration-tests/spring-web/src/test/java/io/quarkus/it/spring/web/openapi/OpenApiWithConfigPMT.java index 6c3657b0e45e5..929d54fb73e9c 100644 --- a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/spring/OpenApiWithConfigTestCase.java +++ b/integration-tests/spring-web/src/test/java/io/quarkus/it/spring/web/openapi/OpenApiWithConfigPMT.java @@ -1,25 +1,27 @@ -package io.quarkus.smallrye.openapi.test.spring; +package io.quarkus.it.spring.web.openapi; import org.hamcrest.Matchers; import org.jboss.shrinkwrap.api.ShrinkWrap; -import org.jboss.shrinkwrap.api.asset.StringAsset; 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; +import io.quarkus.test.QuarkusProdModeTest; import io.restassured.RestAssured; -public class OpenApiWithConfigTestCase { +public class OpenApiWithConfigPMT { private static final String OPEN_API_PATH = "/q/openapi"; @RegisterExtension - static QuarkusUnitTest runner = new QuarkusUnitTest() + static QuarkusProdModeTest runner = new QuarkusProdModeTest() .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) .addClasses(OpenApiController.class) - .addAsManifestResource("test-openapi.yaml", "openapi.yaml") - .addAsResource(new StringAsset("mp.openapi.scan.disable=true\nmp.openapi.servers=https://api.acme.org/"), - "application.properties")); + .addAsResource("test-roles.properties") + .addAsResource("test-users.properties") + .addAsManifestResource("test-openapi.yaml", "openapi.yaml")) + .overrideConfigKey("mp.openapi.scan.disable", "true") + .overrideConfigKey("mp.openapi.servers", "https://api.acme.org/") + .setRun(true); @Test public void testOpenAPI() { @@ -35,4 +37,4 @@ public void testOpenAPI() { .body("paths", Matchers.hasKey("/openapi")) .body("paths", Matchers.not(Matchers.hasKey("/resource"))); } -} \ No newline at end of file +} diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/spring/SwaggerAndOpenAPIWithCommonPrefixTest.java b/integration-tests/spring-web/src/test/java/io/quarkus/it/spring/web/openapi/SwaggerAndOpenAPIWithCommonPrefixPMT.java similarity index 62% rename from extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/spring/SwaggerAndOpenAPIWithCommonPrefixTest.java rename to integration-tests/spring-web/src/test/java/io/quarkus/it/spring/web/openapi/SwaggerAndOpenAPIWithCommonPrefixPMT.java index 99973f8e17855..56463b45a47bd 100644 --- a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/spring/SwaggerAndOpenAPIWithCommonPrefixTest.java +++ b/integration-tests/spring-web/src/test/java/io/quarkus/it/spring/web/openapi/SwaggerAndOpenAPIWithCommonPrefixPMT.java @@ -1,26 +1,29 @@ -package io.quarkus.smallrye.openapi.test.spring; +package io.quarkus.it.spring.web.openapi; import static org.hamcrest.Matchers.containsString; import org.jboss.shrinkwrap.api.ShrinkWrap; -import org.jboss.shrinkwrap.api.asset.StringAsset; 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; +import io.quarkus.test.QuarkusProdModeTest; import io.restassured.RestAssured; /** * This test is a reproducer for https://github.com/quarkusio/quarkus/issues/4613. */ -public class SwaggerAndOpenAPIWithCommonPrefixTest { +public class SwaggerAndOpenAPIWithCommonPrefixPMT { @RegisterExtension - static final QuarkusUnitTest config = new QuarkusUnitTest() + static final QuarkusProdModeTest config = new QuarkusProdModeTest() .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) .addClass(OpenApiController.class) - .addAsResource(new StringAsset("quarkus.smallrye-openapi.path=swagger"), "application.properties")); + .addAsResource("test-roles.properties") + .addAsResource("test-users.properties")) + .overrideConfigKey("quarkus.smallrye-openapi.path", "swagger") + .overrideConfigKey("quarkus.swagger-ui.always-include", "true") + .setRun(true); @Test public void shouldWorkEvenWithCommonPrefix() { @@ -28,4 +31,4 @@ public void shouldWorkEvenWithCommonPrefix() { RestAssured.when().get("/q/swagger").then().statusCode(200) .body(containsString("/resource"), containsString("QUERY_PARAM_1")); } -} \ No newline at end of file +} diff --git a/integration-tests/spring-web/src/test/resources/test-openapi.yaml b/integration-tests/spring-web/src/test/resources/test-openapi.yaml new file mode 100644 index 0000000000000..385b940a3c594 --- /dev/null +++ b/integration-tests/spring-web/src/test/resources/test-openapi.yaml @@ -0,0 +1,12 @@ +openapi: 3.0.0 +info: + title: Test OpenAPI + description: Some description + version: 4.2 +paths: + /openapi: + get: + operationId: someOperation + responses: + 200: + description: Ok \ No newline at end of file