diff --git a/bom/application/pom.xml b/bom/application/pom.xml
index ce7594cee018f..c6d7b2ff64e92 100644
--- a/bom/application/pom.xml
+++ b/bom/application/pom.xml
@@ -1813,6 +1813,16 @@
quarkus-spring-web-resteasy-classic-deployment
${project.version}
+
+ io.quarkus
+ quarkus-spring-web-resteasy-reactive
+ ${project.version}
+
+
+ io.quarkus
+ quarkus-spring-web-resteasy-reactive-deployment
+ ${project.version}
+
io.quarkus
quarkus-spring-data-jpa
diff --git a/devtools/bom-descriptor-json/pom.xml b/devtools/bom-descriptor-json/pom.xml
index 7b48d90c97e05..539b46999d48e 100644
--- a/devtools/bom-descriptor-json/pom.xml
+++ b/devtools/bom-descriptor-json/pom.xml
@@ -2684,6 +2684,19 @@
+
+ io.quarkus
+ quarkus-spring-web-resteasy-reactive
+ ${project.version}
+ pom
+ test
+
+
+ *
+ *
+
+
+
io.quarkus
quarkus-swagger-ui
diff --git a/docs/pom.xml b/docs/pom.xml
index f591ade57a3f8..696f1b21877e9 100644
--- a/docs/pom.xml
+++ b/docs/pom.xml
@@ -2645,6 +2645,19 @@
+
+ io.quarkus
+ quarkus-spring-web-resteasy-reactive-deployment
+ ${project.version}
+ pom
+ test
+
+
+ *
+ *
+
+
+
io.quarkus
quarkus-swagger-ui-deployment
diff --git a/extensions/spring-web/core/runtime/src/main/java/io/quarkus/spring/web/runtime/ResponseEntityConverter.java b/extensions/spring-web/core/common-runtime/src/main/java/io.quarkus.spring.web.runtime.common/ResponseEntityConverter.java
similarity index 90%
rename from extensions/spring-web/core/runtime/src/main/java/io/quarkus/spring/web/runtime/ResponseEntityConverter.java
rename to extensions/spring-web/core/common-runtime/src/main/java/io.quarkus.spring.web.runtime.common/ResponseEntityConverter.java
index 5095128303196..4980298b480f1 100644
--- a/extensions/spring-web/core/runtime/src/main/java/io/quarkus/spring/web/runtime/ResponseEntityConverter.java
+++ b/extensions/spring-web/core/common-runtime/src/main/java/io.quarkus.spring.web.runtime.common/ResponseEntityConverter.java
@@ -1,4 +1,4 @@
-package io.quarkus.spring.web.runtime;
+package io.quarkus.spring.web.runtime.common;
import java.util.Collections;
import java.util.HashMap;
@@ -21,7 +21,7 @@ public static Response toResponse(ResponseEntity responseEntity, MediaType defau
Response.ResponseBuilder responseBuilder = Response.status(responseEntity.getStatusCodeValue())
.entity(responseEntity.getBody());
var jaxRsHeaders = toJaxRsHeaders(responseEntity.getHeaders());
- if (!jaxRsHeaders.containsKey(HttpHeaders.CONTENT_TYPE)) {
+ if (!jaxRsHeaders.containsKey(HttpHeaders.CONTENT_TYPE) && (defaultMediaType != null)) {
jaxRsHeaders.put(HttpHeaders.CONTENT_TYPE, Collections.singletonList(defaultMediaType.toString()));
}
for (var entry : jaxRsHeaders.entrySet()) {
diff --git a/extensions/spring-web/core/deployment/pom.xml b/extensions/spring-web/core/deployment/pom.xml
index 774c9a39dba77..b1f3e0bded41f 100644
--- a/extensions/spring-web/core/deployment/pom.xml
+++ b/extensions/spring-web/core/deployment/pom.xml
@@ -27,10 +27,19 @@
quarkus-spring-web-resteasy-classic-deployment
true
+
+ io.quarkus
+ quarkus-spring-web-resteasy-reactive-deployment
+ true
+
io.quarkus
quarkus-jaxrs-spi-deployment
+
+ io.quarkus
+ quarkus-resteasy-reactive-spi-deployment
+
io.quarkus
quarkus-resteasy-common-spi
diff --git a/extensions/spring-web/core/deployment/src/main/java/io/quarkus/spring/web/deployment/AbstractExceptionMapperGenerator.java b/extensions/spring-web/core/deployment/src/main/java/io/quarkus/spring/web/deployment/AbstractExceptionMapperGenerator.java
index 8c50809a7612a..3333e7501c34f 100644
--- a/extensions/spring-web/core/deployment/src/main/java/io/quarkus/spring/web/deployment/AbstractExceptionMapperGenerator.java
+++ b/extensions/spring-web/core/deployment/src/main/java/io/quarkus/spring/web/deployment/AbstractExceptionMapperGenerator.java
@@ -26,9 +26,12 @@ abstract class AbstractExceptionMapperGenerator {
protected final DotName exceptionDotName;
protected final ClassOutput classOutput;
- AbstractExceptionMapperGenerator(DotName exceptionDotName, ClassOutput classOutput) {
+ private final boolean isResteasyClassic;
+
+ AbstractExceptionMapperGenerator(DotName exceptionDotName, ClassOutput classOutput, boolean isResteasyClassic) {
this.exceptionDotName = exceptionDotName;
this.classOutput = classOutput;
+ this.isResteasyClassic = isResteasyClassic;
}
abstract void generateMethodBody(MethodCreator toResponse);
@@ -36,8 +39,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 +65,20 @@ 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);
- }
+ if (isResteasyClassic) {
+ String generatedSubtypeClassName = "io.quarkus.spring.web.mappers.Subtype" + exceptionDotName.withoutPackagePrefix()
+ + "Mapper_" + HashUtil.sha1(exceptionDotName.toString());
+ // 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 generatedSubtypeClassName;
+ }
+ return generatedClassName;
}
protected void preGenerateMethodBody(ClassCreator cc) {
@@ -93,6 +99,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/core/deployment/src/main/java/io/quarkus/spring/web/deployment/ControllerAdviceAbstractExceptionMapperGenerator.java b/extensions/spring-web/core/deployment/src/main/java/io/quarkus/spring/web/deployment/ControllerAdviceExceptionMapperGenerator.java
similarity index 73%
rename from extensions/spring-web/core/deployment/src/main/java/io/quarkus/spring/web/deployment/ControllerAdviceAbstractExceptionMapperGenerator.java
rename to extensions/spring-web/core/deployment/src/main/java/io/quarkus/spring/web/deployment/ControllerAdviceExceptionMapperGenerator.java
index f3c0a5ce9a113..57c525e7f8ebb 100644
--- a/extensions/spring-web/core/deployment/src/main/java/io/quarkus/spring/web/deployment/ControllerAdviceAbstractExceptionMapperGenerator.java
+++ b/extensions/spring-web/core/deployment/src/main/java/io/quarkus/spring/web/deployment/ControllerAdviceExceptionMapperGenerator.java
@@ -12,7 +12,9 @@
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;
@@ -29,9 +31,9 @@
import io.quarkus.gizmo.MethodCreator;
import io.quarkus.gizmo.MethodDescriptor;
import io.quarkus.gizmo.ResultHandle;
-import io.quarkus.spring.web.runtime.ResponseEntityConverter;
+import io.quarkus.spring.web.runtime.common.ResponseEntityConverter;
-class ControllerAdviceAbstractExceptionMapperGenerator extends AbstractExceptionMapperGenerator {
+class ControllerAdviceExceptionMapperGenerator extends AbstractExceptionMapperGenerator {
private static final DotName RESPONSE_ENTITY = DotName.createSimple("org.springframework.http.ResponseEntity");
@@ -54,14 +56,9 @@ class ControllerAdviceAbstractExceptionMapperGenerator extends AbstractException
private final boolean isResteasyClassic;
- ControllerAdviceAbstractExceptionMapperGenerator(MethodInfo controllerAdviceMethod, DotName exceptionDotName,
+ ControllerAdviceExceptionMapperGenerator(MethodInfo controllerAdviceMethod, DotName exceptionDotName,
ClassOutput classOutput, TypesUtil typesUtil, boolean isResteasyClassic) {
- super(exceptionDotName, classOutput);
-
- // TODO: remove this restriction
- if (!isResteasyClassic) {
- throw new IllegalStateException("Currently Spring Web can only work with RESTEasy Classic");
- }
+ super(exceptionDotName, classOutput, isResteasyClassic);
this.controllerAdviceMethod = controllerAdviceMethod;
this.typesUtil = typesUtil;
@@ -80,6 +77,10 @@ class ControllerAdviceAbstractExceptionMapperGenerator extends AbstractException
*/
@Override
protected void preGenerateMethodBody(ClassCreator cc) {
+ if (!isResteasyClassic) {
+ return;
+ }
+
int notAllowedParameterIndex = -1;
for (int i = 0; i < parameterTypes.size(); i++) {
Type parameterType = parameterTypes.get(i);
@@ -197,14 +198,23 @@ private ResultHandle getResponseContentType(MethodCreator methodCreator, List generatedExceptionMappers,
BuildProducer providersProducer,
+ BuildProducer exceptionMapperProducer,
BuildProducer reflectiveClassProducer,
+ BuildProducer unremovableBeanProducer,
Capabilities capabilities) {
boolean isResteasyClassicAvailable = capabilities.isPresent(Capability.RESTEASY_JSON_JACKSON);
@@ -120,15 +127,18 @@ public void generateExceptionMapperProviders(BeanArchiveIndexBuildItem beanArchi
// Look for all exception classes that are annotated with @ResponseStatus
- IndexView index = beanArchiveIndexBuildItem.getIndex();
+ IndexView indexView = index.getIndex();
ClassOutput classOutput = new GeneratedClassGizmoAdaptor(generatedExceptionMappers, true);
- generateMappersForResponseStatusOnException(providersProducer, index, classOutput, typesUtil,
+ generateMappersForResponseStatusOnException(providersProducer, exceptionMapperProducer, indexView, classOutput,
+ typesUtil,
isResteasyClassicAvailable);
- generateMappersForExceptionHandlerInControllerAdvice(providersProducer, reflectiveClassProducer, index, classOutput,
+ generateMappersForExceptionHandlerInControllerAdvice(providersProducer, exceptionMapperProducer,
+ reflectiveClassProducer, unremovableBeanProducer, indexView, classOutput,
typesUtil, isResteasyClassicAvailable);
}
private void generateMappersForResponseStatusOnException(BuildProducer providersProducer,
+ BuildProducer exceptionMapperProducer,
IndexView index, ClassOutput classOutput, TypesUtil typesUtil, boolean isResteasyClassic) {
Collection responseStatusInstances = index
.getAnnotations(RESPONSE_STATUS);
@@ -141,19 +151,24 @@ private void generateMappersForResponseStatusOnException(BuildProducer providersProducer,
- BuildProducer reflectiveClassProducer, IndexView index, ClassOutput classOutput,
+ BuildProducer exceptionMapperProducer,
+ BuildProducer reflectiveClassProducer,
+ BuildProducer unremovableBeanProducer, IndexView index, ClassOutput classOutput,
TypesUtil typesUtil, boolean isResteasyClassic) {
AnnotationInstance controllerAdviceInstance = getSingleControllerAdviceInstance(index);
@@ -187,12 +202,21 @@ 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, isResteasyClassic).generate();
providersProducer.produce(new ResteasyJaxrsProviderBuildItem(name));
+ exceptionMapperProducer.produce(
+ new ExceptionMapperBuildItem(name, handledExceptionType.name().toString(), Priorities.USER, false));
}
}
+
+ // allow access to HttpHeaders from Arc.container()
+ if (!isResteasyClassic) {
+ unremovableBeanProducer.produce(
+ UnremovableBeanBuildItem.beanClassNames("org.jboss.resteasy.reactive.server.injection.ContextProducers",
+ HttpHeaders.class.getName()));
+ }
}
private AnnotationInstance getSingleControllerAdviceInstance(IndexView index) {
diff --git a/extensions/spring-web/core/runtime/pom.xml b/extensions/spring-web/core/runtime/pom.xml
index 6ea9dd5873d21..5a91754456c1e 100644
--- a/extensions/spring-web/core/runtime/pom.xml
+++ b/extensions/spring-web/core/runtime/pom.xml
@@ -19,6 +19,11 @@
quarkus-spring-web-resteasy-classic
true
+
+ io.quarkus
+ quarkus-spring-web-resteasy-reactive
+ true
+
io.quarkus
quarkus-spring-di
diff --git a/extensions/spring-web/pom.xml b/extensions/spring-web/pom.xml
index 4d142216408aa..d9a8941f3e503 100644
--- a/extensions/spring-web/pom.xml
+++ b/extensions/spring-web/pom.xml
@@ -16,5 +16,6 @@
core
resteasy-classic
+ resteasy-reactive
diff --git a/extensions/spring-web/resteasy-reactive/deployment/pom.xml b/extensions/spring-web/resteasy-reactive/deployment/pom.xml
new file mode 100644
index 0000000000000..a523fccb6e58a
--- /dev/null
+++ b/extensions/spring-web/resteasy-reactive/deployment/pom.xml
@@ -0,0 +1,59 @@
+
+
+
+ quarkus-spring-web-resteasy-reactive-parent
+ io.quarkus
+ 999-SNAPSHOT
+
+
+ 4.0.0
+
+ quarkus-spring-web-resteasy-reactive-deployment
+ Quarkus - Spring Web - RESTEasy Reactive - Deployment
+
+
+
+ io.quarkus
+ quarkus-resteasy-reactive-deployment
+
+
+ io.quarkus
+ quarkus-resteasy-server-common-spi
+
+
+
+ io.quarkus
+ quarkus-spring-web-resteasy-reactive
+
+
+
+ io.quarkus
+ quarkus-junit5-internal
+ test
+
+
+ io.rest-assured
+ rest-assured
+ test
+
+
+
+
+
+
+ maven-compiler-plugin
+
+
+
+ io.quarkus
+ quarkus-extension-processor
+ ${project.version}
+
+
+
+
+
+
+
diff --git a/extensions/spring-web/resteasy-reactive/deployment/src/main/java/io/quarkus/spring/web/deployment/SpringWebResteasyReactiveProcessor.java b/extensions/spring-web/resteasy-reactive/deployment/src/main/java/io/quarkus/spring/web/deployment/SpringWebResteasyReactiveProcessor.java
new file mode 100644
index 0000000000000..4a9b50fbedc87
--- /dev/null
+++ b/extensions/spring-web/resteasy-reactive/deployment/src/main/java/io/quarkus/spring/web/deployment/SpringWebResteasyReactiveProcessor.java
@@ -0,0 +1,453 @@
+package io.quarkus.spring.web.deployment;
+
+import static org.jboss.jandex.AnnotationInstance.create;
+import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.DEFAULT_VALUE;
+import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.REST_COOKIE_PARAM;
+import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.REST_MATRIX_PARAM;
+import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.REST_PATH_PARAM;
+import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.REST_QUERY_PARAM;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.ws.rs.Priorities;
+import javax.ws.rs.core.Response;
+
+import org.jboss.jandex.AnnotationInstance;
+import org.jboss.jandex.AnnotationTarget;
+import org.jboss.jandex.AnnotationValue;
+import org.jboss.jandex.ClassInfo;
+import org.jboss.jandex.DotName;
+import org.jboss.jandex.IndexView;
+import org.jboss.jandex.MethodInfo;
+import org.jboss.jandex.Type;
+import org.jboss.logging.Logger;
+import org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames;
+import org.jboss.resteasy.reactive.common.processor.scanning.ResteasyReactiveScanner;
+import org.jboss.resteasy.reactive.common.processor.transformation.AnnotationsTransformer;
+import org.jboss.resteasy.reactive.common.processor.transformation.Transformation;
+import org.jboss.resteasy.reactive.server.model.FixedHandlerChainCustomizer;
+import org.jboss.resteasy.reactive.server.model.HandlerChainCustomizer;
+import org.jboss.resteasy.reactive.server.processor.scanning.MethodScanner;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.server.ResponseStatusException;
+
+import io.quarkus.deployment.annotations.BuildProducer;
+import io.quarkus.deployment.annotations.BuildStep;
+import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
+import io.quarkus.resteasy.reactive.server.spi.AnnotationsTransformerBuildItem;
+import io.quarkus.resteasy.reactive.server.spi.MethodScannerBuildItem;
+import io.quarkus.resteasy.reactive.spi.AdditionalResourceClassBuildItem;
+import io.quarkus.resteasy.reactive.spi.ExceptionMapperBuildItem;
+import io.quarkus.resteasy.server.common.spi.AdditionalJaxRsResourceMethodParamAnnotations;
+import io.quarkus.spring.web.runtime.ResponseEntityHandler;
+import io.quarkus.spring.web.runtime.ResponseStatusHandler;
+import io.quarkus.spring.web.runtime.common.ResponseStatusExceptionMapper;
+
+public class SpringWebResteasyReactiveProcessor {
+
+ private static final Logger LOGGER = Logger.getLogger(SpringWebResteasyReactiveProcessor.class.getName());
+
+ private static final DotName REST_CONTROLLER_ANNOTATION = DotName
+ .createSimple("org.springframework.web.bind.annotation.RestController");
+
+ private static final DotName REQUEST_MAPPING = DotName
+ .createSimple("org.springframework.web.bind.annotation.RequestMapping");
+ private static final DotName GET_MAPPING = DotName.createSimple("org.springframework.web.bind.annotation.GetMapping");
+ private static final DotName POST_MAPPING = DotName.createSimple("org.springframework.web.bind.annotation.PostMapping");
+ private static final DotName PUT_MAPPING = DotName.createSimple("org.springframework.web.bind.annotation.PutMapping");
+ private static final DotName DELETE_MAPPING = DotName.createSimple("org.springframework.web.bind.annotation.DeleteMapping");
+ private static final DotName PATCH_MAPPING = DotName.createSimple("org.springframework.web.bind.annotation.PatchMapping");
+ private static final List MAPPING_ANNOTATIONS = List.of(REQUEST_MAPPING, GET_MAPPING, POST_MAPPING,
+ PUT_MAPPING, DELETE_MAPPING, PATCH_MAPPING);
+
+ private static final DotName 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");
+ private static final DotName EXCEPTION_HANDLER = DotName
+ .createSimple("org.springframework.web.bind.annotation.ExceptionHandler");
+
+ private static final DotName REST_CONTROLLER_ADVICE = DotName
+ .createSimple("org.springframework.web.bind.annotation.RestControllerAdvice");
+
+ private static final DotName MODEL_AND_VIEW = DotName.createSimple("org.springframework.web.servlet.ModelAndView");
+ private static final DotName VIEW = DotName.createSimple("org.springframework.web.servlet.View");
+ private static final DotName MODEL = DotName.createSimple("org.springframework.ui.Model");
+
+ private static final DotName HTTP_ENTITY = DotName.createSimple("org.springframework.http.HttpEntity");
+ private static final DotName RESPONSE_ENTITY = DotName.createSimple("org.springframework.http.ResponseEntity");
+
+ private static final Set DISALLOWED_EXCEPTION_CONTROLLER_RETURN_TYPES = 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
+ 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 registerStandardExceptionMappers(BuildProducer producer) {
+ producer.produce(new ExceptionMapperBuildItem(ResponseStatusExceptionMapper.class.getName(),
+ ResponseStatusException.class.getName(), Priorities.USER, false));
+ }
+
+ @BuildStep
+ public void registerAdditionalResourceClasses(CombinedIndexBuildItem index,
+ BuildProducer additionalResourceClassProducer) {
+
+ validateControllers(index.getIndex());
+
+ for (AnnotationInstance restController : index.getIndex()
+ .getAnnotations(REST_CONTROLLER_ANNOTATION)) {
+ ClassInfo targetClass = restController.target().asClass();
+ additionalResourceClassProducer.produce(new AdditionalResourceClassBuildItem(targetClass,
+ getSinglePathOfInstance(targetClass.classAnnotation(REQUEST_MAPPING), "")));
+ }
+ }
+
+ /**
+ * 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 (targetClass.classAnnotation(REST_CONTROLLER_ANNOTATION) == null) {
+ classesWithoutRestController.add(targetClass.name());
+ }
+ }
+ }
+
+ 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.");
+ }
+ }
+ }
+
+ // 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;
+ }
+
+ // 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 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;
+ }
+ AnnotationValue pathValue = instance.value(property);
+ if (pathValue != null) {
+ return pathValue.asStringArray();
+ }
+ return null;
+ }
+
+ @BuildStep
+ public void methodAnnotationsTransformer(BuildProducer producer) {
+
+ producer.produce(new AnnotationsTransformerBuildItem(new AnnotationsTransformer() {
+
+ @Override
+ public boolean appliesTo(AnnotationTarget.Kind kind) {
+ return kind == AnnotationTarget.Kind.METHOD;
+ }
+
+ @Override
+ public void transform(TransformationContext transformationContext) {
+ AnnotationTarget target = transformationContext.getTarget();
+ if (target.kind() != AnnotationTarget.Kind.METHOD) {
+ return;
+ }
+ MethodInfo methodInfo = target.asMethod();
+ Transformation transform = transformationContext.transform();
+ DotName jaxRSMethodAnnotation = null;
+ String path = null;
+ String[] produces = null;
+ String[] consumes = null;
+
+ AnnotationInstance mappingAnnotationInstance = methodInfo.annotation(REQUEST_MAPPING);
+ if (mappingAnnotationInstance != null) {
+ AnnotationValue methodValue = mappingAnnotationInstance.value("method");
+ if (methodValue == null) {
+ jaxRSMethodAnnotation = ResteasyReactiveDotNames.GET;
+ } else {
+ String[] methods = methodValue.asEnumArray();
+ if (methods.length > 1) {
+ throw new IllegalArgumentException(
+ "Usage of multiple methods using '@RequestMapping' is not allowed. Offending method is '"
+ + methodInfo.declaringClass().name() + "#" + methodInfo.name() + "'");
+ }
+ DotName methodDotName = ResteasyReactiveScanner.METHOD_TO_BUILTIN_HTTP_ANNOTATIONS.get(methods[0]);
+ if (methodDotName == null) {
+ throw new IllegalArgumentException(
+ "Unsupported HTTP method '" + methods[0] + "' for @RequestMapping. Offending method is '"
+ + methodInfo.declaringClass().name() + "#" + methodInfo.name() + "'");
+ }
+ jaxRSMethodAnnotation = methodDotName;
+ }
+ } else {
+ if (methodInfo.hasAnnotation(GET_MAPPING)) {
+ jaxRSMethodAnnotation = ResteasyReactiveDotNames.GET;
+ mappingAnnotationInstance = methodInfo.annotation(GET_MAPPING);
+ } else if (methodInfo.hasAnnotation(POST_MAPPING)) {
+ jaxRSMethodAnnotation = ResteasyReactiveDotNames.POST;
+ mappingAnnotationInstance = methodInfo.annotation(POST_MAPPING);
+ } else if (methodInfo.hasAnnotation(PUT_MAPPING)) {
+ jaxRSMethodAnnotation = ResteasyReactiveDotNames.PUT;
+ mappingAnnotationInstance = methodInfo.annotation(PUT_MAPPING);
+ } else if (methodInfo.hasAnnotation(DELETE_MAPPING)) {
+ jaxRSMethodAnnotation = ResteasyReactiveDotNames.DELETE;
+ mappingAnnotationInstance = methodInfo.annotation(DELETE_MAPPING);
+ } else if (methodInfo.hasAnnotation(PATCH_MAPPING)) {
+ jaxRSMethodAnnotation = ResteasyReactiveDotNames.PATCH;
+ mappingAnnotationInstance = methodInfo.annotation(PATCH_MAPPING);
+ }
+ }
+
+ if (jaxRSMethodAnnotation == null) {
+ return;
+ }
+
+ 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();
+ }
+
+ 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;
+ }
+
+ 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) }));
+ }
+ }
+
+ 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)) }));
+ }
+
+ 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("/");
+ }
+ 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;
+ }
+
+ }));
+ }
+
+ @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();
+ }
+ });
+ }
+
+ //TODO: replace with RESTEasy Reactive @ResponseStatus support using an annotation transformer
+ @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/resteasy-reactive/pom.xml b/extensions/spring-web/resteasy-reactive/pom.xml
new file mode 100644
index 0000000000000..9802049d4d966
--- /dev/null
+++ b/extensions/spring-web/resteasy-reactive/pom.xml
@@ -0,0 +1,23 @@
+
+
+
+ quarkus-spring-web-parent-aggregator
+ io.quarkus
+ 999-SNAPSHOT
+ ../pom.xml
+
+ 4.0.0
+
+ quarkus-spring-web-resteasy-reactive-parent
+ Quarkus - Spring Web - RESTEasy Reactive - Parent
+ pom
+
+ deployment
+ runtime
+ tests
+
+
+
+
diff --git a/extensions/spring-web/resteasy-reactive/runtime/pom.xml b/extensions/spring-web/resteasy-reactive/runtime/pom.xml
new file mode 100644
index 0000000000000..1b6563b637e5f
--- /dev/null
+++ b/extensions/spring-web/resteasy-reactive/runtime/pom.xml
@@ -0,0 +1,48 @@
+
+
+
+ quarkus-spring-web-resteasy-reactive-parent
+ io.quarkus
+ 999-SNAPSHOT
+
+
+ 4.0.0
+
+ quarkus-spring-web-resteasy-reactive
+ Quarkus - Spring Web - RESTEasy Reactive - Runtime
+
+
+
+ io.quarkus
+ quarkus-resteasy-reactive
+
+
+ io.quarkus
+ quarkus-spring-web-common
+
+
+
+
+
+
+ io.quarkus
+ quarkus-bootstrap-maven-plugin
+
+
+ process-resources
+
+ extension-descriptor
+
+
+
+ io.quarkus:quarkus-resteasy-reactive-jackson
+
+
+
+
+
+
+
+
diff --git a/extensions/spring-web/resteasy-reactive/runtime/src/main/java/io/quarkus/spring/web/runtime/ResponseEntityHandler.java b/extensions/spring-web/resteasy-reactive/runtime/src/main/java/io/quarkus/spring/web/runtime/ResponseEntityHandler.java
new file mode 100644
index 0000000000000..a365e4f16079d
--- /dev/null
+++ b/extensions/spring-web/resteasy-reactive/runtime/src/main/java/io/quarkus/spring/web/runtime/ResponseEntityHandler.java
@@ -0,0 +1,19 @@
+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;
+
+import io.quarkus.spring.web.runtime.common.ResponseEntityConverter;
+
+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, null));
+ }
+ }
+}
diff --git a/extensions/spring-web/resteasy-reactive/runtime/src/main/java/io/quarkus/spring/web/runtime/ResponseStatusHandler.java b/extensions/spring-web/resteasy-reactive/runtime/src/main/java/io/quarkus/spring/web/runtime/ResponseStatusHandler.java
new file mode 100644
index 0000000000000..23a9f3736b46a
--- /dev/null
+++ b/extensions/spring-web/resteasy-reactive/runtime/src/main/java/io/quarkus/spring/web/runtime/ResponseStatusHandler.java
@@ -0,0 +1,36 @@
+package io.quarkus.spring.web.runtime;
+
+import org.jboss.resteasy.reactive.common.jaxrs.ResponseImpl;
+import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext;
+import org.jboss.resteasy.reactive.server.spi.ServerRestHandler;
+
+public class ResponseStatusHandler implements ServerRestHandler {
+
+ // make mutable to allow for bytecode serialization
+ private int defaultResponseCode;
+ private int newResponseCode;
+
+ public int getDefaultResponseCode() {
+ return defaultResponseCode;
+ }
+
+ public void setDefaultResponseCode(int defaultResponseCode) {
+ this.defaultResponseCode = defaultResponseCode;
+ }
+
+ public int getNewResponseCode() {
+ return newResponseCode;
+ }
+
+ public void setNewResponseCode(int newResponseCode) {
+ this.newResponseCode = newResponseCode;
+ }
+
+ @Override
+ public void handle(ResteasyReactiveRequestContext requestContext) throws Exception {
+ ResponseImpl response = (ResponseImpl) requestContext.getResponse().get();
+ if (response.getStatus() == defaultResponseCode) { // only set the status if it has not already been set
+ response.setStatus(newResponseCode);
+ }
+ }
+}
diff --git a/extensions/spring-web/resteasy-reactive/runtime/src/main/java/io/quarkus/spring/web/runtime/ResteasyReactiveResponseContentTypeResolver.java b/extensions/spring-web/resteasy-reactive/runtime/src/main/java/io/quarkus/spring/web/runtime/ResteasyReactiveResponseContentTypeResolver.java
new file mode 100644
index 0000000000000..2324cb923e5ae
--- /dev/null
+++ b/extensions/spring-web/resteasy-reactive/runtime/src/main/java/io/quarkus/spring/web/runtime/ResteasyReactiveResponseContentTypeResolver.java
@@ -0,0 +1,21 @@
+package io.quarkus.spring.web.runtime;
+
+import java.util.List;
+
+import javax.ws.rs.core.Variant;
+
+import org.jboss.resteasy.reactive.server.core.request.ServerDrivenNegotiation;
+
+import io.quarkus.spring.web.runtime.common.AbstractResponseContentTypeResolver;
+
+@SuppressWarnings("unused")
+public class ResteasyReactiveResponseContentTypeResolver extends AbstractResponseContentTypeResolver {
+
+ @Override
+ protected Variant negotiateBestMatch(List acceptHeaders, List variants) {
+ ServerDrivenNegotiation negotiation = new ServerDrivenNegotiation();
+ negotiation.setAcceptHeaders(acceptHeaders);
+
+ return negotiation.getBestMatch(variants);
+ }
+}
diff --git a/extensions/spring-web/resteasy-reactive/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/spring-web/resteasy-reactive/runtime/src/main/resources/META-INF/quarkus-extension.yaml
new file mode 100644
index 0000000000000..2ecdd23b44ad4
--- /dev/null
+++ b/extensions/spring-web/resteasy-reactive/runtime/src/main/resources/META-INF/quarkus-extension.yaml
@@ -0,0 +1,5 @@
+---
+artifact: ${project.groupId}:${project.artifactId}:${project.version}
+name: "Spring Web RESTEasy Reactive"
+metadata:
+ unlisted: true
diff --git a/extensions/spring-web/resteasy-reactive/tests/pom.xml b/extensions/spring-web/resteasy-reactive/tests/pom.xml
new file mode 100644
index 0000000000000..1ebdfb1d54304
--- /dev/null
+++ b/extensions/spring-web/resteasy-reactive/tests/pom.xml
@@ -0,0 +1,56 @@
+
+
+
+ quarkus-spring-web-resteasy-reactive-parent
+ io.quarkus
+ 999-SNAPSHOT
+
+
+ 4.0.0
+
+
+ quarkus-spring-web-resteasy-reactive-tests
+ Quarkus - Spring Web - RESTEasy Reactive - Tests
+
+
+ true
+ true
+
+
+
+
+ io.quarkus
+ quarkus-resteasy-reactive-jackson-deployment
+
+
+ io.quarkus
+ quarkus-spring-web-deployment
+
+
+ io.quarkus
+ quarkus-spring-web-resteasy-reactive-deployment
+
+
+ io.quarkus
+ quarkus-resteasy-common-spi
+
+
+
+ io.quarkus
+ quarkus-junit5-internal
+ test
+
+
+ io.rest-assured
+ rest-assured
+ test
+
+
+ org.assertj
+ assertj-core
+ test
+
+
+
diff --git a/extensions/spring-web/resteasy-reactive/tests/src/test/java/io/quarkus/spring/web/test/BasicMappingTest.java b/extensions/spring-web/resteasy-reactive/tests/src/test/java/io/quarkus/spring/web/test/BasicMappingTest.java
new file mode 100644
index 0000000000000..6bacc2170e5f5
--- /dev/null
+++ b/extensions/spring-web/resteasy-reactive/tests/src/test/java/io/quarkus/spring/web/test/BasicMappingTest.java
@@ -0,0 +1,226 @@
+package io.quarkus.spring.web.test;
+
+import static io.restassured.RestAssured.given;
+import static io.restassured.RestAssured.when;
+import static org.hamcrest.Matchers.is;
+
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.spec.JavaArchive;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.quarkus.test.QuarkusUnitTest;
+
+public class BasicMappingTest {
+
+ @RegisterExtension
+ static QuarkusUnitTest runner = new QuarkusUnitTest()
+ .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
+ .addClasses(SomeClass.class, Greeting.class, TestController.class, ResponseEntityController.class,
+ ResponseStatusController.class, GreetingControllerWithNoRequestMapping.class));
+
+ @Test
+ public void verifyGetWithQueryParam() {
+ when().get(TestController.CONTROLLER_PATH + "/hello?name=people")
+ .then()
+ .statusCode(200)
+ .contentType("text/plain")
+ .body(is("hello people"));
+ }
+
+ @Test
+ public void verifyRequestMappingWithNoMethod() {
+ when().get(TestController.CONTROLLER_PATH + "/hello4?name=people")
+ .then()
+ .statusCode(200)
+ .contentType("text/plain")
+ .body(is("hello people"));
+ }
+
+ @Test
+ public void verifyGetToMethodWithoutForwardSlash() {
+ when().get(TestController.CONTROLLER_PATH + "/yolo")
+ .then()
+ .statusCode(200)
+ .body(is("yolo"));
+ }
+
+ @Test
+ public void verifyGetUsingDefaultValue() {
+ when().get(TestController.CONTROLLER_PATH + "/hello2")
+ .then()
+ .statusCode(200)
+ .body(is("hello world"));
+ }
+
+ @Test
+ public void verifyGetUsingNonLatinChars() {
+ when().get(TestController.CONTROLLER_PATH + "/hello3?name=Γιώργος")
+ .then()
+ .statusCode(200)
+ .body(is("hello Γιώργος"));
+ }
+
+ @Test
+ public void verifyPathWithWildcard() {
+ when().get(TestController.CONTROLLER_PATH + "/wildcard/whatever/world")
+ .then()
+ .statusCode(200)
+ .body(is("world"));
+ }
+
+ @Test
+ public void verifyPathWithMultipleWildcards() {
+ when().get(TestController.CONTROLLER_PATH + "/wildcard2/something/folks/somethingelse")
+ .then()
+ .statusCode(200)
+ .body(is("folks"));
+ }
+
+ @Test
+ public void verifyPathWithAntStyleWildCard() {
+ when().get(TestController.CONTROLLER_PATH + "/antwildcard/whatever/we/want")
+ .then()
+ .statusCode(200)
+ .body(is("ant"));
+ }
+
+ @Test
+ public void verifyPathWithCharacterWildCard() {
+ for (char c : new char[] { 't', 'r' }) {
+ when().get(TestController.CONTROLLER_PATH + String.format("/ca%cs", c))
+ .then()
+ .statusCode(200)
+ .body(is("single"));
+ }
+ }
+
+ @Test
+ public void verifyPathWithMultipleCharacterWildCards() {
+ for (String path : new String[] { "/cars/shop/info", "/cart/show/info" }) {
+ when().get(TestController.CONTROLLER_PATH + path)
+ .then()
+ .statusCode(200)
+ .body(is("multiple"));
+ }
+ }
+
+ @Test
+ public void verifyPathVariableTypeConversion() {
+ when().get(TestController.CONTROLLER_PATH + "/int/9")
+ .then()
+ .statusCode(200)
+ .body(is("10"));
+ }
+
+ @Test
+ public void verifyJsonGetWithPathParamAndGettingMapping() {
+ when().get(TestController.CONTROLLER_PATH + "/json/dummy")
+ .then()
+ .statusCode(200)
+ .contentType("application/json")
+ .body("message", is("dummy"));
+ }
+
+ @Test
+ public void verifyJsonOnRequestMappingGetWithPathParamAndRequestMapping() {
+ when().get(TestController.CONTROLLER_PATH + "/json2/dummy")
+ .then()
+ .statusCode(200)
+ .contentType("application/json")
+ .body("message", is("dummy"));
+ }
+
+ @Test
+ public void verifyJsonPostWithPostMapping() {
+ given().body("{\"message\": \"hi\"}")
+ .contentType("application/json")
+ .when().post(TestController.CONTROLLER_PATH + "/json")
+ .then()
+ .statusCode(200)
+ .contentType("text/plain")
+ .body(is("hi"));
+ }
+
+ @Test
+ public void verifyJsonPostWithRequestMapping() {
+ given().body("{\"message\": \"hi\"}")
+ .contentType("application/json")
+ .when().post(TestController.CONTROLLER_PATH + "/json2")
+ .then()
+ .statusCode(200)
+ .contentType("text/plain")
+ .body(is("hi"));
+ }
+
+ @Test
+ public void verifyMultipleInputAndJsonResponse() {
+ given().body("{\"message\": \"hi\"}")
+ .contentType("application/json")
+ .when().put(TestController.CONTROLLER_PATH + "/json3?suffix=!")
+ .then()
+ .statusCode(200)
+ .contentType("application/json")
+ .body("message", is("hi!"));
+ }
+
+ @Test
+ public void verifyEmptyContentResponseEntity() {
+ when().get(ResponseEntityController.CONTROLLER_PATH + "/noContent")
+ .then()
+ .statusCode(204);
+ }
+
+ @Test
+ public void verifyStringContentResponseEntity() {
+ when().get(ResponseEntityController.CONTROLLER_PATH + "/string")
+ .then()
+ .statusCode(200)
+ .contentType("text/plain")
+ .body(is("hello world"));
+ }
+
+ @Test
+ public void verifyJsonContentResponseEntity() {
+ when().get(ResponseEntityController.CONTROLLER_PATH + "/json")
+ .then()
+ .statusCode(200)
+ .contentType("application/json")
+ .body("message", is("dummy"))
+ .header("custom-header", "somevalue");
+ }
+
+ @Test
+ public void verifyJsonContentResponseEntityWithoutType() {
+ when().get(ResponseEntityController.CONTROLLER_PATH + "/json2")
+ .then()
+ .statusCode(200)
+ .contentType("application/json")
+ .body("message", is("dummy"));
+ }
+
+ @Test
+ public void verifyEmptyContentResponseStatus() {
+ when().get(ResponseStatusController.CONTROLLER_PATH + "/noContent")
+ .then()
+ .statusCode(200);
+ }
+
+ @Test
+ public void verifyStringResponseStatus() {
+ when().get(ResponseStatusController.CONTROLLER_PATH + "/string")
+ .then()
+ .statusCode(202)
+ .contentType("text/plain")
+ .body(is("accepted"));
+ }
+
+ @Test
+ public void verifyControllerWithoutRequestMapping() {
+ when().get("/hello")
+ .then()
+ .statusCode(200)
+ .contentType("text/plain")
+ .body(is("hello world"));
+ }
+}
diff --git a/extensions/spring-web/resteasy-reactive/tests/src/test/java/io/quarkus/spring/web/test/Greeting.java b/extensions/spring-web/resteasy-reactive/tests/src/test/java/io/quarkus/spring/web/test/Greeting.java
new file mode 100644
index 0000000000000..ebe214bd3ae1e
--- /dev/null
+++ b/extensions/spring-web/resteasy-reactive/tests/src/test/java/io/quarkus/spring/web/test/Greeting.java
@@ -0,0 +1,14 @@
+package io.quarkus.spring.web.test;
+
+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/resteasy-reactive/tests/src/test/java/io/quarkus/spring/web/test/GreetingControllerWithNoRequestMapping.java b/extensions/spring-web/resteasy-reactive/tests/src/test/java/io/quarkus/spring/web/test/GreetingControllerWithNoRequestMapping.java
new file mode 100644
index 0000000000000..e79b2bb3e67dc
--- /dev/null
+++ b/extensions/spring-web/resteasy-reactive/tests/src/test/java/io/quarkus/spring/web/test/GreetingControllerWithNoRequestMapping.java
@@ -0,0 +1,13 @@
+package io.quarkus.spring.web.test;
+
+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/resteasy-reactive/tests/src/test/java/io/quarkus/spring/web/test/ResponseEntityController.java b/extensions/spring-web/resteasy-reactive/tests/src/test/java/io/quarkus/spring/web/test/ResponseEntityController.java
new file mode 100644
index 0000000000000..31bd989f05245
--- /dev/null
+++ b/extensions/spring-web/resteasy-reactive/tests/src/test/java/io/quarkus/spring/web/test/ResponseEntityController.java
@@ -0,0 +1,33 @@
+package io.quarkus.spring.web.test;
+
+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/resteasy-reactive/tests/src/test/java/io/quarkus/spring/web/test/ResponseStatusAndExceptionHandlerTest.java b/extensions/spring-web/resteasy-reactive/tests/src/test/java/io/quarkus/spring/web/test/ResponseStatusAndExceptionHandlerTest.java
new file mode 100644
index 0000000000000..20c3c8a9cb104
--- /dev/null
+++ b/extensions/spring-web/resteasy-reactive/tests/src/test/java/io/quarkus/spring/web/test/ResponseStatusAndExceptionHandlerTest.java
@@ -0,0 +1,70 @@
+package io.quarkus.spring.web.test;
+
+import static io.restassured.RestAssured.when;
+
+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 org.springframework.http.HttpStatus;
+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.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+import io.quarkus.test.QuarkusUnitTest;
+
+public class ResponseStatusAndExceptionHandlerTest {
+
+ @RegisterExtension
+ static QuarkusUnitTest runner = new QuarkusUnitTest()
+ .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
+ .addClasses(ExceptionController.class, RestExceptionHandler.class));
+
+ @Test
+ public void testRestControllerAdvice() {
+ when().get("/exception").then().statusCode(400);
+ }
+
+ @Test
+ public void testResponseStatusOnException() {
+ when().get("/exception2").then().statusCode(202);
+ }
+
+ @RestController
+ public static class ExceptionController {
+
+ public static final StackTraceElement[] EMPTY_STACK_TRACE = new StackTraceElement[0];
+
+ @GetMapping("/exception")
+ @ResponseStatus(HttpStatus.OK)
+ public String throwRuntimeException() {
+ RuntimeException runtimeException = new RuntimeException();
+ runtimeException.setStackTrace(EMPTY_STACK_TRACE);
+ throw runtimeException;
+ }
+
+ @GetMapping("/exception2")
+ public String throwMyException() {
+ MyException myException = new MyException();
+ myException.setStackTrace(EMPTY_STACK_TRACE);
+ throw myException;
+ }
+ }
+
+ @RestControllerAdvice
+ public static class RestExceptionHandler {
+
+ @ExceptionHandler(RuntimeException.class)
+ public ResponseEntity