Skip to content

Commit c30290b

Browse files
committed
@PathVariable supports 'required' attribute (for model attribute methods)
Issue: SPR-14646 (cherry picked from commit e08b1b7)
1 parent d26421f commit c30290b

File tree

3 files changed

+111
-25
lines changed

3 files changed

+111
-25
lines changed

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

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2014 the original author or authors.
2+
* Copyright 2002-2016 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.
@@ -22,6 +22,8 @@
2222
import java.lang.annotation.RetentionPolicy;
2323
import java.lang.annotation.Target;
2424

25+
import org.springframework.core.annotation.AliasFor;
26+
2527
/**
2628
* Annotation which indicates that a method parameter should be bound to a URI template
2729
* variable. Supported for {@link RequestMapping} annotated handler methods in Servlet
@@ -32,6 +34,7 @@
3234
* then the map is populated with all path variable names and values.
3335
*
3436
* @author Arjen Poutsma
37+
* @author Juergen Hoeller
3538
* @since 3.0
3639
* @see RequestMapping
3740
* @see org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter
@@ -43,8 +46,26 @@
4346
public @interface PathVariable {
4447

4548
/**
46-
* The URI template variable to bind to.
49+
* Alias for {@link #name}.
4750
*/
51+
@AliasFor("name")
4852
String value() default "";
4953

54+
/**
55+
* The name of the path variable to bind to.
56+
* @since 4.3.3
57+
*/
58+
@AliasFor("value")
59+
String name() default "";
60+
61+
/**
62+
* Whether the path variable is required.
63+
* <p>Defaults to {@code true}, leading to an exception being thrown if the path
64+
* variable is missing in the incoming request. Switch this to {@code false} if
65+
* you prefer a {@code null} or Java 8 {@code java.util.Optional} in this case.
66+
* e.g. on a {@code ModelAttribute} method which serves for different requests.
67+
* @since 4.3.3
68+
*/
69+
boolean required() default true;
70+
5071
}

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

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
*
5858
* @author Rossen Stoyanchev
5959
* @author Arjen Poutsma
60+
* @author Juergen Hoeller
6061
* @since 3.1
6162
*/
6263
public class PathVariableMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver
@@ -65,10 +66,6 @@ public class PathVariableMethodArgumentResolver extends AbstractNamedValueMethod
6566
private static final TypeDescriptor STRING_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(String.class);
6667

6768

