Support access to all URI vars via @PathVariable Map

Issue: SPR-9289
This commit is contained in:
Rossen Stoyanchev
2012-05-14 16:01:16 -04:00
parent 698d004260
commit 1d0e484eac
8 changed files with 256 additions and 48 deletions

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2002-2012 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.servlet.mvc.method.annotation;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.core.MethodParameter;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.servlet.HandlerMapping;
/**
* Resolves {@link Map} method arguments annotated with an @{@link PathVariable}
* where the annotation does not specify a path variable name. The created
* {@link Map} contains all URI template name/value pairs.
*
* @author Rossen Stoyanchev
* @since 3.2
* @see PathVariableMethodArgumentResolver
*/
public class PathVariableMapMethodArgumentResolver implements HandlerMethodArgumentResolver {
public boolean supportsParameter(MethodParameter parameter) {
PathVariable annot = parameter.getParameterAnnotation(PathVariable.class);
return ((annot != null) && (Map.class.isAssignableFrom(parameter.getParameterType()))
&& (!StringUtils.hasText(annot.value())));
}
/**
* Return a Map with all URI template variables.
* @throws ServletRequestBindingException if no URI vars are found in the
* request attribute {@link HandlerMapping#URI_TEMPLATE_VARIABLES_ATTRIBUTE}
*/
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
@SuppressWarnings("unchecked")
Map<String, String> uriTemplateVars =
(Map<String, String>) webRequest.getAttribute(
HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
if (CollectionUtils.isEmpty(uriTemplateVars)) {
throw new ServletRequestBindingException(
"No URI template variables for method parameter type [" + parameter.getParameterType() + "]");
}
return new LinkedHashMap<String, String>(uriTemplateVars);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2011 the original author or authors.
* Copyright 2002-2012 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.
@@ -16,10 +16,13 @@
package org.springframework.web.servlet.mvc.method.annotation;
import java.beans.PropertyEditor;
import java.util.HashMap;
import java.util.Map;
import org.springframework.core.MethodParameter;
import org.springframework.core.convert.converter.Converter;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.PathVariable;
@@ -27,6 +30,7 @@ import org.springframework.web.bind.annotation.ValueConstants;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver;
import org.springframework.web.method.annotation.RequestParamMapMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.View;
@@ -34,13 +38,23 @@ import org.springframework.web.servlet.View;
/**
* Resolves method arguments annotated with an @{@link PathVariable}.
*
* <p>An @{@link PathVariable} is a named value that gets resolved from a URI template variable. It is always
* required and does not have a default value to fall back on. See the base class
* {@link org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver} for more information on how named values are processed.
*
* <p>A {@link WebDataBinder} is invoked to apply type conversion to resolved path variable values that
* <p>An @{@link PathVariable} is a named value that gets resolved from a URI
* template variable. It is always required and does not have a default value
* to fall back on. See the base class
* {@link org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver}
* for more information on how named values are processed.
*
* <p>If the method parameter type is {@link Map}, the name specified in the
* annotation is used to resolve the URI variable String value. The value is
* then converted to a {@link Map} via type conversion assuming a suitable
* {@link Converter} or {@link PropertyEditor} has been registered.
* Or if the annotation does not specify name the
* {@link RequestParamMapMethodArgumentResolver} is used instead to provide
* access to all URI variables in a map.
*
* <p>A {@link WebDataBinder} is invoked to apply type conversion to resolved path variable values that
* don't yet match the method parameter type.
*
*
* @author Rossen Stoyanchev
* @author Arjen Poutsma
* @since 3.1
@@ -52,7 +66,14 @@ public class PathVariableMethodArgumentResolver extends AbstractNamedValueMethod
}
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(PathVariable.class);
if (!parameter.hasParameterAnnotation(PathVariable.class)) {
return false;
}
if (Map.class.isAssignableFrom(parameter.getParameterType())) {
String paramName = parameter.getParameterAnnotation(PathVariable.class).value();
return StringUtils.hasText(paramName);
}
return true;
}
@Override
@@ -64,7 +85,7 @@ public class PathVariableMethodArgumentResolver extends AbstractNamedValueMethod
@Override
@SuppressWarnings("unchecked")
protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
Map<String, String> uriTemplateVars =
Map<String, String> uriTemplateVars =
(Map<String, String>) request.getAttribute(
HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
return (uriTemplateVars != null) ? uriTemplateVars.get(name) : null;
@@ -79,10 +100,10 @@ public class PathVariableMethodArgumentResolver extends AbstractNamedValueMethod
@Override
@SuppressWarnings("unchecked")
protected void handleResolvedValue(Object arg,
String name,
protected void handleResolvedValue(Object arg,
String name,
MethodParameter parameter,
ModelAndViewContainer mavContainer,
ModelAndViewContainer mavContainer,
NativeWebRequest request) {
String key = View.PATH_VARIABLES;
int scope = RequestAttributes.SCOPE_REQUEST;

View File

@@ -454,6 +454,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));
resolvers.add(new RequestParamMapMethodArgumentResolver());
resolvers.add(new PathVariableMethodArgumentResolver());
resolvers.add(new PathVariableMapMethodArgumentResolver());
resolvers.add(new ServletModelAttributeMethodProcessor(false));
resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters()));
resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters()));

View File

@@ -0,0 +1,104 @@
/*
* Copyright 2002-2012 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.servlet.mvc.method.annotation;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.springframework.core.MethodParameter;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.servlet.HandlerMapping;
/**
* Test fixture with {@link PathVariableMapMethodArgumentResolver}.
*
* @author Rossen Stoyanchev
*/
public class PathVariableMapMethodArgumentResolverTests {
private PathVariableMapMethodArgumentResolver resolver;
private MethodParameter paramMap;
private MethodParameter paramNamedMap;
private MethodParameter paramMapNoAnnot;
private ModelAndViewContainer mavContainer;
private ServletWebRequest webRequest;
private MockHttpServletRequest request;
@Before
public void setUp() throws Exception {
resolver = new PathVariableMapMethodArgumentResolver();
Method method = getClass().getMethod("handle", Map.class, Map.class, Map.class);
paramMap = new MethodParameter(method, 0);
paramNamedMap = new MethodParameter(method, 1);
paramMapNoAnnot = new MethodParameter(method, 2);
mavContainer = new ModelAndViewContainer();
request = new MockHttpServletRequest();
webRequest = new ServletWebRequest(request, new MockHttpServletResponse());
}
@Test
public void supportsParameter() {
assertTrue(resolver.supportsParameter(paramMap));
assertFalse(resolver.supportsParameter(paramNamedMap));
assertFalse(resolver.supportsParameter(paramMapNoAnnot));
}
@Test
public void resolveArgument() throws Exception {
Map<String, String> uriTemplateVars = new HashMap<String, String>();
uriTemplateVars.put("name1", "value1");
uriTemplateVars.put("name2", "value2");
request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVars);
Object result = resolver.resolveArgument(paramMap, mavContainer, webRequest, null);
assertEquals(uriTemplateVars, result);
}
@Test(expected=ServletRequestBindingException.class)
public void resolveArgumentNoUriVars() throws Exception {
resolver.resolveArgument(paramMap, mavContainer, webRequest, null);
}
public void handle(
@PathVariable Map<String, String> map,
@PathVariable(value = "name") Map<String, String> namedMap,
Map<String, String> mapWithoutAnnotat) {
}
}