Skip to content

Commit

Permalink
Support for populating Map<String, String> and MultiValueMap<String, …
Browse files Browse the repository at this point in the history
…String> with request parameters in @RequestParam
  • Loading branch information
aureamunoz committed Jan 8, 2025
1 parent 9eb22bd commit 22e5638
Show file tree
Hide file tree
Showing 12 changed files with 380 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
Expand Down Expand Up @@ -262,6 +263,17 @@ public List<String> getAllQueryParams(String name) {
return context.queryParam(name);
}

@Override
public Map<String, String> getQueryParams() {
MultiMap entries = context.queryParams();
Map<String, String> queryParams = new HashMap<>(entries.size());
if (!entries.isEmpty()) {
entries.entries().stream()
.map(stringStringEntry -> queryParams.put(stringStringEntry.getKey(), stringStringEntry.getValue()));
}
return queryParams;
}

@Override
public String query() {
return request.getQueryString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -87,17 +90,32 @@ public MapParamConverter(Class<T> 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<Map<String, Object>> list = objectMapper.readValue(value, listType);

Map<String, Object> mergedMap = new LinkedHashMap<>();
for (Map<String, Object> 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);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package io.quarkus.spring.web.resteasy.reactive.deployment;

import java.util.HashMap;
import java.util.Map;

import jakarta.ws.rs.ext.ParamConverter;

public class MapParamConverter implements ParamConverter<Map<String, String>> {

@Override
public Map<String, String> fromString(String value) {
// Parse params to a Map
Map<String, String> map = new HashMap<>();
if (value != null && !value.isEmpty()) {
String[] pairs = value.split("&");
for (String pair : pairs) {
String[] keyValue = pair.split("=");
if (keyValue.length == 2) {
map.put(keyValue[0], keyValue[1]);
}
}
}
return map;
}

@Override
public String toString(Map<String, String> value) {
StringBuilder sb = new StringBuilder();
value.forEach((key, val) -> sb.append(key).append("=").append(val).append("&"));
return sb.length() > 0 ? sb.substring(0, sb.length() - 1) : "";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.quarkus.spring.web.resteasy.reactive.deployment;

import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.Map;

import jakarta.ws.rs.ext.ParamConverter;
import jakarta.ws.rs.ext.ParamConverterProvider;
import jakarta.ws.rs.ext.Provider;

@Provider
public class MapParamConverterProvider implements ParamConverterProvider {
@Override
public <T> ParamConverter<T> getConverter(Class<T> rawType, Type genericType, Annotation[] annotations) {
if (rawType == Map.class) {
return (ParamConverter<T>) new MapParamConverter();
}
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package io.quarkus.spring.web.requestparam;

import java.util.List;
import java.util.Map;
import java.util.Optional;

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<String> 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<String, String> allParams) {
return "Parameters are " + allParams.entrySet();
}

@GetMapping("/api/foos/multivalue")
@ResponseBody
public String getFoosMultiValue(@RequestParam List<String> id) {
return "IDs are " + id;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
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]"));

when().get("/api/foos/multivalue?id=1&id=2")
.then()
.statusCode(200).log().body()
.body(containsString("IDs are [1, 2]"));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
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;
Expand Down Expand Up @@ -1374,6 +1375,14 @@ && isParameterContainerType(paramType.asClassType())) {
handleSortedSetParam(existingConverters, errorLocation, hasRuntimeConverters, builder, elementType,
currentMethodInfo);
}
} else if (pt.name().equals(MAP)) {
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);
Expand Down Expand Up @@ -1534,6 +1543,10 @@ protected void handleSetParam(Map<String, String> existingConverters, String err
PARAM builder, String elementType, MethodInfo currentMethodInfo) {
}

protected void handleMapParam(Map<String, String> existingConverters, String errorLocation, boolean hasRuntimeConverters,
PARAM builder, String elementType, MethodInfo currentMethodInfo) {
}

protected void handleListParam(Map<String, String> existingConverters, String errorLocation, boolean hasRuntimeConverters,
PARAM builder, String elementType, MethodInfo currentMethodInfo) {
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -532,6 +533,14 @@ protected void handleSetParam(Map<String, String> existingConverters, String err
builder.setConverter(new SetConverter.SetSupplier(converter));
}

@Override
protected void handleMapParam(Map<String, String> 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<String, String> existingConverters, String errorLocation, boolean hasRuntimeConverters,
ServerIndexedParameter builder, String elementType, MethodInfo currentMethodInfo) {
Expand Down
Loading

0 comments on commit 22e5638

Please sign in to comment.