diff --git a/spring-web/src/test/java/org/springframework/web/method/MvcAnnotationPredicates.java b/spring-web/src/test/java/org/springframework/web/method/MvcAnnotationPredicates.java index 9429bcb0ffb..7c875d70b4f 100644 --- a/spring-web/src/test/java/org/springframework/web/method/MvcAnnotationPredicates.java +++ b/spring-web/src/test/java/org/springframework/web/method/MvcAnnotationPredicates.java @@ -22,6 +22,7 @@ import java.util.function.Predicate; import org.springframework.core.MethodParameter; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.MatrixVariable; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -61,6 +62,9 @@ public class MvcAnnotationPredicates { return new RequestPartPredicate(); } + public static MatrixVariablePredicate matrixAttribute() { + return new MatrixVariablePredicate(); + } // Method predicates @@ -305,4 +309,40 @@ public class MvcAnnotationPredicates { } } + public static class MatrixVariablePredicate implements Predicate { + + private String name; + + private String pathVar; + + + public MatrixVariablePredicate name(String name) { + this.name = name; + return this; + } + + public MatrixVariablePredicate noName() { + this.name = ""; + return this; + } + + public MatrixVariablePredicate pathVar(String name) { + this.pathVar = name; + return this; + } + + public MatrixVariablePredicate noPathVar() { + this.pathVar = ValueConstants.DEFAULT_NONE; + return this; + } + + @Override + public boolean test(MethodParameter parameter) { + MatrixVariable annotation = parameter.getParameterAnnotation(MatrixVariable.class); + return annotation != null && + (this.name == null || this.name.equalsIgnoreCase(annotation.name())) && + (this.pathVar == null || this.pathVar.equalsIgnoreCase(annotation.pathVar())); + } + } + } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractNamedValueSyncArgumentResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractNamedValueSyncArgumentResolver.java index dca688438c0..5251be88bd5 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractNamedValueSyncArgumentResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractNamedValueSyncArgumentResolver.java @@ -38,13 +38,16 @@ import org.springframework.web.server.ServerWebExchange; public abstract class AbstractNamedValueSyncArgumentResolver extends AbstractNamedValueArgumentResolver implements SyncHandlerMethodArgumentResolver { + /** * @param factory a bean factory to use for resolving ${...} * placeholder and #{...} SpEL expressions in default values; * or {@code null} if default values are not expected to have expressions * @param registry for checking reactive type wrappers */ - protected AbstractNamedValueSyncArgumentResolver(@Nullable ConfigurableBeanFactory factory, ReactiveAdapterRegistry registry) { + protected AbstractNamedValueSyncArgumentResolver(@Nullable ConfigurableBeanFactory factory, + ReactiveAdapterRegistry registry) { + super(factory, registry); } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ControllerMethodResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ControllerMethodResolver.java index d030bcb7ef6..91d3031c300 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ControllerMethodResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ControllerMethodResolver.java @@ -144,6 +144,8 @@ class ControllerMethodResolver { registrar.add(new RequestParamMapMethodArgumentResolver(reactiveRegistry)); registrar.add(new PathVariableMethodArgumentResolver(beanFactory, reactiveRegistry)); registrar.add(new PathVariableMapMethodArgumentResolver(reactiveRegistry)); + registrar.add(new MatrixVariableMethodArgumentResolver(beanFactory, reactiveRegistry)); + registrar.add(new MatrixVariableMapMethodArgumentResolver(reactiveRegistry)); registrar.addIfRequestBody(readers -> new RequestBodyArgumentResolver(readers, reactiveRegistry)); registrar.addIfRequestBody(readers -> new RequestPartMethodArgumentResolver(readers, reactiveRegistry)); registrar.addIfModelAttribute(() -> new ModelAttributeMethodArgumentResolver(reactiveRegistry, false)); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/MatrixVariableMapMethodArgumentResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/MatrixVariableMapMethodArgumentResolver.java new file mode 100644 index 00000000000..66c68ca9ed1 --- /dev/null +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/MatrixVariableMapMethodArgumentResolver.java @@ -0,0 +1,116 @@ +/* + * Copyright 2002-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.web.reactive.result.method.annotation; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.springframework.core.MethodParameter; +import org.springframework.core.ReactiveAdapterRegistry; +import org.springframework.core.ResolvableType; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.MatrixVariable; +import org.springframework.web.bind.annotation.ValueConstants; +import org.springframework.web.reactive.BindingContext; +import org.springframework.web.reactive.HandlerMapping; +import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolverSupport; +import org.springframework.web.reactive.result.method.SyncHandlerMethodArgumentResolver; +import org.springframework.web.server.ServerWebExchange; + +/** + * Resolves arguments of type {@link Map} annotated with {@link MatrixVariable + * @MatrixVariable} where the annotation does not specify a name. In other words + * the purpose of this resolver is to provide access to multiple matrix + * variables, either all or associted with a specific path variable. + * + *

