Skip to content

Commit efa40e2

Browse files
committed
Add support for multi-value query parameters
Signed-off-by: Daeho Kwon <trewq231@naver.com>
1 parent bec5f6a commit efa40e2

File tree

3 files changed

+99
-10
lines changed

3 files changed

+99
-10
lines changed

core/src/main/java/com/linecorp/armeria/internal/server/annotation/AnnotatedValueResolver.java

+51-6
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
import com.google.common.base.MoreObjects;
6565
import com.google.common.base.Splitter;
6666
import com.google.common.collect.ImmutableList;
67+
import com.google.common.collect.ImmutableSet;
6768
import com.google.common.collect.Iterables;
6869
import com.google.common.primitives.Primitives;
6970

@@ -602,18 +603,62 @@ private static AnnotatedValueResolver ofQueryParamMap(String name,
602603
AnnotatedElement annotatedElement,
603604
AnnotatedElement typeElement, Class<?> type,
604605
DescriptionInfo description) {
606+
final Type valueType = ((ParameterizedType) ((Parameter) typeElement).getParameterizedType())
607+
.getActualTypeArguments()[1];
608+
final Class<?> rawValueType = ClassUtil.typeToClass(valueType);
609+
assert rawValueType != null;
610+
611+
if (valueType instanceof ParameterizedType && !(rawValueType == Iterable.class ||
612+
rawValueType == List.class ||
613+
rawValueType == Collection.class ||
614+
rawValueType == Set.class)) {
615+
throw new IllegalArgumentException(
616+
"Invalid Map value type: " + rawValueType
617+
+ " (expected Iterable, List, Collection or Set)");
618+
}
619+
620+
final BiFunction<AnnotatedValueResolver, ResolverContext, Object> biFunction;
621+
622+
if (List.class.isAssignableFrom(rawValueType) ||
623+
Collection.class.isAssignableFrom(rawValueType) ||
624+
Iterable.class.isAssignableFrom(rawValueType)
625+
) {
626+
biFunction = (resolver, ctx) -> ctx.queryParams().stream()
627+
.collect(toImmutableMap(
628+
Entry::getKey,
629+
e -> ImmutableList.of(e.getValue()),
630+
(existing, replacement) ->
631+
ImmutableList.<String>builder()
632+
.addAll(existing)
633+
.addAll(replacement)
634+
.build()
635+
));
636+
} else if (Set.class.isAssignableFrom(rawValueType)) {
637+
biFunction = (resolver, ctx) -> ctx.queryParams().stream()
638+
.collect(toImmutableMap(
639+
Entry::getKey,
640+
e -> ImmutableSet.of(e.getValue()),
641+
(existing, replacement) ->
642+
ImmutableSet.<String>builder()
643+
.addAll(existing)
644+
.addAll(replacement)
645+
.build()
646+
));
647+
} else {
648+
biFunction = (resolver, ctx) -> ctx.queryParams().stream()
649+
.collect(toImmutableMap(
650+
Entry::getKey,
651+
Entry::getValue,
652+
(existing, replacement) -> replacement
653+
));
654+
}
605655

606656
return new Builder(annotatedElement, type, name)
607657
.annotationType(Param.class)
608658
.typeElement(typeElement)
609659
.description(description)
610660
.aggregation(AggregationStrategy.FOR_FORM_DATA)
611-
.resolver((resolver, ctx) -> ctx.queryParams().stream()
612-
.collect(toImmutableMap(
613-
Entry::getKey,
614-
Entry::getValue,
615-
(existing, replacement) -> replacement
616-
)))
661+
.resolver(biFunction)
617662
.build();
618663
}
619664

core/src/test/java/com/linecorp/armeria/internal/server/annotation/AnnotatedServiceTest.java

+26
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,22 @@ public String map(RequestContext ctx, @Param Map<String, Object> map) {
639639
.map(entry -> entry.getKey() + '=' + entry.getValue())
640640
.collect(Collectors.joining(", "));
641641
}
642+
643+
@Get("/param/listMap")
644+
public String listMap(RequestContext ctx, @Param Map<String, List<Object>> map) {
645+
validateContext(ctx);
646+
return map.isEmpty() ? "empty" : map.entrySet().stream()
647+
.map(entry -> entry.getKey() + '=' + entry.getValue())
648+
.collect(Collectors.joining(", "));
649+
}
650+
651+
@Get("/param/setMap")
652+
public String setMap(RequestContext ctx, @Param Map<String, Set<Object>> map) {
653+
validateContext(ctx);
654+
return map.isEmpty() ? "empty" : map.entrySet().stream()
655+
.map(entry -> entry.getKey() + '=' + entry.getValue())
656+
.collect(Collectors.joining(", "));
657+
}
642658
}
643659

