Skip to content

Commit 0dff99c

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

3 files changed

Lines changed: 97 additions & 10 deletions

File tree

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

Lines changed: 49 additions & 6 deletions
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,60 @@ 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 && !(List.class.isAssignableFrom(rawValueType) ||
612+
Set.class.isAssignableFrom(rawValueType))) {
613+
throw new IllegalArgumentException(
614+
"Invalid parameterized map value type: " + rawValueType +
615+
" (expected List or Set)");
616+
}
617+
618+
final BiFunction<AnnotatedValueResolver, ResolverContext, Object> biFunction;
619+
620+
if (Set.class.isAssignableFrom(rawValueType)) {
621+
biFunction = (resolver, ctx) -> ctx.queryParams().stream()
622+
.collect(toImmutableMap(
623+
Entry::getKey,
624+
e -> ImmutableSet.of(e.getValue()),
625+
(existing, replacement) ->
626+
ImmutableSet.<String>builder()
627+
.addAll(existing)
628+
.addAll(replacement)
629+
.build()
630+
));
631+
} else if (List.class.isAssignableFrom(rawValueType) ||
632+
Collection.class.isAssignableFrom(rawValueType) ||
633+
Iterable.class.isAssignableFrom(rawValueType)
634+
) {
635+
biFunction = (resolver, ctx) -> ctx.queryParams().stream()
636+
.collect(toImmutableMap(
637+
Entry::getKey,
638+
e -> ImmutableList.of(e.getValue()),
639+
(existing, replacement) ->
640+
ImmutableList.<String>builder()
641+
.addAll(existing)
642+
.addAll(replacement)
643+
.build()
644+
));
645+
} else {
646+
biFunction = (resolver, ctx) -> ctx.queryParams().stream()
647+
.collect(toImmutableMap(
648+
Entry::getKey,
649+
Entry::getValue,
650+
(existing, replacement) -> replacement
651+
));
652+
}
605653

606654
return new Builder(annotatedElement, type, name)
607655
.annotationType(Param.class)
608656
.typeElement(typeElement)
609657
.description(description)
610658
.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-
)))
659+
.resolver(biFunction)
617660
.build();
618661
}
619662

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

Lines changed: 26 additions & 0 deletions
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

Lines changed: 22 additions & 4 deletions
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 parameterized 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)