68-
public PathVariableMethodArgumentResolver() {
69-
}
70-
71-
7269
@Override
7370
public boolean supportsParameter(MethodParameter parameter) {
7471
if (!parameter.hasParameterAnnotation(PathVariable.class)) {
@@ -96,9 +93,7 @@ protected Object resolveName(String name, MethodParameter parameter, NativeWebRe
9693
}
9794

9895
@Override
99-
protected void handleMissingValue(String name, MethodParameter parameter)
100-
throws ServletRequestBindingException {
101-
96+
protected void handleMissingValue(String name, MethodParameter parameter) throws ServletRequestBindingException {
10297
throw new MissingPathVariableException(name, parameter);
10398
}
10499

@@ -121,13 +116,13 @@ protected void handleResolvedValue(Object arg, String name, MethodParameter para
121116
public void contributeMethodArgument(MethodParameter parameter, Object value,
122117
UriComponentsBuilder builder, Map<String, Object> uriVariables, ConversionService conversionService) {
123118

124-
if (Map.class.isAssignableFrom(parameter.getNestedParameterType())) {
119+
if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) {
125120
return;
126121
}
127122

128123
PathVariable ann = parameter.getParameterAnnotation(PathVariable.class);
129-
String name = (ann == null || StringUtils.isEmpty(ann.value()) ? parameter.getParameterName() : ann.value());
130-
value = formatUriValue(conversionService, new TypeDescriptor(parameter), value);
124+
String name = (ann != null && !StringUtils.isEmpty(ann.value()) ? ann.value() : parameter.getParameterName());
125+
value = formatUriValue(conversionService, new TypeDescriptor(parameter.nestedIfOptional()), value);
131126
uriVariables.put(name, value);
132127
}
133128

@@ -150,7 +145,7 @@ else if (cs != null) {
150145
private static class PathVariableNamedValueInfo extends NamedValueInfo {
151146

152147
public PathVariableNamedValueInfo(PathVariable annotation) {
153-
super(annotation.value(), true, ValueConstants.DEFAULT_NONE);
148+
super(annotation.name(), annotation.required(), ValueConstants.DEFAULT_NONE);
154149
}
155150
}
156151

spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/PathVariableMethodArgumentResolverTests.java

Lines changed: 82 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2015 the original author or authors.
2+
* Copyright 2002-2016 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,29 +16,37 @@
1616

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

19-
import static org.junit.Assert.*;
20-
2119
import java.lang.reflect.Method;
2220
import java.util.HashMap;
2321
import java.util.Map;
22+
import java.util.Optional;
2423

2524
import org.junit.Before;
2625
import org.junit.Test;
2726

2827
import org.springframework.core.MethodParameter;
28+
import org.springframework.core.annotation.SynthesizingMethodParameter;
29+
import org.springframework.core.convert.support.DefaultConversionService;
2930
import org.springframework.mock.web.test.MockHttpServletRequest;
3031
import org.springframework.mock.web.test.MockHttpServletResponse;
32+
import org.springframework.util.ReflectionUtils;
3133
import org.springframework.web.bind.MissingPathVariableException;
3234
import org.springframework.web.bind.annotation.PathVariable;
35+
import org.springframework.web.bind.support.ConfigurableWebBindingInitializer;
36+
import org.springframework.web.bind.support.DefaultDataBinderFactory;
37+
import org.springframework.web.bind.support.WebDataBinderFactory;
3338
import org.springframework.web.context.request.ServletWebRequest;
3439
import org.springframework.web.method.support.ModelAndViewContainer;
3540
import org.springframework.web.servlet.HandlerMapping;
3641
import org.springframework.web.servlet.View;
3742

43+
import static org.junit.Assert.*;
44+
3845
/**
3946
* Test fixture with {@link PathVariableMethodArgumentResolver}.
4047
*
4148
* @author Rossen Stoyanchev
49+
* @author Juergen Hoeller
4250
*/
4351
public class PathVariableMethodArgumentResolverTests {
4452

@@ -48,25 +56,33 @@ public class PathVariableMethodArgumentResolverTests {
4856

4957
private MethodParameter paramString;
5058

59+
private MethodParameter paramNotRequired;
60+
61+
private MethodParameter paramOptional;
62+
5163
private ModelAndViewContainer mavContainer;
5264

5365
private ServletWebRequest webRequest;
5466

5567
private MockHttpServletRequest request;
5668

69+
5770
@Before
5871
public void setUp() throws Exception {
5972
resolver = new PathVariableMethodArgumentResolver();
6073

61-
Method method = getClass().getMethod("handle", String.class, String.class);
62-
paramNamedString = new MethodParameter(method, 0);
63-
paramString = new MethodParameter(method, 1);
74+
Method method = ReflectionUtils.findMethod(getClass(), "handle", (Class<?>[]) null);
75+
paramNamedString = new SynthesizingMethodParameter(method, 0);
76+
paramString = new SynthesizingMethodParameter(method, 1);
77+
paramNotRequired = new SynthesizingMethodParameter(method, 2);
78+
paramOptional = new SynthesizingMethodParameter(method, 3);
6479

6580
mavContainer = new ModelAndViewContainer();
6681
request = new MockHttpServletRequest();
6782
webRequest = new ServletWebRequest(request, new MockHttpServletResponse());
6883
}
6984

85+
7086
@Test
7187
public void supportsParameter() {
7288
assertTrue("Parameter with @PathVariable annotation", resolver.supportsParameter(paramNamedString));
@@ -89,21 +105,58 @@ public void resolveArgument() throws Exception {
89105
assertEquals("value", pathVars.get("name"));
90106
}
91107

92-
@SuppressWarnings("unchecked")
108+
@Test
109+
public void resolveArgumentNotRequired() throws Exception {
110+
Map<String, String> uriTemplateVars = new HashMap<>();
111+
uriTemplateVars.put("name", "value");
112+
request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVars);
113+
114+
String result = (String) resolver.resolveArgument(paramNotRequired, mavContainer, webRequest, null);
115+
assertEquals("PathVariable not resolved correctly", "value", result);
116+
117+
@SuppressWarnings("unchecked")
118+
Map<String, Object> pathVars = (Map<String, Object>) request.getAttribute(View.PATH_VARIABLES);
119+
assertNotNull(pathVars);
120+
assertEquals(1, pathVars.size());
121+
assertEquals("value", pathVars.get("name"));
122+
}
123+
124+
@Test
125+
public void resolveArgumentWrappedAsOptional() throws Exception {
126+
Map<String, String> uriTemplateVars = new HashMap<>();
127+
uriTemplateVars.put("name", "value");
128+
request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVars);
129+
130+
ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer();
131+
initializer.setConversionService(new DefaultConversionService());
132+
WebDataBinderFactory binderFactory = new DefaultDataBinderFactory(initializer);
133+
134+
@SuppressWarnings("unchecked")
135+
Optional<String> result = (Optional<String>)
136+
resolver.resolveArgument(paramOptional, mavContainer, webRequest, binderFactory);
137+
assertEquals("PathVariable not resolved correctly", "value", result.get());
138+
139+
@SuppressWarnings("unchecked")
140+
Map<String, Object> pathVars = (Map<String, Object>) request.getAttribute(View.PATH_VARIABLES);
141+
assertNotNull(pathVars);
142+
assertEquals(1, pathVars.size());
143+
assertEquals(Optional.of("value"), pathVars.get("name"));
144+
}
145+
93146
@Test
94147
public void resolveArgumentWithExistingPathVars() throws Exception {
95148
Map<String, String> uriTemplateVars = new HashMap<String, String>();
96149
uriTemplateVars.put("name", "value");
97150
request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVars);
98151

99-
Map<String, Object> pathVars;
100152
uriTemplateVars.put("oldName", "oldValue");
101153
request.setAttribute(View.PATH_VARIABLES, uriTemplateVars);
102154

103155
String result = (String) resolver.resolveArgument(paramNamedString, mavContainer, webRequest, null);
104156
assertEquals("PathVariable not resolved correctly", "value", result);
105157

106-
pathVars = (Map<String, Object>) request.getAttribute(View.PATH_VARIABLES);
158+
@SuppressWarnings("unchecked")
159+
Map<String, Object> pathVars = (Map<String, Object>) request.getAttribute(View.PATH_VARIABLES);
107160
assertNotNull(pathVars);
108161
assertEquals(2, pathVars.size());
109162
assertEquals("value", pathVars.get("name"));
@@ -113,11 +166,28 @@ public void resolveArgumentWithExistingPathVars() throws Exception {
113166
@Test(expected = MissingPathVariableException.class)
114167
public void handleMissingValue() throws Exception {
115168
resolver.resolveArgument(paramNamedString, mavContainer, webRequest, null);
116-
fail("Unresolved path variable should lead to exception.");
169+
fail("Unresolved path variable should lead to exception");
170+
}
171+
172+
@Test
173+
public void nullIfNotRequired() throws Exception {
174+
assertNull(resolver.resolveArgument(paramNotRequired, mavContainer, webRequest, null));
117175
}
118176

177+
@Test
178+
public void wrapEmptyWithOptional() throws Exception {
179+
ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer();
180+
initializer.setConversionService(new DefaultConversionService());
181+
WebDataBinderFactory binderFactory = new DefaultDataBinderFactory(initializer);
182+
183+
assertEquals(Optional.empty(), resolver.resolveArgument(paramOptional, mavContainer, webRequest, binderFactory));
184+
}
185+
186+
119187
@SuppressWarnings("unused")
120-
public void handle(@PathVariable(value = "name") String param1, String param2) {
188+
public void handle(@PathVariable("name") String param1, String param2,
189+
@PathVariable(name="name", required = false) String param3,
190+
@PathVariable("name") Optional<String> param4) {
121191
}
122192

123-
}
193+
}

0 commit comments

Comments
 (0)