Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
import com.google.common.base.MoreObjects;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.primitives.Primitives;

Expand Down Expand Up @@ -602,18 +603,60 @@ private static AnnotatedValueResolver ofQueryParamMap(String name,
AnnotatedElement annotatedElement,
AnnotatedElement typeElement, Class<?> type,
DescriptionInfo description) {
final Type valueType = ((ParameterizedType) ((Parameter) typeElement).getParameterizedType())
.getActualTypeArguments()[1];
final Class<?> rawValueType = ClassUtil.typeToClass(valueType);
assert rawValueType != null;

if (valueType instanceof ParameterizedType && !(List.class.isAssignableFrom(rawValueType) ||
Set.class.isAssignableFrom(rawValueType))) {
throw new IllegalArgumentException(
"Invalid parameterized map value type: " + rawValueType +
" (expected List or Set)");
}

final BiFunction<AnnotatedValueResolver, ResolverContext, Object> biFunction;

if (Set.class.isAssignableFrom(rawValueType)) {
biFunction = (resolver, ctx) -> ctx.queryParams().stream()
.collect(toImmutableMap(
Entry::getKey,
e -> ImmutableSet.of(e.getValue()),
(existing, replacement) ->
ImmutableSet.<String>builder()
Comment thread
kwondh5217 marked this conversation as resolved.
.addAll(existing)
.addAll(replacement)
.build()
));
} else if (List.class.isAssignableFrom(rawValueType) ||
Collection.class.isAssignableFrom(rawValueType) ||
Iterable.class.isAssignableFrom(rawValueType)
) {
biFunction = (resolver, ctx) -> ctx.queryParams().stream()
.collect(toImmutableMap(
Entry::getKey,
e -> ImmutableList.of(e.getValue()),
(existing, replacement) ->
ImmutableList.<String>builder()
.addAll(existing)
.addAll(replacement)
.build()
));
} else {
biFunction = (resolver, ctx) -> ctx.queryParams().stream()
.collect(toImmutableMap(
Entry::getKey,
Entry::getValue,
(existing, replacement) -> replacement
));
}

return new Builder(annotatedElement, type, name)
.annotationType(Param.class)
.typeElement(typeElement)
.description(description)
.aggregation(AggregationStrategy.FOR_FORM_DATA)
.resolver((resolver, ctx) -> ctx.queryParams().stream()
.collect(toImmutableMap(
Entry::getKey,
Entry::getValue,
(existing, replacement) -> replacement
)))
.resolver(biFunction)
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,22 @@ public String map(RequestContext ctx, @Param Map<String, Object> map) {
.map(entry -> entry.getKey() + '=' + entry.getValue())
.collect(Collectors.joining(", "));
}

@Get("/param/listMap")
public String listMap(RequestContext ctx, @Param Map<String, List<Object>> map) {
validateContext(ctx);
return map.isEmpty() ? "empty" : map.entrySet().stream()
.map(entry -> entry.getKey() + '=' + entry.getValue())
.collect(Collectors.joining(", "));
}

@Get("/param/setMap")
public String setMap(RequestContext ctx, @Param Map<String, Set<Object>> map) {
validateContext(ctx);
return map.isEmpty() ? "empty" : map.entrySet().stream()
.map(entry -> entry.getKey() + '=' + entry.getValue())
.collect(Collectors.joining(", "));
}
}

