Skip to content

Commit 1d0e484

Browse files
committed
Support access to all URI vars via @PathVariable Map
Issue: SPR-9289
1 parent 698d004 commit 1d0e484

File tree

8 files changed

+256
-48
lines changed

8 files changed

+256
-48
lines changed

spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@
7979
* will match against the regular expression {@code [^\.]*} (i.e. any character
8080
* other than period), but this can be changed by specifying another regular
8181
* expression, like so: /hotels/{hotel:\d+}.
82+
* Additionally, {@code @PathVariable} can be used on a
83+
* {@link java.util.Map Map<String, String>} to gain access to all
84+
* URI template variables.
8285
* <li>{@link RequestParam @RequestParam} annotated parameters for access to
8386
* specific Servlet/Portlet request parameters. Parameter values will be
8487
* converted to the declared method argument type. Additionally,

spring-web/src/main/java/org/springframework/web/method/annotation/RequestParamMethodArgumentResolver.java

Lines changed: 37 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2011 the original author or authors.
2+
* Copyright 2002-2012 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -44,24 +44,25 @@
4444
import org.springframework.web.util.WebUtils;
4545

4646
/**
47-
* Resolves method arguments annotated with @{@link RequestParam}, arguments of
48-
* type {@link MultipartFile} in conjunction with Spring's {@link MultipartResolver}
49-
* abstraction, and arguments of type {@code javax.servlet.http.Part} in conjunction
50-
* with Servlet 3.0 multipart requests. This resolver can also be created in default
51-
* resolution mode in which simple types (int, long, etc.) not annotated
52-
* with @{@link RequestParam} are also treated as request parameters with the
47+
* Resolves method arguments annotated with @{@link RequestParam}, arguments of
48+
* type {@link MultipartFile} in conjunction with Spring's {@link MultipartResolver}
49+
* abstraction, and arguments of type {@code javax.servlet.http.Part} in conjunction
50+
* with Servlet 3.0 multipart requests. This resolver can also be created in default
51+
* resolution mode in which simple types (int, long, etc.) not annotated
52+
* with @{@link RequestParam} are also treated as request parameters with the
5353
* parameter name derived from the argument name.
54-
*
55-
* <p>If the method parameter type is {@link Map}, the request parameter name is used to
56-
* resolve the request parameter String value. The value is then converted to a {@link Map}
57-
* via type conversion assuming a suitable {@link Converter} or {@link PropertyEditor} has
58-
* been registered. If a request parameter name is not specified with a {@link Map} method
59-
* parameter type, the {@link RequestParamMapMethodArgumentResolver} is used instead
60-
* providing access to all request parameters in the form of a map.
61-
*
62-
* <p>A {@link WebDataBinder} is invoked to apply type conversion to resolved request
54+
*
55+
* <p>If the method parameter type is {@link Map}, the name specified in the
56+
* annotation is used to resolve the request parameter String value. The value is
57+
* then converted to a {@link Map} via type conversion assuming a suitable
58+
* {@link Converter} or {@link PropertyEditor} has been registered.
59+
* Or if a request parameter name is not specified the
60+
* {@link RequestParamMapMethodArgumentResolver} is used instead to provide
61+
* access to all request parameters in the form of a map.
62+
*
63+
* <p>A {@link WebDataBinder} is invoked to apply type conversion to resolved request
6364
* header values that don't yet match the method parameter type.
64-
*
65+
*
6566
* @author Arjen Poutsma
6667
* @author Rossen Stoyanchev
6768
* @since 3.1
@@ -72,15 +73,15 @@ public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethod
7273
private final boolean useDefaultResolution;
7374

7475
/**
75-
* @param beanFactory a bean factory used for resolving ${...} placeholder
76-
* and #{...} SpEL expressions in default values, or {@code null} if default
76+
* @param beanFactory a bean factory used for resolving ${...} placeholder
77+
* and #{...} SpEL expressions in default values, or {@code null} if default
7778
* values are not expected to contain expressions
78-
* @param useDefaultResolution in default resolution mode a method argument
79-
* that is a simple type, as defined in {@link BeanUtils#isSimpleProperty},
80-
* is treated as a request parameter even if it itsn't annotated, the
79+
* @param useDefaultResolution in default resolution mode a method argument
80+
* that is a simple type, as defined in {@link BeanUtils#isSimpleProperty},
81+
* is treated as a request parameter even if it itsn't annotated, the
8182
* request parameter name is derived from the method parameter name.
8283
*/
83-
public RequestParamMethodArgumentResolver(ConfigurableBeanFactory beanFactory,
84+
public RequestParamMethodArgumentResolver(ConfigurableBeanFactory beanFactory,
8485
boolean useDefaultResolution) {
8586
super(beanFactory);
8687
this.useDefaultResolution = useDefaultResolution;
@@ -89,15 +90,15 @@ public RequestParamMethodArgumentResolver(ConfigurableBeanFactory beanFactory,
8990
/**
9091
* Supports the following:
9192
* <ul>
92-
* <li>@RequestParam-annotated method arguments.
93-
* This excludes {@link Map} params where the annotation doesn't
94-
* specify a name. See {@link RequestParamMapMethodArgumentResolver}
93+
* <li>@RequestParam-annotated method arguments.
94+
* This excludes {@link Map} params where the annotation doesn't
95+
* specify a name. See {@link RequestParamMapMethodArgumentResolver}
9596
* instead for such params.
96-
* <li>Arguments of type {@link MultipartFile}
97+
* <li>Arguments of type {@link MultipartFile}
9798
* unless annotated with @{@link RequestPart}.
98-
* <li>Arguments of type {@code javax.servlet.http.Part}
99+
* <li>Arguments of type {@code javax.servlet.http.Part}
99100
* unless annotated with @{@link RequestPart}.
100-
* <li>In default resolution mode, simple type arguments
101+
* <li>In default resolution mode, simple type arguments
101102
* even if not with @{@link RequestParam}.
102103
* </ul>
103104
*/
@@ -131,18 +132,18 @@ else if (this.useDefaultResolution) {
131132
@Override
132133
protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
133134
RequestParam annotation = parameter.getParameterAnnotation(RequestParam.class);
134-
return (annotation != null) ?
135-
new RequestParamNamedValueInfo(annotation) :
135+
return (annotation != null) ?
136+
new RequestParamNamedValueInfo(annotation) :
136137
new RequestParamNamedValueInfo();
137138
}
138139

139140
@Override
140141
protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest webRequest) throws Exception {
141142

142143
Object arg;
143-
144+
144145
HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
145-
MultipartHttpServletRequest multipartRequest =
146+
MultipartHttpServletRequest multipartRequest =
146147
WebUtils.getNativeRequest(servletRequest, MultipartHttpServletRequest.class);
147148

148149
if (MultipartFile.class.equals(parameter.getParameterType())) {
@@ -174,7 +175,7 @@ else if ("javax.servlet.http.Part".equals(parameter.getParameterType().getName()
174175
}
175176
}
176177
}
177-
178+
178179
return arg;
179180
}
180181

@@ -184,7 +185,7 @@ private void assertIsMultipartRequest(HttpServletRequest request) {
184185
throw new MultipartException("The current request is not a multipart request");
185186
}
186187
}
187-
188+
188189
private boolean isMultipartFileCollection(MethodParameter parameter) {
189190
Class<?> paramType = parameter.getParameterType();
190191
if (Collection.class.equals(paramType) || List.class.isAssignableFrom(paramType)){
@@ -206,7 +207,7 @@ private class RequestParamNamedValueInfo extends NamedValueInfo {
206207
private RequestParamNamedValueInfo() {
207208
super("", false, ValueConstants.DEFAULT_NONE);
208209
}
209-
210+
210211
private RequestParamNamedValueInfo(RequestParam annotation) {
211212
super(annotation.value(), annotation.required(), annotation.defaultValue());
212213
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright 2002-2012 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.servlet.mvc.method.annotation;
18+
19+
import java.util.LinkedHashMap;
20+
import java.util.Map;
21+
22+
import org.springframework.core.MethodParameter;
23+
import org.springframework.util.CollectionUtils;
24+
import org.springframework.util.StringUtils;
25+
import org.springframework.web.bind.ServletRequestBindingException;
26+
import org.springframework.web.bind.annotation.PathVariable;
27+
import org.springframework.web.bind.support.WebDataBinderFactory;
28+
import org.springframework.web.context.request.NativeWebRequest;
29+
import org.springframework.web.context.request.RequestAttributes;
30+
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
31+
import org.springframework.web.method.support.ModelAndViewContainer;
32+
import org.springframework.web.servlet.HandlerMapping;
33+
34+
/**
35+
* Resolves {@link Map} method arguments annotated with an @{@link PathVariable}
36+
* where the annotation does not specify a path variable name. The created
37+
* {@link Map} contains all URI template name/value pairs.
38+
*
39+
* @author Rossen Stoyanchev
40+
* @since 3.2
41+
* @see PathVariableMethodArgumentResolver
42+
*/
43+
public class PathVariableMapMethodArgumentResolver implements HandlerMethodArgumentResolver {
44+
45+
public boolean supportsParameter(MethodParameter parameter) {
46+
PathVariable annot = parameter.getParameterAnnotation(PathVariable.class);
47+
return ((annot != null) && (Map.class.isAssignableFrom(parameter.getParameterType()))
48+
&& (!StringUtils.hasText(annot.value())));
49+
}
50+
51+
/**
52+
* Return a Map with all URI template variables.
53+
* @throws ServletRequestBindingException if no URI vars are found in the
54+
* request attribute {@link HandlerMapping#URI_TEMPLATE_VARIABLES_ATTRIBUTE}
55+
*/
56+
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
57+
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
58+
59+
@SuppressWarnings("unchecked")
60+
Map<String, String> uriTemplateVars =
61+
(Map<String, String>) webRequest.getAttribute(
62+
HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
63+
64+
if (CollectionUtils.isEmpty(uriTemplateVars)) {
65+
throw new ServletRequestBindingException(
66+
"No URI template variables for method parameter type [" + parameter.getParameterType() + "]");
67+
}
68+
69+
return new LinkedHashMap<String, String>(uriTemplateVars);
70+
}
71+
72+
}

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/PathVariableMethodArgumentResolver.java

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2011 the original author or authors.
2+
* Copyright 2002-2012 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,31 +16,45 @@
1616

1717
package org.springframework.web.servlet.mvc.method.annotation;
1818

19+
import java.beans.PropertyEditor;
1920
import java.util.HashMap;
2021
import java.util.Map;
2122

2223
import org.springframework.core.MethodParameter;
24+
import org.springframework.core.convert.converter.Converter;
25+
import org.springframework.util.StringUtils;
2326
import org.springframework.web.bind.ServletRequestBindingException;
2427
import org.springframework.web.bind.WebDataBinder;
2528
import org.springframework.web.bind.annotation.PathVariable;
2629
import org.springframework.web.bind.annotation.ValueConstants;
2730
import org.springframework.web.context.request.NativeWebRequest;
2831
import org.springframework.web.context.request.RequestAttributes;
2932
import org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver;
33+
import org.springframework.web.method.annotation.RequestParamMapMethodArgumentResolver;
3034
import org.springframework.web.method.support.ModelAndViewContainer;
3135
import org.springframework.web.servlet.HandlerMapping;
3236
import org.springframework.web.servlet.View;
3337

3438
/**
3539
* Resolves method arguments annotated with an @{@link PathVariable}.
3640
*
37-
* <p>An @{@link PathVariable} is a named value that gets resolved from a URI template variable. It is always
38-
* required and does not have a default value to fall back on. See the base class
39-
* {@link org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver} for more information on how named values are processed.
40-
*
41-
* <p>A {@link WebDataBinder} is invoked to apply type conversion to resolved path variable values that
41+
* <p>An @{@link PathVariable} is a named value that gets resolved from a URI
42+
* template variable. It is always required and does not have a default value
43+
* to fall back on. See the base class
44+
* {@link org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver}
45+
* for more information on how named values are processed.
46+
*
47+
* <p>If the method parameter type is {@link Map}, the name specified in the
48+
* annotation is used to resolve the URI variable String value. The value is
49+
* then converted to a {@link Map} via type conversion assuming a suitable
50+
* {@link Converter} or {@link PropertyEditor} has been registered.
51+
* Or if the annotation does not specify name the
52+
* {@link RequestParamMapMethodArgumentResolver} is used instead to provide
53+
* access to all URI variables in a map.
54+
*
55+
* <p>A {@link WebDataBinder} is invoked to apply type conversion to resolved path variable values that
4256
* don't yet match the method parameter type.
43-
*
57+
*
4458
* @author Rossen Stoyanchev
4559
* @author Arjen Poutsma
4660
* @since 3.1
@@ -52,7 +66,14 @@ public PathVariableMethodArgumentResolver() {
5266
}
5367

5468
public boolean supportsParameter(MethodParameter parameter) {
55-
return parameter.hasParameterAnnotation(PathVariable.class);
69+
if (!parameter.hasParameterAnnotation(PathVariable.class)) {
70+
return false;
71+
}
72+
if (Map.class.isAssignableFrom(parameter.getParameterType())) {
73+
String paramName = parameter.getParameterAnnotation(PathVariable.class).value();
74+
return StringUtils.hasText(paramName);
75+
}
76+
return true;
5677
}
5778

5879
@Override
@@ -64,7 +85,7 @@ protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
6485
@Override
6586
@SuppressWarnings("unchecked")
6687
protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
67-
Map<String, String> uriTemplateVars =
88+
Map<String, String> uriTemplateVars =
6889
(Map<String, String>) request.getAttribute(
6990
HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
7091
return (uriTemplateVars != null) ? uriTemplateVars.get(name) : null;
@@ -79,10 +100,10 @@ protected void handleMissingValue(String name, MethodParameter param) throws Ser
79100

80101
@Override
81102
@SuppressWarnings("unchecked")
82-
protected void handleResolvedValue(Object arg,
83-
String name,
103+
protected void handleResolvedValue(Object arg,
104+
String name,
84105
MethodParameter parameter,
85-
ModelAndViewContainer mavContainer,
106+
ModelAndViewContainer mavContainer,
86107
NativeWebRequest request) {
87108
String key = View.PATH_VARIABLES;
88109
int scope = RequestAttributes.SCOPE_REQUEST;

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,7 @@ private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
454454
resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));
455455
resolvers.add(new RequestParamMapMethodArgumentResolver());
456456
resolvers.add(new PathVariableMethodArgumentResolver());
457+
resolvers.add(new PathVariableMapMethodArgumentResolver());
457458
resolvers.add(new ServletModelAttributeMethodProcessor(false));
458459
resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters()));
459460
resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters()));

0 commit comments

Comments
 (0)