644660
@ResponseConverter(UnformattedStringConverterFunction.class)
@@ -1080,6 +1096,16 @@ void testParam() throws Exception {
10801096
testBody(hc, get("/7/param/map?key1=value1&key2=value2"),
10811097
"key1=value1, key2=value2");
10821098
testBody(hc, get("/7/param/map"), "empty");
1099+
1100+
// Case all query parameters test multi value map of List
1101+
testBody(hc, get("/7/param/listMap?key1=value1&key1=value2&key2=value1&key2=value2"),
1102+
"key1=[value1, value2], key2=[value1, value2]");
1103+
testBody(hc, get("/7/param/listMap"), "empty");
1104+
1105+
// Case all query parameters test multi value map of Set
1106+
testBody(hc, get("/7/param/setMap?key1=value1&key1=value1&key2=value2&key2=value2"),
1107+
"key1=[value1], key2=[value2]");
1108+
testBody(hc, get("/7/param/setMap"), "empty");
10831109
}
10841110
}
10851111

core/src/test/java/com/linecorp/armeria/internal/server/annotation/AnnotatedValueResolverTest.java

+22-4
Original file line numberDiff line numberDiff line change
@@ -108,11 +108,14 @@ class AnnotatedValueResolverTest {
108108
"value3",
109109
"value2");
110110

111+
static final Set<String> queryParamMaps = ImmutableSet.of("queryParamMap",
112+
"queryParamListMap",
113+
"queryParamSetMap");
114+
111115
static final ResolverContext resolverContext;
112116
static final ServiceRequestContext context;
113117
static final HttpRequest request;
114118
static final RequestHeaders originalHeaders;
115-
static final String QUERY_PARAM_MAP = "queryParamMap";
116119
static Map<String, AttributeKey<?>> successExpectAttrKeys;
117120
static Map<String, AttributeKey<?>> failExpectAttrKeys;
118121

@@ -182,6 +185,15 @@ void ofMethods() {
182185
// Ignore this exception because MixedBean class has not annotated method.
183186
}
184187
});
188+
189+
// Validate that invalid multi-value map parameter types trigger an exception
190+
getAllMethods(InvalidMultiValueMapService.class,
191+
method -> !Modifier.isPrivate(method.getModifiers())).forEach(
192+
method -> assertThatThrownBy(() -> AnnotatedValueResolver.ofServiceMethod(
193+
method, pathParams, objectResolvers, false, noopDependencyInjector, null))
194+
.isInstanceOf(IllegalArgumentException.class)
195+
.hasMessageContaining("Invalid Map value type")
196+
);
185197
}
186198

187199
@Test
@@ -364,7 +376,7 @@ private static void testResolver(AnnotatedValueResolver resolver) {
364376
}
365377
}
366378
} else {
367-
if (QUERY_PARAM_MAP.equals(resolver.httpElementName())) {
379+
if (queryParamMaps.contains(resolver.httpElementName())) {
368380
assertThat(resolver.defaultValue()).isNull();
369381
} else {
370382
assertThat(resolver.defaultValue()).isNotNull();
@@ -376,7 +388,7 @@ private static void testResolver(AnnotatedValueResolver resolver) {
376388
.isEqualTo(resolver.elementType());
377389
} else if (resolver.shouldWrapValueAsOptional()) {
378390
assertThat(value).isEqualTo(Optional.of(resolver.defaultValue()));
379-
} else if (QUERY_PARAM_MAP.equals(resolver.httpElementName())) {
391+
} else if (queryParamMaps.contains(resolver.httpElementName())) {
380392
assertThat(value).isNotNull();
381393
assertThat(value).isInstanceOf(Map.class);
382394
assertThat((Map<?, ?>) value).size()
@@ -459,6 +471,8 @@ void method1(@Param String var1,
459471
@Param @Default List<String> emptyParam3,
460472
@Param @Default List<Integer> emptyParam4,
461473
@Param Map<String, Object> queryParamMap,
474+
@Param Map<String, List<Object>> queryParamListMap,
475+
@Param Map<String, Set<Object>> queryParamSetMap,
462476
@Header List<String> header1,
463477
@Header("header1") Optional<List<ValueEnum>> optionalHeader1,
464478
@Header String header2,
@@ -519,7 +533,7 @@ void attributeTest(
519533
Queue<String> successQueueToQueue,
520534
@Attribute("failCastListToSet")
521535
Set<String> failCastListToSet
522-
) { }
536+
) {}
523537

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

551+
static class InvalidMultiValueMapService {
552+
void invalidParamWithMapOfMap(@Param Map<String, Map<String, String>> param) {}
553+
}
554+
537555
private static Map<String, AttributeKey<?>> injectFailCaseOfAttrKeyToServiceContextForAttributeTest() {
538556
final ServiceRequestContext ctx = resolverContext.context();
539557
final Map<String, AttributeKey<?>> expectFailAttrs = new HashMap<>();

0 commit comments

Comments
 (0)