When a name is specified, an argument of type Map is considered to be an + * single attribute with a Map value, and is resolved by + * {@link MatrixVariableMethodArgumentResolver} instead. + * + * @author Rossen Stoyanchev + * @since 5.0.1 + * @see MatrixVariableMethodArgumentResolver + */ +public class MatrixVariableMapMethodArgumentResolver extends HandlerMethodArgumentResolverSupport + implements SyncHandlerMethodArgumentResolver { + + + public MatrixVariableMapMethodArgumentResolver(ReactiveAdapterRegistry registry) { + super(registry); + } + + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return checkAnnotatedParamNoReactiveWrapper(parameter, MatrixVariable.class, + (annot, type) -> (Map.class.isAssignableFrom(type) && !StringUtils.hasText(annot.name()))); + } + + @Nullable + @Override + public Object resolveArgumentValue(MethodParameter parameter, BindingContext bindingContext, + ServerWebExchange exchange) { + + Map> matrixVariables = + exchange.getAttribute(HandlerMapping.MATRIX_VARIABLES_ATTRIBUTE); + + if (CollectionUtils.isEmpty(matrixVariables)) { + return Collections.emptyMap(); + } + + MultiValueMap map = new LinkedMultiValueMap<>(); + MatrixVariable annotation = parameter.getParameterAnnotation(MatrixVariable.class); + Assert.state(annotation != null, "No MatrixVariable annotation"); + String pathVariable = annotation.pathVar(); + + if (!pathVariable.equals(ValueConstants.DEFAULT_NONE)) { + MultiValueMap mapForPathVariable = matrixVariables.get(pathVariable); + if (mapForPathVariable == null) { + return Collections.emptyMap(); + } + map.putAll(mapForPathVariable); + } + else { + for (MultiValueMap vars : matrixVariables.values()) { + for (String name : vars.keySet()) { + for (String value : vars.get(name)) { + map.add(name, value); + } + } + } + } + + return (isSingleValueMap(parameter) ? map.toSingleValueMap() : map); + } + + private boolean isSingleValueMap(MethodParameter parameter) { + if (!MultiValueMap.class.isAssignableFrom(parameter.getParameterType())) { + ResolvableType[] genericTypes = ResolvableType.forMethodParameter(parameter).getGenerics(); + if (genericTypes.length == 2) { + Class declaredClass = genericTypes[1].getRawClass(); + return (declaredClass == null || !List.class.isAssignableFrom(declaredClass)); + } + } + return false; + } + +} \ No newline at end of file diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/MatrixVariableMethodArgumentResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/MatrixVariableMethodArgumentResolver.java new file mode 100644 index 00000000000..7310fb23c23 --- /dev/null +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/MatrixVariableMethodArgumentResolver.java @@ -0,0 +1,136 @@ +/* + * Copyright 2002-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.web.reactive.result.method.annotation; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.core.MethodParameter; +import org.springframework.core.ReactiveAdapterRegistry; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.MatrixVariable; +import org.springframework.web.bind.annotation.ValueConstants; +import org.springframework.web.reactive.HandlerMapping; +import org.springframework.web.server.ServerErrorException; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.ServerWebInputException; + +/** + * Resolves arguments annotated with {@link MatrixVariable @MatrixVariable}. + * + *

If the method parameter is of type {@link Map} it will by resolved by + * {@link MatrixVariableMapMethodArgumentResolver} instead unless the annotation + * specifies a name in which case it is considered to be a single attribute of + * type map (vs multiple attributes collected in a map). + * + * @author Rossen Stoyanchev + * @since 5.0.1 + * @see MatrixVariableMapMethodArgumentResolver + */ +public class MatrixVariableMethodArgumentResolver extends AbstractNamedValueSyncArgumentResolver { + + + protected MatrixVariableMethodArgumentResolver(@Nullable ConfigurableBeanFactory factory, + ReactiveAdapterRegistry registry) { + + super(factory, registry); + } + + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return checkAnnotatedParamNoReactiveWrapper(parameter, MatrixVariable.class, + (annot, type) -> !Map.class.isAssignableFrom(type) || StringUtils.hasText(annot.name())); + } + + + @Override + protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { + MatrixVariable ann = parameter.getParameterAnnotation(MatrixVariable.class); + Assert.state(ann != null, "No MatrixVariable annotation"); + return new MatrixVariableNamedValueInfo(ann); + } + + @Nullable + @Override + protected Object resolveNamedValue(String name, MethodParameter param, ServerWebExchange exchange) { + + Map> pathParameters = + exchange.getAttribute(HandlerMapping.MATRIX_VARIABLES_ATTRIBUTE); + + if (CollectionUtils.isEmpty(pathParameters)) { + return null; + } + + MatrixVariable ann = param.getParameterAnnotation(MatrixVariable.class); + Assert.state(ann != null, "No MatrixVariable annotation"); + String pathVar = ann.pathVar(); + List paramValues = null; + + if (!pathVar.equals(ValueConstants.DEFAULT_NONE)) { + if (pathParameters.containsKey(pathVar)) { + paramValues = pathParameters.get(pathVar).get(name); + } + } + else { + boolean found = false; + paramValues = new ArrayList<>(); + for (MultiValueMap params : pathParameters.values()) { + if (params.containsKey(name)) { + if (found) { + String paramType = param.getNestedParameterType().getName(); + throw new ServerErrorException( + "Found more than one match for URI path parameter '" + name + + "' for parameter type [" + paramType + "]. Use 'pathVar' attribute to disambiguate."); + } + paramValues.addAll(params.get(name)); + found = true; + } + } + } + + if (CollectionUtils.isEmpty(paramValues)) { + return null; + } + else if (paramValues.size() == 1) { + return paramValues.get(0); + } + else { + return paramValues; + } + } + + @Override + protected void handleMissingValue(String name, MethodParameter parameter) throws ServerWebInputException { + throw new ServerWebInputException("Missing matrix variable '" + name + + "' for method parameter of type " + parameter.getNestedParameterType().getSimpleName()); + } + + + private static class MatrixVariableNamedValueInfo extends NamedValueInfo { + + private MatrixVariableNamedValueInfo(MatrixVariable annotation) { + super(annotation.name(), annotation.required(), annotation.defaultValue()); + } + } + +} diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ControllerInputIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ControllerInputIntegrationTests.java new file mode 100644 index 00000000000..c5ad949f8d3 --- /dev/null +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ControllerInputIntegrationTests.java @@ -0,0 +1,99 @@ +/* + * Copyright 2002-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.web.reactive.result.method.annotation; + +import org.junit.Test; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.MatrixVariable; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.reactive.config.EnableWebFlux; + +import static org.junit.Assert.assertEquals; + +/** + * {@code @RequestMapping} integration focusing on controller method parameters. + * Also see: + *

+ * @author Rossen Stoyanchev + */ +public class ControllerInputIntegrationTests extends AbstractRequestMappingIntegrationTests { + + + @Override + protected ApplicationContext initApplicationContext() { + AnnotationConfigApplicationContext wac = new AnnotationConfigApplicationContext(); + wac.register(WebConfig.class, TestRestController.class); + wac.refresh(); + return wac; + } + + + @Test + public void handleWithParam() throws Exception { + String expected = "Hello George!"; + assertEquals(expected, performGet("/param?name=George", new HttpHeaders(), String.class).getBody()); + } + + @Test // SPR-15140 + public void handleWithEncodedParam() throws Exception { + String expected = "Hello + \u00e0!"; + assertEquals(expected, performGet("/param?name=%20%2B+%C3%A0", new HttpHeaders(), String.class).getBody()); + } + + @Test + public void matrixVariable() throws Exception { + String expected = "p=11, q2=22, q4=44"; + String url = "/first;p=11/second;q=22/third-fourth;q=44"; + assertEquals(expected, performGet(url, new HttpHeaders(), String.class).getBody()); + } + + + @Configuration + @EnableWebFlux + static class WebConfig { + } + + @RestController + @SuppressWarnings("unused") + private static class TestRestController { + + @GetMapping("/param") + public Publisher param(@RequestParam String name) { + return Flux.just("Hello ", name, "!"); + } + + @GetMapping("/{one}/{two}/{three}-{four}") + public String matrixVar( + @MatrixVariable int p, + @MatrixVariable(name = "q", pathVar = "two") int q2, + @MatrixVariable(name = "q", pathVar = "four") int q4) { + + return "p=" + p + ", q2=" + q2 + ", q4=" + q4; + } + } + +} diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MatrixVariablesMapMethodArgumentResolverTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MatrixVariablesMapMethodArgumentResolverTests.java new file mode 100644 index 00000000000..a94c461806d --- /dev/null +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MatrixVariablesMapMethodArgumentResolverTests.java @@ -0,0 +1,194 @@ +/* + * Copyright 2002-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.web.reactive.result.method.annotation; + +import java.time.Duration; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.core.MethodParameter; +import org.springframework.core.ReactiveAdapterRegistry; +import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest; +import org.springframework.mock.web.test.server.MockServerWebExchange; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.annotation.MatrixVariable; +import org.springframework.web.method.ResolvableMethod; +import org.springframework.web.reactive.BindingContext; +import org.springframework.web.reactive.HandlerMapping; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.springframework.web.method.MvcAnnotationPredicates.matrixAttribute; + +/** + * Unit tests for {@link MatrixVariableMapMethodArgumentResolver}. + * @author Rossen Stoyanchev + */ +public class MatrixVariablesMapMethodArgumentResolverTests { + + private final MatrixVariableMapMethodArgumentResolver resolver = + new MatrixVariableMapMethodArgumentResolver(new ReactiveAdapterRegistry()); + + private final MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/")); + + private final ResolvableMethod testMethod = ResolvableMethod.on(this.getClass()).named("handle").build(); + + + @Before + public void setUp() throws Exception { + this.exchange.getAttributes().put(HandlerMapping.MATRIX_VARIABLES_ATTRIBUTE, new LinkedHashMap<>()); + } + + + @Test + public void supportsParameter() { + + assertFalse(this.resolver.supportsParameter(this.testMethod.arg(String.class))); + + assertTrue(this.resolver.supportsParameter(this.testMethod.annot(matrixAttribute().noName()) + .arg(Map.class, String.class, String.class))); + + assertTrue(this.resolver.supportsParameter(this.testMethod.annot(matrixAttribute().noPathVar()) + .arg(MultiValueMap.class, String.class, String.class))); + + assertTrue(this.resolver.supportsParameter(this.testMethod.annot(matrixAttribute().pathVar("cars")) + .arg(MultiValueMap.class, String.class, String.class))); + + assertFalse(this.resolver.supportsParameter(this.testMethod.annot(matrixAttribute().name("name")) + .arg(Map.class, String.class, String.class))); + } + + @Test + public void resolveArgument() throws Exception { + MultiValueMap params = getMatrixVariables("cars"); + params.add("colors", "red"); + params.add("colors", "green"); + params.add("colors", "blue"); + params.add("year", "2012"); + + MethodParameter param = this.testMethod.annot(matrixAttribute().noName()) + .arg(Map.class, String.class, String.class); + + @SuppressWarnings("unchecked") + Map map = + (Map) this.resolver.resolveArgument( + param, new BindingContext(), this.exchange).block(Duration.ZERO); + + assertNotNull(map); + assertEquals("red", map.get("colors")); + + param = this.testMethod + .annot(matrixAttribute().noPathVar()) + .arg(MultiValueMap.class, String.class, String.class); + + @SuppressWarnings("unchecked") + MultiValueMap multivalueMap = + (MultiValueMap) this.resolver.resolveArgument( + param, new BindingContext(), this.exchange).block(Duration.ZERO); + + assertEquals(Arrays.asList("red", "green", "blue"), multivalueMap.get("colors")); + } + + @Test + public void resolveArgumentPathVariable() throws Exception { + MultiValueMap params1 = getMatrixVariables("cars"); + params1.add("colors", "red"); + params1.add("colors", "purple"); + + MultiValueMap params2 = getMatrixVariables("planes"); + params2.add("colors", "yellow"); + params2.add("colors", "orange"); + + MethodParameter param = this.testMethod.annot(matrixAttribute().pathVar("cars")) + .arg(MultiValueMap.class, String.class, String.class); + + @SuppressWarnings("unchecked") + Map mapForPathVar = (Map) + this.resolver.resolveArgument(param, new BindingContext(), this.exchange).block(Duration.ZERO); + + assertNotNull(mapForPathVar); + assertEquals(Arrays.asList("red", "purple"), mapForPathVar.get("colors")); + + param = this.testMethod.annot(matrixAttribute().noName()).arg(Map.class, String.class, String.class); + + @SuppressWarnings("unchecked") + Map mapAll = (Map) + this.resolver.resolveArgument(param, new BindingContext(), this.exchange).block(Duration.ZERO); + + assertNotNull(mapAll); + assertEquals("red", mapAll.get("colors")); + } + + @Test + public void resolveArgumentNoParams() throws Exception { + + MethodParameter param = this.testMethod.annot(matrixAttribute().noName()) + .arg(Map.class, String.class, String.class); + + @SuppressWarnings("unchecked") + Map map = (Map) + this.resolver.resolveArgument(param, new BindingContext(), this.exchange).block(Duration.ZERO); + + assertEquals(Collections.emptyMap(), map); + } + + @Test + public void resolveArgumentNoMatch() throws Exception { + MultiValueMap params2 = getMatrixVariables("planes"); + params2.add("colors", "yellow"); + params2.add("colors", "orange"); + + MethodParameter param = this.testMethod.annot(matrixAttribute().pathVar("cars")) + .arg(MultiValueMap.class, String.class, String.class); + + @SuppressWarnings("unchecked") + Map map = (Map) this.resolver.resolveArgument( + param, new BindingContext(), this.exchange).block(Duration.ZERO); + + assertEquals(Collections.emptyMap(), map); + } + + + @SuppressWarnings("unchecked") + private MultiValueMap getMatrixVariables(String pathVarName) { + Map> matrixVariables = + this.exchange.getAttribute(HandlerMapping.MATRIX_VARIABLES_ATTRIBUTE); + + MultiValueMap params = new LinkedMultiValueMap<>(); + matrixVariables.put(pathVarName, params); + + return params; + } + + + @SuppressWarnings("unused") + void handle( + String stringArg, + @MatrixVariable Map map, + @MatrixVariable MultiValueMap multivalueMap, + @MatrixVariable(pathVar="cars") MultiValueMap mapForPathVar, + @MatrixVariable("name") Map mapWithName) { + } + +} diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MatrixVariablesMethodArgumentResolverTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MatrixVariablesMethodArgumentResolverTests.java new file mode 100644 index 00000000000..ae7131fd5b5 --- /dev/null +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MatrixVariablesMethodArgumentResolverTests.java @@ -0,0 +1,149 @@ +/* + * Copyright 2002-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.web.reactive.result.method.annotation; + +import java.time.Duration; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.core.MethodParameter; +import org.springframework.core.ReactiveAdapterRegistry; +import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest; +import org.springframework.mock.web.test.server.MockServerWebExchange; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.annotation.MatrixVariable; +import org.springframework.web.method.ResolvableMethod; +import org.springframework.web.reactive.BindingContext; +import org.springframework.web.reactive.HandlerMapping; +import org.springframework.web.server.ServerErrorException; +import org.springframework.web.server.ServerWebInputException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.springframework.web.method.MvcAnnotationPredicates.matrixAttribute; + +/** + * Unit tests for {@link MatrixVariableMethodArgumentResolver}. + * @author Rossen Stoyanchev + */ +public class MatrixVariablesMethodArgumentResolverTests { + + private MatrixVariableMethodArgumentResolver resolver = + new MatrixVariableMethodArgumentResolver(null, new ReactiveAdapterRegistry()); + + private final MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/")); + + private ResolvableMethod testMethod = ResolvableMethod.on(this.getClass()).named("handle").build(); + + + @Before + public void setUp() throws Exception { + this.exchange.getAttributes().put(HandlerMapping.MATRIX_VARIABLES_ATTRIBUTE, new LinkedHashMap<>()); + } + + + @Test + public void supportsParameter() { + + assertFalse(this.resolver.supportsParameter(this.testMethod.arg(String.class))); + + assertTrue(this.resolver.supportsParameter(this.testMethod + .annot(matrixAttribute().noName()).arg(List.class, String.class))); + + assertTrue(this.resolver.supportsParameter(this.testMethod + .annot(matrixAttribute().name("year")).arg(int.class))); + } + + @Test + public void resolveArgument() throws Exception { + MultiValueMap params = getVariablesFor("cars"); + params.add("colors", "red"); + params.add("colors", "green"); + params.add("colors", "blue"); + MethodParameter param = this.testMethod.annot(matrixAttribute().noName()).arg(List.class, String.class); + + assertEquals(Arrays.asList("red", "green", "blue"), + this.resolver.resolveArgument(param, new BindingContext(), this.exchange).block(Duration.ZERO)); + } + + @Test + public void resolveArgumentPathVariable() throws Exception { + getVariablesFor("cars").add("year", "2006"); + getVariablesFor("bikes").add("year", "2005"); + MethodParameter param = this.testMethod.annot(matrixAttribute().name("year")).arg(int.class); + + Object actual = this.resolver.resolveArgument(param, new BindingContext(), this.exchange).block(Duration.ZERO); + assertEquals(2006, actual); + } + + @Test + public void resolveArgumentDefaultValue() throws Exception { + MethodParameter param = this.testMethod.annot(matrixAttribute().name("year")).arg(int.class); + Object actual = this.resolver.resolveArgument(param, new BindingContext(), this.exchange).block(Duration.ZERO); + assertEquals(2013, actual); + } + + @Test(expected = ServerErrorException.class) + public void resolveArgumentMultipleMatches() throws Exception { + getVariablesFor("var1").add("colors", "red"); + getVariablesFor("var2").add("colors", "green"); + + MethodParameter param = this.testMethod.annot(matrixAttribute().noName()).arg(List.class, String.class); + this.resolver.resolveArgument(param, new BindingContext(), this.exchange).block(Duration.ZERO); + } + + @Test(expected = ServerWebInputException.class) + public void resolveArgumentRequired() throws Exception { + MethodParameter param = this.testMethod.annot(matrixAttribute().noName()).arg(List.class, String.class); + this.resolver.resolveArgument(param, new BindingContext(), this.exchange).block(Duration.ZERO); + } + + @Test + public void resolveArgumentNoMatch() throws Exception { + MultiValueMap params = getVariablesFor("cars"); + params.add("anotherYear", "2012"); + MethodParameter param = this.testMethod.annot(matrixAttribute().name("year")).arg(int.class); + + Object actual = this.resolver.resolveArgument(param, new BindingContext(), this.exchange).block(Duration.ZERO); + assertEquals(2013, actual); + } + + @SuppressWarnings("unchecked") + private MultiValueMap getVariablesFor(String pathVarName) { + Map> matrixVariables = + this.exchange.getAttribute(HandlerMapping.MATRIX_VARIABLES_ATTRIBUTE); + + MultiValueMap params = new LinkedMultiValueMap<>(); + matrixVariables.put(pathVarName, params); + return params; + } + + + @SuppressWarnings("unused") + void handle( + String stringArg, + @MatrixVariable List colors, + @MatrixVariable(name = "year", pathVar = "cars", required = false, defaultValue = "2013") int preferredYear) { + } + +} diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java index 4e0a1c40a94..5eee687983b 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java @@ -26,13 +26,13 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.reactive.config.EnableWebFlux; -import static org.junit.Assert.*; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; /** * Integration tests with {@code @RequestMapping} handler methods. @@ -55,40 +55,23 @@ public class RequestMappingIntegrationTests extends AbstractRequestMappingIntegr } - @Test - public void handleWithParam() throws Exception { - String expected = "Hello George!"; - assertEquals(expected, performGet("/param?name=George", new HttpHeaders(), String.class).getBody()); - } - - @Test // SPR-15140 - public void handleWithEncodedParam() throws Exception { - String expected = "Hello + \u00e0!"; - assertEquals(expected, performGet("/param?name=%20%2B+%C3%A0", new HttpHeaders(), String.class).getBody()); - } - - @Test - public void longStreamResult() throws Exception { - String[] expected = {"0", "1", "2", "3", "4"}; - assertArrayEquals(expected, performGet("/long-stream-result", new HttpHeaders(), String[].class).getBody()); - } - - @Test - public void objectStreamResultWithAllMediaType() throws Exception { - String expected = "[{\"name\":\"bar\"}]"; - assertEquals(expected, performGet("/object-stream-result", MediaType.ALL, String.class).getBody()); - } - @Test public void httpHead() throws Exception { - String url = "http://localhost:" + this.port + "/param?name=George"; + String url = "http://localhost:" + this.port + "/text"; HttpHeaders headers = getRestTemplate().headForHeaders(url); String contentType = headers.getFirst("Content-Type"); assertNotNull(contentType); assertEquals("text/html;charset=utf-8", contentType.toLowerCase()); - assertEquals(13, headers.getContentLength()); + assertEquals(3, headers.getContentLength()); } + @Test + public void stream() throws Exception { + String[] expected = {"0", "1", "2", "3", "4"}; + assertArrayEquals(expected, performGet("/stream", new HttpHeaders(), String[].class).getBody()); + } + + @Configuration @EnableWebFlux static class WebConfig { @@ -96,41 +79,17 @@ public class RequestMappingIntegrationTests extends AbstractRequestMappingIntegr @RestController + @SuppressWarnings("unused") private static class TestRestController { - @GetMapping("/param") - public Publisher handleWithParam(@RequestParam String name) { - return Flux.just("Hello ", name, "!"); - } - - @GetMapping("/long-stream-result") - public Publisher longStreamResponseBody() { - return Flux.interval(Duration.ofMillis(100)).take(5); - } - - @GetMapping("/object-stream-result") - public Publisher objectStreamResponseBody() { - return Flux.just(new Foo("bar")); - } - } - - - private static class Foo { - - private String name; - - public Foo(String name) { - this.name = name; - } - - @SuppressWarnings("unused") - public String getName() { - return name; + @GetMapping("/text") + public String text() { + return "Foo"; } - @SuppressWarnings("unused") - public void setName(String name) { - this.name = name; + @GetMapping("/stream") + public Publisher stream() { + return Flux.interval(Duration.ofMillis(50)).take(5); } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariableMapMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariableMapMethodArgumentResolver.java index 0db14533dc4..daaa094a327 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariableMapMethodArgumentResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariableMapMethodArgumentResolver.java @@ -38,9 +38,13 @@ import org.springframework.web.method.support.ModelAndViewContainer; import org.springframework.web.servlet.HandlerMapping; /** - * Resolves method arguments of type Map annotated with - * {@link MatrixVariable @MatrixVariable} where the annotation does not - * specify a name. If a name is specified then the argument will by resolved by the + * Resolves arguments of type {@link Map} annotated with {@link MatrixVariable + * @MatrixVariable} where the annotation does not specify a name. In other words + * the purpose of this resolver is to provide access to multiple matrix + * variables, either all or associted with a specific path variable. + * + *

When a name is specified, an argument of type Map is considered to be an + * single attribute with a Map value, and is resolved by * {@link MatrixVariableMethodArgumentResolver} instead. * * @author Rossen Stoyanchev @@ -48,6 +52,7 @@ import org.springframework.web.servlet.HandlerMapping; */ public class MatrixVariableMapMethodArgumentResolver implements HandlerMethodArgumentResolver { + @Override public boolean supportsParameter(MethodParameter parameter) { MatrixVariable matrixVariable = parameter.getParameterAnnotation(MatrixVariable.class); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariableMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariableMethodArgumentResolver.java index bf1c860a6c1..a0bd0d29513 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariableMethodArgumentResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariableMethodArgumentResolver.java @@ -35,10 +35,12 @@ import org.springframework.web.method.annotation.AbstractNamedValueMethodArgumen import org.springframework.web.servlet.HandlerMapping; /** - * Resolves method arguments annotated with {@link MatrixVariable @MatrixVariable}. + * Resolves arguments annotated with {@link MatrixVariable @MatrixVariable}. * - *

If the method parameter is of type Map and no name is specified, then it will - * by resolved by the {@link MatrixVariableMapMethodArgumentResolver} instead. + *

If the method parameter is of type {@link Map} it will by resolved by + * {@link MatrixVariableMapMethodArgumentResolver} instead unless the annotation + * specifies a name in which case it is considered to be a single attribute of + * type map (vs multiple attributes collected in a map). * * @author Rossen Stoyanchev * @author Sam Brannen diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariablesMapMethodArgumentResolverTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariablesMapMethodArgumentResolverTests.java index cd1f69bb0df..6f0f439b9be 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariablesMapMethodArgumentResolverTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariablesMapMethodArgumentResolverTests.java @@ -16,7 +16,6 @@ package org.springframework.web.servlet.mvc.method.annotation; -import java.lang.reflect.Method; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; @@ -26,17 +25,20 @@ import org.junit.Before; import org.junit.Test; import org.springframework.core.MethodParameter; -import org.springframework.core.annotation.SynthesizingMethodParameter; import org.springframework.mock.web.test.MockHttpServletRequest; import org.springframework.mock.web.test.MockHttpServletResponse; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.MatrixVariable; import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.method.ResolvableMethod; import org.springframework.web.method.support.ModelAndViewContainer; import org.springframework.web.servlet.HandlerMapping; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.springframework.web.method.MvcAnnotationPredicates.matrixAttribute; /** * Test fixture with {@link MatrixVariableMethodArgumentResolver}. @@ -53,11 +55,7 @@ public class MatrixVariablesMapMethodArgumentResolverTests { private MockHttpServletRequest request; - private MethodParameter paramString; - private MethodParameter paramMap; - private MethodParameter paramMultivalueMap; - private MethodParameter paramMapForPathVar; - private MethodParameter paramMapWithName; + private final ResolvableMethod testMethod = ResolvableMethod.on(this.getClass()).named("handle").build(); @Before @@ -69,107 +67,126 @@ public class MatrixVariablesMapMethodArgumentResolverTests { Map> params = new LinkedHashMap<>(); this.request.setAttribute(HandlerMapping.MATRIX_VARIABLES_ATTRIBUTE, params); - - Method method = getClass().getMethod("handle", String.class, - Map.class, MultiValueMap.class, MultiValueMap.class, Map.class); - - this.paramString = new SynthesizingMethodParameter(method, 0); - this.paramMap = new SynthesizingMethodParameter(method, 1); - this.paramMultivalueMap = new SynthesizingMethodParameter(method, 2); - this.paramMapForPathVar = new SynthesizingMethodParameter(method, 3); - this.paramMapWithName = new SynthesizingMethodParameter(method, 4); } @Test public void supportsParameter() { - assertFalse(resolver.supportsParameter(paramString)); - assertTrue(resolver.supportsParameter(paramMap)); - assertTrue(resolver.supportsParameter(paramMultivalueMap)); - assertTrue(resolver.supportsParameter(paramMapForPathVar)); - assertFalse(resolver.supportsParameter(paramMapWithName)); + + assertFalse(this.resolver.supportsParameter(this.testMethod.arg(String.class))); + + assertTrue(this.resolver.supportsParameter(this.testMethod.annot(matrixAttribute().noName()) + .arg(Map.class, String.class, String.class))); + + assertTrue(this.resolver.supportsParameter(this.testMethod.annot(matrixAttribute().noPathVar()) + .arg(MultiValueMap.class, String.class, String.class))); + + assertTrue(this.resolver.supportsParameter(this.testMethod.annot(matrixAttribute().pathVar("cars")) + .arg(MultiValueMap.class, String.class, String.class))); + + assertFalse(this.resolver.supportsParameter(this.testMethod.annot(matrixAttribute().name("name")) + .arg(Map.class, String.class, String.class))); } @Test public void resolveArgument() throws Exception { - MultiValueMap params = getMatrixVariables("cars"); + MultiValueMap params = getVariablesFor("cars"); params.add("colors", "red"); params.add("colors", "green"); params.add("colors", "blue"); params.add("year", "2012"); + MethodParameter param = this.testMethod.annot(matrixAttribute().noName()) + .arg(Map.class, String.class, String.class); + @SuppressWarnings("unchecked") - Map map = (Map) this.resolver.resolveArgument( - this.paramMap, this.mavContainer, this.webRequest, null); + Map map = (Map) + this.resolver.resolveArgument(param, this.mavContainer, this.webRequest, null); assertEquals("red", map.get("colors")); + param = this.testMethod + .annot(matrixAttribute().noPathVar()) + .arg(MultiValueMap.class, String.class, String.class); + @SuppressWarnings("unchecked") - MultiValueMap multivalueMap = (MultiValueMap) this.resolver.resolveArgument( - this.paramMultivalueMap, this.mavContainer, this.webRequest, null); + MultiValueMap multivalueMap = (MultiValueMap) + this.resolver.resolveArgument(param, this.mavContainer, this.webRequest, null); assertEquals(Arrays.asList("red", "green", "blue"), multivalueMap.get("colors")); } @Test public void resolveArgumentPathVariable() throws Exception { - MultiValueMap params1 = getMatrixVariables("cars"); + MultiValueMap params1 = getVariablesFor("cars"); params1.add("colors", "red"); params1.add("colors", "purple"); - MultiValueMap params2 = getMatrixVariables("planes"); + MultiValueMap params2 = getVariablesFor("planes"); params2.add("colors", "yellow"); params2.add("colors", "orange"); + MethodParameter param = this.testMethod.annot(matrixAttribute().pathVar("cars")) + .arg(MultiValueMap.class, String.class, String.class); + @SuppressWarnings("unchecked") Map mapForPathVar = (Map) this.resolver.resolveArgument( - this.paramMapForPathVar, this.mavContainer, this.webRequest, null); + param, this.mavContainer, this.webRequest, null); assertEquals(Arrays.asList("red", "purple"), mapForPathVar.get("colors")); + param = this.testMethod.annot(matrixAttribute().noName()).arg(Map.class, String.class, String.class); + @SuppressWarnings("unchecked") - Map mapAll = (Map) this.resolver.resolveArgument( - this.paramMap, this.mavContainer, this.webRequest, null); + Map mapAll = (Map) + this.resolver.resolveArgument(param, this.mavContainer, this.webRequest, null); assertEquals("red", mapAll.get("colors")); } @Test public void resolveArgumentNoParams() throws Exception { + + MethodParameter param = this.testMethod.annot(matrixAttribute().noName()) + .arg(Map.class, String.class, String.class); + @SuppressWarnings("unchecked") - Map map = (Map) this.resolver.resolveArgument( - this.paramMap, this.mavContainer, this.webRequest, null); + Map map = (Map) + this.resolver.resolveArgument(param, this.mavContainer, this.webRequest, null); assertEquals(Collections.emptyMap(), map); } @Test public void resolveArgumentNoMatch() throws Exception { - MultiValueMap params2 = getMatrixVariables("planes"); + MultiValueMap params2 = getVariablesFor("planes"); params2.add("colors", "yellow"); params2.add("colors", "orange"); + MethodParameter param = this.testMethod.annot(matrixAttribute().pathVar("cars")) + .arg(MultiValueMap.class, String.class, String.class); + @SuppressWarnings("unchecked") - Map map = (Map) this.resolver.resolveArgument( - this.paramMapForPathVar, this.mavContainer, this.webRequest, null); + Map map = (Map) + this.resolver.resolveArgument(param, this.mavContainer, this.webRequest, null); assertEquals(Collections.emptyMap(), map); } @SuppressWarnings("unchecked") - private MultiValueMap getMatrixVariables(String pathVarName) { + private MultiValueMap getVariablesFor(String pathVarName) { Map> matrixVariables = (Map>) this.request.getAttribute( HandlerMapping.MATRIX_VARIABLES_ATTRIBUTE); MultiValueMap params = new LinkedMultiValueMap<>(); matrixVariables.put(pathVarName, params); - return params; } + @SuppressWarnings("unused") public void handle( String stringArg, @MatrixVariable Map map, diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariablesMethodArgumentResolverTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariablesMethodArgumentResolverTests.java index 92db11b0e05..89b4b75afb7 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariablesMethodArgumentResolverTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariablesMethodArgumentResolverTests.java @@ -16,7 +16,6 @@ package org.springframework.web.servlet.mvc.method.annotation; -import java.lang.reflect.Method; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; @@ -25,7 +24,6 @@ import java.util.Map; import org.junit.Before; import org.junit.Test; -import org.springframework.core.LocalVariableTableParameterNameDiscoverer; import org.springframework.core.MethodParameter; import org.springframework.mock.web.test.MockHttpServletRequest; import org.springframework.mock.web.test.MockHttpServletResponse; @@ -34,14 +32,17 @@ import org.springframework.util.MultiValueMap; import org.springframework.web.bind.ServletRequestBindingException; import org.springframework.web.bind.annotation.MatrixVariable; import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.method.ResolvableMethod; import org.springframework.web.method.support.ModelAndViewContainer; import org.springframework.web.servlet.HandlerMapping; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.springframework.web.method.MvcAnnotationPredicates.matrixAttribute; /** * Test fixture with {@link MatrixVariableMethodArgumentResolver}. - * * @author Rossen Stoyanchev */ public class MatrixVariablesMethodArgumentResolverTests { @@ -54,9 +55,7 @@ public class MatrixVariablesMethodArgumentResolverTests { private MockHttpServletRequest request; - private MethodParameter paramString; - private MethodParameter paramColors; - private MethodParameter paramYear; + private ResolvableMethod testMethod = ResolvableMethod.on(this.getClass()).named("handle").build(); @Before @@ -68,77 +67,81 @@ public class MatrixVariablesMethodArgumentResolverTests { Map> params = new LinkedHashMap<>(); this.request.setAttribute(HandlerMapping.MATRIX_VARIABLES_ATTRIBUTE, params); - - Method method = getClass().getMethod("handle", String.class, List.class, int.class); - this.paramString = new MethodParameter(method, 0); - this.paramColors = new MethodParameter(method, 1); - this.paramColors.initParameterNameDiscovery(new LocalVariableTableParameterNameDiscoverer()); - this.paramYear = new MethodParameter(method, 2); } @Test public void supportsParameter() { - assertFalse(resolver.supportsParameter(paramString)); - assertTrue(resolver.supportsParameter(paramColors)); - assertTrue(resolver.supportsParameter(paramYear)); + + assertFalse(this.resolver.supportsParameter(this.testMethod.arg(String.class))); + + assertTrue(this.resolver.supportsParameter( + this.testMethod.annot(matrixAttribute().noName()).arg(List.class, String.class))); + + assertTrue(this.resolver.supportsParameter( + this.testMethod.annot(matrixAttribute().name("year")).arg(int.class))); } @Test public void resolveArgument() throws Exception { - MultiValueMap params = getMatrixVariables("cars"); + MultiValueMap params = getVariablesFor("cars"); params.add("colors", "red"); params.add("colors", "green"); params.add("colors", "blue"); + MethodParameter param = this.testMethod.annot(matrixAttribute().noName()).arg(List.class, String.class); assertEquals(Arrays.asList("red", "green", "blue"), - this.resolver.resolveArgument(this.paramColors, this.mavContainer, this.webRequest, null)); + this.resolver.resolveArgument(param, this.mavContainer, this.webRequest, null)); } @Test public void resolveArgumentPathVariable() throws Exception { - getMatrixVariables("cars").add("year", "2006"); - getMatrixVariables("bikes").add("year", "2005"); + getVariablesFor("cars").add("year", "2006"); + getVariablesFor("bikes").add("year", "2005"); + MethodParameter param = this.testMethod.annot(matrixAttribute().name("year")).arg(int.class); - assertEquals("2006", this.resolver.resolveArgument(this.paramYear, this.mavContainer, this.webRequest, null)); + assertEquals("2006", this.resolver.resolveArgument(param, this.mavContainer, this.webRequest, null)); } @Test public void resolveArgumentDefaultValue() throws Exception { - assertEquals("2013", resolver.resolveArgument(this.paramYear, this.mavContainer, this.webRequest, null)); + MethodParameter param = this.testMethod.annot(matrixAttribute().name("year")).arg(int.class); + assertEquals("2013", resolver.resolveArgument(param, this.mavContainer, this.webRequest, null)); } @Test(expected = ServletRequestBindingException.class) public void resolveArgumentMultipleMatches() throws Exception { - getMatrixVariables("var1").add("colors", "red"); - getMatrixVariables("var2").add("colors", "green"); + getVariablesFor("var1").add("colors", "red"); + getVariablesFor("var2").add("colors", "green"); + MethodParameter param = this.testMethod.annot(matrixAttribute().noName()).arg(List.class, String.class); - this.resolver.resolveArgument(this.paramColors, this.mavContainer, this.webRequest, null); + this.resolver.resolveArgument(param, this.mavContainer, this.webRequest, null); } @Test(expected = ServletRequestBindingException.class) public void resolveArgumentRequired() throws Exception { - resolver.resolveArgument(this.paramColors, this.mavContainer, this.webRequest, null); + MethodParameter param = this.testMethod.annot(matrixAttribute().noName()).arg(List.class, String.class); + this.resolver.resolveArgument(param, this.mavContainer, this.webRequest, null); } @Test public void resolveArgumentNoMatch() throws Exception { - MultiValueMap params = getMatrixVariables("cars"); + MultiValueMap params = getVariablesFor("cars"); params.add("anotherYear", "2012"); + MethodParameter param = this.testMethod.annot(matrixAttribute().name("year")).arg(int.class); - assertEquals("2013", this.resolver.resolveArgument(this.paramYear, this.mavContainer, this.webRequest, null)); + assertEquals("2013", this.resolver.resolveArgument(param, this.mavContainer, this.webRequest, null)); } @SuppressWarnings("unchecked") - private MultiValueMap getMatrixVariables(String pathVarName) { + private MultiValueMap getVariablesFor(String pathVarName) { Map> matrixVariables = (Map>) this.request.getAttribute( HandlerMapping.MATRIX_VARIABLES_ATTRIBUTE); MultiValueMap params = new LinkedMultiValueMap<>(); matrixVariables.put(pathVarName, params); - return params; } diff --git a/src/docs/asciidoc/web/webflux.adoc b/src/docs/asciidoc/web/webflux.adoc index 4188115256a..40c4d9fa6d9 100644 --- a/src/docs/asciidoc/web/webflux.adoc +++ b/src/docs/asciidoc/web/webflux.adoc @@ -924,6 +924,10 @@ Java 8+: `java.time.ZoneId` |For access to URI template variables. // TODO: See <>. +|`@MatrixVariable` +|For access to name-value pairs in URI path segments. +// TODO: See <>. + |`@RequestParam` |For access to Servlet request parameters. Parameter values are converted to the declared method argument type.