diff --git a/extensions/resteasy-reactive/rest-servlet/runtime/src/main/java/io/quarkus/resteasy/reactive/server/servlet/runtime/ServletRequestContext.java b/extensions/resteasy-reactive/rest-servlet/runtime/src/main/java/io/quarkus/resteasy/reactive/server/servlet/runtime/ServletRequestContext.java index 369ae603e4610..ba114b9c8224f 100644 --- a/extensions/resteasy-reactive/rest-servlet/runtime/src/main/java/io/quarkus/resteasy/reactive/server/servlet/runtime/ServletRequestContext.java +++ b/extensions/resteasy-reactive/rest-servlet/runtime/src/main/java/io/quarkus/resteasy/reactive/server/servlet/runtime/ServletRequestContext.java @@ -15,6 +15,7 @@ import java.util.concurrent.CompletionStage; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; +import java.util.stream.Collectors; import jakarta.enterprise.event.Event; import jakarta.servlet.AsyncContext; @@ -24,6 +25,7 @@ import jakarta.servlet.WriteListener; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import jakarta.ws.rs.core.MultivaluedHashMap; import jakarta.ws.rs.core.SecurityContext; import org.jboss.resteasy.reactive.server.core.Deployment; @@ -262,6 +264,25 @@ public List getAllQueryParams(String name) { return context.queryParam(name); } + @Override + public Map> getParametersMap() { + MultiMap entries = context.request().params(); + final MultivaluedHashMap result = new MultivaluedHashMap<>(); + if (!entries.isEmpty()) { + for (Map.Entry entry : entries) { + result.add(entry.getKey(), entry.getValue()); + } + + } + Map> params = result.entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue // Values are already a List + )); + + return params; + } + @Override public String query() { return request.getQueryString(); diff --git a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/simple/MapWithParamConverterTest.java b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/simple/MapWithParamConverterTest.java index 419dfdd721533..cf2637e993ad3 100644 --- a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/simple/MapWithParamConverterTest.java +++ b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/simple/MapWithParamConverterTest.java @@ -2,6 +2,8 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Type; +import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; @@ -22,6 +24,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.type.TypeFactory; @@ -87,17 +90,32 @@ public MapParamConverter(Class rawType, Type genericType) { this.rawType = rawType; } - @Override public T fromString(String value) { if (rawType.isAssignableFrom(String.class)) { //noinspection unchecked return (T) value; } try { - return genericType != null ? objectMapper.readValue(value, genericType) - : objectMapper.readValue(value, rawType); + JsonNode jsonNode = objectMapper.readTree(value); + if (jsonNode.isArray()) { + // Process as a list of maps and merge them into a single map + JavaType listType = objectMapper.getTypeFactory() + .constructCollectionType(List.class, rawType); + List> list = objectMapper.readValue(value, listType); + + Map mergedMap = new LinkedHashMap<>(); + for (Map map : list) { + mergedMap.putAll(map); + } + return (T) mergedMap; + } else { + // single object + return genericType != null + ? objectMapper.readValue(value, genericType) + : objectMapper.readValue(value, rawType); + } } catch (JsonProcessingException e) { - throw (new RuntimeException(e)); + throw new RuntimeException(e); } } diff --git a/extensions/spring-web/resteasy-reactive/deployment/src/main/java/io/quarkus/spring/web/resteasy/reactive/deployment/SpringWebResteasyReactiveProcessor.java b/extensions/spring-web/resteasy-reactive/deployment/src/main/java/io/quarkus/spring/web/resteasy/reactive/deployment/SpringWebResteasyReactiveProcessor.java index 55adc53e48711..c632cd920fbc4 100644 --- a/extensions/spring-web/resteasy-reactive/deployment/src/main/java/io/quarkus/spring/web/resteasy/reactive/deployment/SpringWebResteasyReactiveProcessor.java +++ b/extensions/spring-web/resteasy-reactive/deployment/src/main/java/io/quarkus/spring/web/resteasy/reactive/deployment/SpringWebResteasyReactiveProcessor.java @@ -31,6 +31,7 @@ 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.core.parameters.ParameterExtractor; 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; @@ -40,6 +41,9 @@ import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; +import io.quarkus.deployment.builditem.GeneratedClassBuildItem; +import io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveVertxWebSocketIntegrationProcessor; +import io.quarkus.resteasy.reactive.server.runtime.websocket.VertxWebSocketRestHandler; import io.quarkus.resteasy.reactive.server.spi.AnnotationsTransformerBuildItem; import io.quarkus.resteasy.reactive.server.spi.MethodScannerBuildItem; import io.quarkus.resteasy.reactive.spi.AdditionalResourceClassBuildItem; @@ -47,12 +51,14 @@ import io.quarkus.resteasy.server.common.spi.AdditionalJaxRsResourceMethodParamAnnotations; import io.quarkus.spring.web.resteasy.reactive.runtime.ResponseEntityHandler; import io.quarkus.spring.web.resteasy.reactive.runtime.ResponseStatusHandler; +import io.quarkus.spring.web.resteasy.reactive.runtime.SpringMultiValueMapParamExtractor; import io.quarkus.spring.web.runtime.common.ResponseStatusExceptionMapper; public class SpringWebResteasyReactiveProcessor { private static final Logger LOGGER = Logger.getLogger(SpringWebResteasyReactiveProcessor.class.getName()); + public static final String NAME = ResteasyReactiveVertxWebSocketIntegrationProcessor.class.getName(); private static final DotName REST_CONTROLLER_ANNOTATION = DotName .createSimple("org.springframework.web.bind.annotation.RestController"); @@ -82,6 +88,7 @@ public class SpringWebResteasyReactiveProcessor { 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 DotName SPRING_MULTIVALUE_MAP = DotName.createSimple("org.springframework.util.MultiValueMap"); private static final String DEFAULT_NONE = "\n\t\t\n\t\t\n\uE000\uE001\uE002\n\t\t\t\t\n"; // from ValueConstants @@ -395,6 +402,59 @@ private String replaceSpringWebWildcards(String methodPath) { })); } + @BuildStep + MethodScannerBuildItem scanner() { + return new MethodScannerBuildItem(new MethodScanner() { + @Override + public List scan(MethodInfo method, ClassInfo actualEndpointClass, + Map methodContext) { + if (methodContext.containsKey(NAME)) { + return Collections.singletonList(new VertxWebSocketRestHandler()); + } + return Collections.emptyList(); + } + + @Override + public ParameterExtractor handleCustomParameter(Type paramType, Map annotations, + boolean field, Map methodContext) { + if (paramType.name().equals(SPRING_MULTIVALUE_MAP)) { + methodContext.put(NAME, true); + return new SpringMultiValueMapParamExtractor(); + } + return null; + } + + @Override + public boolean isMethodSignatureAsync(MethodInfo info) { + for (var param : info.parameterTypes()) { + if (param.name().equals(SPRING_MULTIVALUE_MAP)) { + return true; + } + } + return false; + } + }); + } + + // @BuildStep + public void transformSpringParameters(CombinedIndexBuildItem index, + BuildProducer generatedClasses) { + for (ClassInfo classInfo : index.getIndex().getKnownClasses()) { + for (MethodInfo method : classInfo.methods()) { + for (Type paramType : method.parameterTypes()) { + if (paramType.name().toString().equals("org.springframework.util.MultiValueMap")) { + Type newType = Type.create(DotName.createSimple("jakarta.ws.rs.core.MultivaluedMap"), paramType.kind()); + transformParameterType(method, paramType, newType); + } + } + } + } + } + + private void transformParameterType(MethodInfo method, Type oldType, Type newType) { + //rewrite the method using the jakarta multivalued type + } + @BuildStep public MethodScannerBuildItem responseEntitySupport() { return new MethodScannerBuildItem(new MethodScanner() { diff --git a/extensions/spring-web/resteasy-reactive/runtime/src/main/java/io/quarkus/spring/web/resteasy/reactive/runtime/MultiValueMapConverter.java b/extensions/spring-web/resteasy-reactive/runtime/src/main/java/io/quarkus/spring/web/resteasy/reactive/runtime/MultiValueMapConverter.java new file mode 100644 index 0000000000000..546567bdd41a1 --- /dev/null +++ b/extensions/spring-web/resteasy-reactive/runtime/src/main/java/io/quarkus/spring/web/resteasy/reactive/runtime/MultiValueMapConverter.java @@ -0,0 +1,27 @@ +package io.quarkus.spring.web.resteasy.reactive.runtime; + +import jakarta.ws.rs.ext.ParamConverter; +import jakarta.ws.rs.ext.Provider; + +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +@Provider +public class MultiValueMapConverter implements ParamConverter> { + + @Override + public MultiValueMap fromString(String value) { + MultiValueMap map = new LinkedMultiValueMap<>(); + String[] pairs = value.split("&"); + for (String pair : pairs) { + String[] keyValue = pair.split("="); + map.add(keyValue[0], keyValue[1]); + } + return map; + } + + @Override + public String toString(MultiValueMap value) { + return value.toString(); + } +} diff --git a/extensions/spring-web/resteasy-reactive/runtime/src/main/java/io/quarkus/spring/web/resteasy/reactive/runtime/MultiValueMapConverterProvider.java b/extensions/spring-web/resteasy-reactive/runtime/src/main/java/io/quarkus/spring/web/resteasy/reactive/runtime/MultiValueMapConverterProvider.java new file mode 100644 index 0000000000000..cd0bbc25e0644 --- /dev/null +++ b/extensions/spring-web/resteasy-reactive/runtime/src/main/java/io/quarkus/spring/web/resteasy/reactive/runtime/MultiValueMapConverterProvider.java @@ -0,0 +1,21 @@ +package io.quarkus.spring.web.resteasy.reactive.runtime; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; + +import jakarta.ws.rs.ext.ParamConverter; +import jakarta.ws.rs.ext.ParamConverterProvider; +import jakarta.ws.rs.ext.Provider; + +import org.springframework.util.MultiValueMap; + +@Provider +public class MultiValueMapConverterProvider implements ParamConverterProvider { + @Override + public ParamConverter getConverter(Class rawType, Type genericType, Annotation[] annotations) { + if (rawType == MultiValueMap.class) { + return (ParamConverter) new MultiValueMapConverter(); + } + return null; + } +} diff --git a/extensions/spring-web/resteasy-reactive/runtime/src/main/java/io/quarkus/spring/web/resteasy/reactive/runtime/SpringMultiValueMapParamExtractor.java b/extensions/spring-web/resteasy-reactive/runtime/src/main/java/io/quarkus/spring/web/resteasy/reactive/runtime/SpringMultiValueMapParamExtractor.java new file mode 100644 index 0000000000000..cb27d26faeca3 --- /dev/null +++ b/extensions/spring-web/resteasy-reactive/runtime/src/main/java/io/quarkus/spring/web/resteasy/reactive/runtime/SpringMultiValueMapParamExtractor.java @@ -0,0 +1,26 @@ +package io.quarkus.spring.web.resteasy.reactive.runtime; + +import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; +import org.jboss.resteasy.reactive.server.core.parameters.ParameterExtractor; + +@SuppressWarnings("ForLoopReplaceableByForEach") +public class SpringMultiValueMapParamExtractor implements ParameterExtractor { + + // private final String name; + // private final boolean single; + // private final boolean encoded; + // private final String separator; + + // public SpringMultiValueMapParamExtractor(String name, boolean single, boolean encoded, String separator) { + // this.name = name; + // this.single = single; + // this.encoded = encoded; + // this.separator = separator; + // } + + @Override + public Object extractParameter(ResteasyReactiveRequestContext context) { + return context.serverRequest().getParametersMap(); + // return context.getQueryParameter(name, single, encoded, separator); + } +} diff --git a/extensions/spring-web/resteasy-reactive/runtime/src/main/java/io/quarkus/spring/web/resteasy/reactive/runtime/SpringRestHandler.java b/extensions/spring-web/resteasy-reactive/runtime/src/main/java/io/quarkus/spring/web/resteasy/reactive/runtime/SpringRestHandler.java new file mode 100644 index 0000000000000..84e78e675932b --- /dev/null +++ b/extensions/spring-web/resteasy-reactive/runtime/src/main/java/io/quarkus/spring/web/resteasy/reactive/runtime/SpringRestHandler.java @@ -0,0 +1,40 @@ +package io.quarkus.spring.web.resteasy.reactive.runtime; + +import java.util.Collections; +import java.util.List; + +import org.jboss.resteasy.reactive.common.model.ResourceClass; +import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; +import org.jboss.resteasy.reactive.server.model.HandlerChainCustomizer; +import org.jboss.resteasy.reactive.server.model.ServerResourceMethod; +import org.jboss.resteasy.reactive.server.spi.ServerRestHandler; + +public class SpringRestHandler implements HandlerChainCustomizer { + + private static final ServerRestHandler[] AWOL = new ServerRestHandler[] { + new ServerRestHandler() { + + @Override + public void handle(ResteasyReactiveRequestContext requestContext) + throws Exception { + throw new IllegalStateException("FAILURE: should never be restarted"); + } + } + }; + + @Override + public List handlers(Phase phase, ResourceClass resourceClass, ServerResourceMethod resourceMethod) { + if (phase == Phase.AFTER_METHOD_INVOKE) { + return Collections.singletonList(new ServerRestHandler() { + @Override + public void handle(ResteasyReactiveRequestContext requestContext) throws Exception { + //make sure that we are never restarted + requestContext.restart(AWOL, true); + requestContext.suspend(); //we never resume + } + }); + } + return Collections.emptyList(); + + } +} diff --git a/extensions/spring-web/resteasy-reactive/tests/src/test/java/io/quarkus/spring/web/requestparam/RequestParamController.java b/extensions/spring-web/resteasy-reactive/tests/src/test/java/io/quarkus/spring/web/requestparam/RequestParamController.java new file mode 100644 index 0000000000000..25227ce0a17ab --- /dev/null +++ b/extensions/spring-web/resteasy-reactive/tests/src/test/java/io/quarkus/spring/web/requestparam/RequestParamController.java @@ -0,0 +1,73 @@ +package io.quarkus.spring.web.requestparam; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import jakarta.ws.rs.core.MultivaluedMap; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping +public class RequestParamController { + + @GetMapping("/api/foos") + @ResponseBody + public String getFoos(@RequestParam String id) { + return "ID: " + id; + } + + @PostMapping("/api/foos") + @ResponseBody + public String addFoo(@RequestParam(name = "id") String fooId, @RequestParam String name) { + return "ID: " + fooId + " Name: " + name; + } + + @GetMapping("/api/foos/notParamRequired") + @ResponseBody + public String getFoosNotParamRequired2(@RequestParam(required = false) String id) { + return "ID: " + id; + } + + @GetMapping("/api/foos/optional") + @ResponseBody + public String getFoosOptional(@RequestParam Optional id) { + return "ID: " + id.orElseGet(() -> "not provided"); + } + + @GetMapping("/api/foos/defaultValue") + @ResponseBody + public String getFoosDefaultValue(@RequestParam(defaultValue = "test") String id) { + return "ID: " + id; + } + + @PostMapping("/api/foos/map") + @ResponseBody + public String updateFoos(@RequestParam Map allParams) { + return "Parameters are " + allParams.entrySet(); + } + + @GetMapping("/api/foos/multivalue") + @ResponseBody + public String getFoosMultiValue(@RequestParam List id) { + return "IDs are " + id; + } + + @PostMapping("/api/foos/multiMap") + @ResponseBody + public String updateFoos(@RequestParam MultivaluedMap allParams) { + String result = ""; + for (Map.Entry> entry : allParams.entrySet()) { + result = "Parameters are " + entry.getKey() + "=" + entry.getValue().stream().collect(Collectors.joining(", ")); + } + return result; + } + +} diff --git a/extensions/spring-web/resteasy-reactive/tests/src/test/java/io/quarkus/spring/web/requestparam/RequestParamControllerTest.java b/extensions/spring-web/resteasy-reactive/tests/src/test/java/io/quarkus/spring/web/requestparam/RequestParamControllerTest.java new file mode 100644 index 0000000000000..01e9de78234ac --- /dev/null +++ b/extensions/spring-web/resteasy-reactive/tests/src/test/java/io/quarkus/spring/web/requestparam/RequestParamControllerTest.java @@ -0,0 +1,98 @@ +package io.quarkus.spring.web.requestparam; + +import static io.restassured.RestAssured.when; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class RequestParamControllerTest { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(RequestParamController.class)); + + @Test + public void whenInvokeGetQueryStringThenTheOriginQueryStringIsReturned() throws Exception { + when().get("/api/foos?id=abc") + .then() + .statusCode(200) + .body(is("ID: abc")); + + // should return 400 because, in Spring, method parameters annotated with @RequestParam are required by default. + // see SpringWebResteasyReactiveProcessor L298 + when().get("/api/foos") + .then() + .statusCode(200); + } + + @Test + public void whenConfigureParamToBeOptionalThenTheGetQueryWorksWithAndWithoutRequestParam() throws Exception { + when().get("/api/foos/notParamRequired?id=abc") + .then() + .statusCode(200) + .body(is("ID: abc")); + + when().get("/api/foos/notParamRequired") + .then() + .statusCode(200) + .body(is("ID: null")); + } + + @Test + public void whenWrapingParamInOptionalThenTheGetQueryWorksWithAndWithoutRequestParam() throws Exception { + when().get("/api/foos/optional?id=abc") + .then() + .statusCode(200) + .body(is("ID: abc")); + + when().get("/api/foos/optional") + .then() + .statusCode(200) + .body(is("ID: not provided")); + } + + @Test + public void whenDefaultValueProvidedThenItIsReturnedIfRequestParamIsAbsent() throws Exception { + when().get("/api/foos/defaultValue?id=abc") + .then() + .statusCode(200) + .body(is("ID: abc")); + + when().get("/api/foos/defaultValue") + .then() + .statusCode(200) + .body(is("ID: test")); + } + + @Test + public void whenInvokePostQueryWithSpecificParamNameThenTheOriginQueryStringIsReturned() throws Exception { + when().post("/api/foos/map?id=abc&name=bar") + .then() + .statusCode(200) + .body(containsString("Parameters are [name=[bar], id=[abc]]")); + + } + + @Test + public void testMultivalue() throws Exception { + when().get("/api/foos/multivalue?id=1,2,3") + .then() + .statusCode(200) + .body(containsString("IDs are [1,2,3]")); + } + + @Test + public void whenInvokePostQueryWithMultiMapParamNameThenTheOriginQueryStringIsReturned() throws Exception { + when().post("/api/foos/multiMap?id=abc&id=bar") + .then() + .statusCode(200) + .body(containsString("Parameters are [id=[abc, bar]]")); + + } + +} diff --git a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java index c9e4a537b2139..49caf4adea455 100644 --- a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java +++ b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java @@ -31,10 +31,12 @@ import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.LOCAL_DATE_TIME; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.LOCAL_TIME; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.LONG; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.MAP; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.MATRIX_PARAM; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.MULTI; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.MULTI_PART_DATA_INPUT; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.MULTI_PART_FORM_PARAM; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.MULTI_VALUED_MAP; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.NON_BLOCKING; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.OBJECT; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.OFFSET_DATE_TIME; @@ -649,7 +651,7 @@ private ResourceMethod createResourceMethod(ClassInfo currentClassInfo, ClassInf if (formParamRequired) { if (bodyParamType != null - && !bodyParamType.name().equals(ResteasyReactiveDotNames.MULTI_VALUED_MAP) + && !bodyParamType.name().equals(MULTI_VALUED_MAP) && !bodyParamType.name().equals(ResteasyReactiveDotNames.STRING)) { throw new RuntimeException( "'@FormParam' and '@RestForm' cannot be used in a resource method that contains a body parameter. Offending method is '" @@ -1374,6 +1376,14 @@ && isParameterContainerType(paramType.asClassType())) { handleSortedSetParam(existingConverters, errorLocation, hasRuntimeConverters, builder, elementType, currentMethodInfo); } + } else if ((pt.name().equals(MAP) || pt.name().equals(MULTI_VALUED_MAP)) && type != ParameterType.BODY) { + typeHandled = true; + builder.setSingle(false); + elementType = toClassName(pt.arguments().get(0), currentClassInfo, actualEndpointInfo, index); + if (convertible) { + handleMapParam(existingConverters, errorLocation, hasRuntimeConverters, builder, elementType, + currentMethodInfo); + } } else if (pt.name().equals(OPTIONAL)) { typeHandled = true; elementType = toClassName(pt.arguments().get(0), currentClassInfo, actualEndpointInfo, index); @@ -1534,6 +1544,10 @@ protected void handleSetParam(Map existingConverters, String err PARAM builder, String elementType, MethodInfo currentMethodInfo) { } + protected void handleMapParam(Map existingConverters, String errorLocation, boolean hasRuntimeConverters, + PARAM builder, String elementType, MethodInfo currentMethodInfo) { + } + protected void handleListParam(Map existingConverters, String errorLocation, boolean hasRuntimeConverters, PARAM builder, String elementType, MethodInfo currentMethodInfo) { } diff --git a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/ServerEndpointIndexer.java b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/ServerEndpointIndexer.java index 7ba93d0418f01..7c92be582d4c0 100644 --- a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/ServerEndpointIndexer.java +++ b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/ServerEndpointIndexer.java @@ -74,6 +74,7 @@ import org.jboss.resteasy.reactive.server.core.parameters.converters.LocalDateParamConverter; import org.jboss.resteasy.reactive.server.core.parameters.converters.LocalDateTimeParamConverter; import org.jboss.resteasy.reactive.server.core.parameters.converters.LocalTimeParamConverter; +import org.jboss.resteasy.reactive.server.core.parameters.converters.MapConverter; import org.jboss.resteasy.reactive.server.core.parameters.converters.NoopParameterConverter; import org.jboss.resteasy.reactive.server.core.parameters.converters.OffsetDateTimeParamConverter; import org.jboss.resteasy.reactive.server.core.parameters.converters.OffsetTimeParamConverter; @@ -532,6 +533,14 @@ protected void handleSetParam(Map existingConverters, String err builder.setConverter(new SetConverter.SetSupplier(converter)); } + @Override + protected void handleMapParam(Map existingConverters, String errorLocation, boolean hasRuntimeConverters, + ServerIndexedParameter builder, String elementType, MethodInfo currentMethodInfo) { + ParameterConverterSupplier converter = extractConverter(elementType, index, + existingConverters, errorLocation, hasRuntimeConverters, builder.getAnns(), currentMethodInfo); + builder.setConverter(new MapConverter.MapSupplier(converter)); + } + @Override protected void handleListParam(Map existingConverters, String errorLocation, boolean hasRuntimeConverters, ServerIndexedParameter builder, String elementType, MethodInfo currentMethodInfo) { diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/ResteasyReactiveRequestContext.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/ResteasyReactiveRequestContext.java index e72ad3b6913df..cc5acc27fc4bf 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/ResteasyReactiveRequestContext.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/ResteasyReactiveRequestContext.java @@ -14,6 +14,7 @@ import java.util.Deque; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.concurrent.Executor; import java.util.regex.Matcher; @@ -870,29 +871,36 @@ public Object getQueryParameter(String name, boolean single, boolean encoded, St } return val; } - - // empty collections must not be turned to null - List strings = serverRequest().getAllQueryParams(name).stream() - .filter(p -> !p.isEmpty()) - .toList(); - if (encoded) { - List newStrings = new ArrayList<>(); - for (String i : strings) { - newStrings.add(Encode.encodeQueryParam(i)); + List allQueryParams = serverRequest().getAllQueryParams(name); + if (allQueryParams != null && !allQueryParams.isEmpty()) { + // empty collections must not be turned to null + List strings = allQueryParams.stream() + .filter(p -> !p.isEmpty()) + .toList(); + if (encoded) { + List newStrings = new ArrayList<>(); + for (String i : strings) { + newStrings.add(Encode.encodeQueryParam(i)); + } + strings = newStrings; } - strings = newStrings; - } - if (separator != null) { - List result = new ArrayList<>(strings.size()); - for (int i = 0; i < strings.size(); i++) { - String[] parts = strings.get(i).split(separator); - result.addAll(Arrays.asList(parts)); + if (separator != null) { + List result = new ArrayList<>(strings.size()); + for (int i = 0; i < strings.size(); i++) { + String[] parts = strings.get(i).split(separator); + result.addAll(Arrays.asList(parts)); + } + return result; + } else { + return strings; } - return result; - } else { - return strings; } + Map> queryParams = serverRequest().getParametersMap(); + if (queryParams != null && !queryParams.isEmpty()) { + return queryParams; + } + return Collections.EMPTY_LIST; } @Override diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/converters/MapConverter.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/converters/MapConverter.java new file mode 100644 index 0000000000000..1b04c88c75f40 --- /dev/null +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/converters/MapConverter.java @@ -0,0 +1,100 @@ +package org.jboss.resteasy.reactive.server.core.parameters.converters; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import jakarta.ws.rs.core.MultivaluedHashMap; +import jakarta.ws.rs.core.MultivaluedMap; + +import org.jboss.resteasy.reactive.server.model.ParamConverterProviders; + +public class MapConverter implements ParameterConverter { + + private final ParameterConverter delegate; + + public MapConverter(ParameterConverter delegate) { + this.delegate = delegate; + } + + @Override + public Object convert(Object parameter) { + if (parameter instanceof Map) { + Map ret = new HashMap<>(); + Map map = (Map) parameter; + for (Map.Entry entry : map.entrySet()) { + if (delegate == null) { + ret.put(entry.getKey(), entry.getValue()); + } else { + ret.put(entry.getKey(), delegate.convert(entry.getValue())); + } + } + return ret; + } else if (parameter instanceof MultivaluedMap) { + MultivaluedMap ret = new MultivaluedHashMap<>(); + MultivaluedMap multivaluedMap = (MultivaluedMap) parameter; + for (Map.Entry> entry : multivaluedMap.entrySet()) { + List retValues = new ArrayList<>(); + for (Object value : entry.getValue()) { + if (delegate == null) { + retValues.add(value); + } else { + retValues.add(delegate.convert(value)); + } + } + ret.put(entry.getKey(), retValues); + } + return ret; + } + if (parameter == null) { + return Collections.emptyMap(); + } else { + return Collections.emptyMap(); + } + } + + @Override + public void init(ParamConverterProviders deployment, Class rawType, Type genericType, Annotation[] annotations) { + delegate.init(deployment, rawType, genericType, annotations); + } + + @Override + public boolean isForSingleObjectContainer() { + return true; + } + + public static class MapSupplier implements DelegatingParameterConverterSupplier { + private ParameterConverterSupplier delegate; + + public MapSupplier() { + } + + // invoked by reflection for BeanParam in ClassInjectorTransformer + public MapSupplier(ParameterConverterSupplier delegate) { + this.delegate = delegate; + } + + @Override + public String getClassName() { + return MapConverter.class.getName(); + } + + @Override + public ParameterConverter get() { + return delegate == null ? new MapConverter(null) : new MapConverter(delegate.get()); + } + + public ParameterConverterSupplier getDelegate() { + return delegate; + } + + public MapSupplier setDelegate(ParameterConverterSupplier delegate) { + this.delegate = delegate; + return this; + } + } +} diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/ServerHttpRequest.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/ServerHttpRequest.java index 53c2d6ea0c05a..c338a7217214e 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/ServerHttpRequest.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/ServerHttpRequest.java @@ -34,6 +34,8 @@ public interface ServerHttpRequest { String getQueryParam(String name); + Map> getParametersMap(); + List getAllQueryParams(String name); String query(); diff --git a/independent-projects/resteasy-reactive/server/vertx/src/main/java/org/jboss/resteasy/reactive/server/vertx/VertxResteasyReactiveRequestContext.java b/independent-projects/resteasy-reactive/server/vertx/src/main/java/org/jboss/resteasy/reactive/server/vertx/VertxResteasyReactiveRequestContext.java index 51e1c8d9605f8..b2e3d9e3bc6d5 100644 --- a/independent-projects/resteasy-reactive/server/vertx/src/main/java/org/jboss/resteasy/reactive/server/vertx/VertxResteasyReactiveRequestContext.java +++ b/independent-projects/resteasy-reactive/server/vertx/src/main/java/org/jboss/resteasy/reactive/server/vertx/VertxResteasyReactiveRequestContext.java @@ -14,8 +14,10 @@ import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; +import java.util.stream.Collectors; import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MultivaluedHashMap; import jakarta.ws.rs.core.MultivaluedMap; import org.jboss.resteasy.reactive.common.ResteasyReactiveConfig; @@ -215,6 +217,25 @@ public List getAllQueryParams(String name) { return context.queryParam(name); } + @Override + public Map> getParametersMap() { + MultiMap entries = context.request().params(); + final MultivaluedHashMap result = new MultivaluedHashMap<>(); + if (!entries.isEmpty()) { + for (Map.Entry entry : entries) { + result.add(entry.getKey(), entry.getValue()); + } + + } + Map> params = result.entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue // Los valores ya son List + )); + + return params; + } + @Override public String query() { return request.query();