@ResponseConverter(UnformattedStringConverterFunction.class)
Expand Down Expand Up @@ -1080,6 +1096,16 @@ void testParam() throws Exception {
testBody(hc, get("/7/param/map?key1=value1&key2=value2"),
"key1=value1, key2=value2");
testBody(hc, get("/7/param/map"), "empty");

// Case all query parameters test multi value map of List
testBody(hc, get("/7/param/listMap?key1=value1&key1=value2&key2=value1&key2=value2"),
"key1=[value1, value2], key2=[value1, value2]");
testBody(hc, get("/7/param/listMap"), "empty");

// Case all query parameters test multi value map of Set
testBody(hc, get("/7/param/setMap?key1=value1&key1=value1&key2=value2&key2=value2"),
"key1=[value1], key2=[value2]");
testBody(hc, get("/7/param/setMap"), "empty");
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,14 @@ class AnnotatedValueResolverTest {
"value3",
"value2");

static final Set<String> queryParamMaps = ImmutableSet.of("queryParamMap",
"queryParamListMap",
"queryParamSetMap");

static final ResolverContext resolverContext;
static final ServiceRequestContext context;
static final HttpRequest request;
static final RequestHeaders originalHeaders;
static final String QUERY_PARAM_MAP = "queryParamMap";
static Map<String, AttributeKey<?>> successExpectAttrKeys;
static Map<String, AttributeKey<?>> failExpectAttrKeys;

Expand Down Expand Up @@ -182,6 +185,15 @@ void ofMethods() {
// Ignore this exception because MixedBean class has not annotated method.
}
});

// Validate that invalid multi-value map parameter types trigger an exception
getAllMethods(InvalidMultiValueMapService.class,
method -> !Modifier.isPrivate(method.getModifiers())).forEach(
method -> assertThatThrownBy(() -> AnnotatedValueResolver.ofServiceMethod(
method, pathParams, objectResolvers, false, noopDependencyInjector, null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Invalid parameterized map value type")
);
}

@Test
Expand Down Expand Up @@ -364,7 +376,7 @@ private static void testResolver(AnnotatedValueResolver resolver) {
}
}
} else {
if (QUERY_PARAM_MAP.equals(resolver.httpElementName())) {
if (queryParamMaps.contains(resolver.httpElementName())) {
assertThat(resolver.defaultValue()).isNull();
} else {
assertThat(resolver.defaultValue()).isNotNull();
Expand All @@ -376,7 +388,7 @@ private static void testResolver(AnnotatedValueResolver resolver) {
.isEqualTo(resolver.elementType());
} else if (resolver.shouldWrapValueAsOptional()) {
assertThat(value).isEqualTo(Optional.of(resolver.defaultValue()));
} else if (QUERY_PARAM_MAP.equals(resolver.httpElementName())) {
} else if (queryParamMaps.contains(resolver.httpElementName())) {
assertThat(value).isNotNull();
assertThat(value).isInstanceOf(Map.class);
assertThat((Map<?, ?>) value).size()
Expand Down Expand Up @@ -459,6 +471,8 @@ void method1(@Param String var1,
@Param @Default List<String> emptyParam3,
@Param @Default List<Integer> emptyParam4,
@Param Map<String, Object> queryParamMap,
@Param Map<String, List<Object>> queryParamListMap,
@Param Map<String, Set<Object>> queryParamSetMap,
@Header List<String> header1,
@Header("header1") Optional<List<ValueEnum>> optionalHeader1,
@Header String header2,
Expand Down Expand Up @@ -519,7 +533,7 @@ void attributeTest(
Queue<String> successQueueToQueue,
@Attribute("failCastListToSet")
Set<String> failCastListToSet
) { }
) {}

void time(@Param @Default("PT20.345S") Duration duration,
@Param @Default("2007-12-03T10:15:30.00Z") Instant instant,
Expand All @@ -534,6 +548,10 @@ void time(@Param @Default("PT20.345S") Duration duration,
@Param @Default("+01:00:00") ZoneOffset zoneOffset) {}
}

static class InvalidMultiValueMapService {
void invalidParamWithMapOfMap(@Param Map<String, Map<String, String>> param) {}
}

private static Map<String, AttributeKey<?>> injectFailCaseOfAttrKeyToServiceContextForAttributeTest() {
final ServiceRequestContext ctx = resolverContext.context();
final Map<String, AttributeKey<?>> expectFailAttrs = new HashMap<>();
Expand Down