Skip to content

Commit 57307a0

Browse files
committed
Decode path vars when url decoding is turned off
When URL decoding is turned off in AbstractHandlerMapping, the extracted path variables are also not encoded. Turning off URL decoding may be necessary for request mapping to work correctly when the path may contain the (encoded) special character '/'. At the same time there is no good reason not to leave path variables encoded. This change ensures path variables are encoded when URL decoding is turned off. Issue: SPR-9098
1 parent 36e7cb2 commit 57307a0

File tree

4 files changed

+88
-33
lines changed

4 files changed

+88
-33
lines changed

spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2010 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.
@@ -18,12 +18,15 @@
1818

1919
import java.io.UnsupportedEncodingException;
2020
import java.net.URLDecoder;
21+
import java.util.LinkedHashMap;
22+
import java.util.Map;
23+
import java.util.Map.Entry;
2124
import java.util.Properties;
25+
2226
import javax.servlet.http.HttpServletRequest;
2327

2428
import org.apache.commons.logging.Log;
2529
import org.apache.commons.logging.LogFactory;
26-
2730
import org.springframework.util.StringUtils;
2831

2932
/**
@@ -37,6 +40,7 @@
3740
*
3841
* @author Juergen Hoeller
3942
* @author Rob Harrop
43+
* @author Rossen Stoyanchev
4044
* @since 14.01.2004
4145
*/
4246
public class UrlPathHelper {
@@ -301,7 +305,7 @@ public String getOriginatingServletPath(HttpServletRequest request) {
301305
* @return the query string
302306
*/
303307
public String getOriginatingQueryString(HttpServletRequest request) {
304-
if ((request.getAttribute(WebUtils.FORWARD_REQUEST_URI_ATTRIBUTE) != null) ||
308+
if ((request.getAttribute(WebUtils.FORWARD_REQUEST_URI_ATTRIBUTE) != null) ||
305309
(request.getAttribute(WebUtils.ERROR_REQUEST_URI_ATTRIBUTE) != null)) {
306310
return (String) request.getAttribute(WebUtils.FORWARD_QUERY_STRING_ATTRIBUTE);
307311
}
@@ -333,21 +337,25 @@ private String decodeAndCleanUriString(HttpServletRequest request, String uri) {
333337
*/
334338
public String decodeRequestString(HttpServletRequest request, String source) {
335339
if (this.urlDecode) {
336-
String enc = determineEncoding(request);
337-
try {
338-
return UriUtils.decode(source, enc);
339-
}
340-
catch (UnsupportedEncodingException ex) {
341-
if (logger.isWarnEnabled()) {
342-
logger.warn("Could not decode request string [" + source + "] with encoding '" + enc +
343-
"': falling back to platform default encoding; exception message: " + ex.getMessage());
344-
}
345-
return URLDecoder.decode(source);
346-
}
340+
return decodeInternal(request, source);
347341
}
348342
return source;
349343
}
350344

345+
private String decodeInternal(HttpServletRequest request, String source) {
346+
String enc = determineEncoding(request);
347+
try {
348+
return UriUtils.decode(source, enc);
349+
}
350+
catch (UnsupportedEncodingException ex) {
351+
if (logger.isWarnEnabled()) {
352+
logger.warn("Could not decode request string [" + source + "] with encoding '" + enc +
353+
"': falling back to platform default encoding; exception message: " + ex.getMessage());
354+
}
355+
return URLDecoder.decode(source);
356+
}
357+
}
358+
351359
/**
352360
* Determine the encoding for the given request.
353361
* Can be overridden in subclasses.
@@ -366,6 +374,27 @@ protected String determineEncoding(HttpServletRequest request) {
366374
return enc;
367375
}
368376

377+
/**
378+
* Decode the given URI path variables via {@link #decodeRequestString(HttpServletRequest, String)}
379+
* unless {@link #setUrlDecode(boolean)} is set to {@code true} in which case
380+
* it is assumed the URL path from which the variables were extracted is
381+
* already decoded through a call to {@link #getLookupPathForRequest(HttpServletRequest)}.
382+
* @param request current HTTP request
383+
* @param vars URI variables extracted from the URL path
384+
* @return the same Map or a new Map instance
385+
*/
386+
public Map<String, String> decodePathVariables(HttpServletRequest request, Map<String, String> vars) {
387+
if (this.urlDecode) {
388+
return vars;
389+
}
390+
else {
391+
Map<String, String> decodedVars = new LinkedHashMap<String, String>(vars.size());
392+
for (Entry<String, String> entry : vars.entrySet()) {
393+
decodedVars.put(entry.getKey(), decodeInternal(request, entry.getValue()));
394+
}
395+
return decodedVars;
396+
}
397+
}
369398

370399
private boolean shouldRemoveTrailingServletPathSlash(HttpServletRequest request) {
371400
if (request.getAttribute(WEBSPHERE_URI_ATTRIBUTE) == null) {

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,10 @@ protected void handleMatch(RequestMappingInfo info, String lookupPath, HttpServl
8888
Set<String> patterns = info.getPatternsCondition().getPatterns();
8989
String bestPattern = patterns.isEmpty() ? lookupPath : patterns.iterator().next();
9090
request.setAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE, bestPattern);
91-
92-
Map<String, String> uriTemplateVariables = getPathMatcher().extractUriTemplateVariables(bestPattern, lookupPath);
93-
request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVariables);
91+
92+
Map<String, String> vars = getPathMatcher().extractUriTemplateVariables(bestPattern, lookupPath);
93+
Map<String, String> decodedVars = getUrlPathHelper().decodePathVariables(request, vars);
94+
request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, decodedVars);
9495

9596
if (!info.getProducesCondition().getProducibleMediaTypes().isEmpty()) {
9697
Set<MediaType> mediaTypes = info.getProducesCondition().getProducibleMediaTypes();

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

Lines changed: 40 additions & 16 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.
@@ -131,7 +131,7 @@ public void bestMatch() throws Exception {
131131
HandlerMethod hm = (HandlerMethod) this.mapping.getHandler(request).getHandler();
132132
assertEquals(this.fooParamMethod.getMethod(), hm.getMethod());
133133
}
134-
134+
135135
@Test
136136
public void requestMethodNotAllowed() throws Exception {
137137
try {
@@ -143,7 +143,7 @@ public void requestMethodNotAllowed() throws Exception {
143143
assertArrayEquals("Invalid supported methods", new String[]{"GET", "HEAD"}, ex.getSupportedMethods());
144144
}
145145
}
146-
146+
147147
@Test
148148
public void mediaTypeNotSupported() throws Exception {
149149
testMediaTypeNotSupported("/person/1");
@@ -159,11 +159,11 @@ private void testMediaTypeNotSupported(String url) throws Exception {
159159
fail("HttpMediaTypeNotSupportedException expected");
160160
}
161161
catch (HttpMediaTypeNotSupportedException ex) {
162-
assertEquals("Invalid supported consumable media types",
162+
assertEquals("Invalid supported consumable media types",
163163
Arrays.asList(new MediaType("application", "xml")), ex.getSupportedMediaTypes());
164164
}
165165
}
166-
166+
167167
@Test
168168
public void mediaTypeNotAccepted() throws Exception {
169169
testMediaTypeNotAccepted("/persons");
@@ -179,7 +179,7 @@ private void testMediaTypeNotAccepted(String url) throws Exception {
179179
fail("HttpMediaTypeNotAcceptableException expected");
180180
}
181181
catch (HttpMediaTypeNotAcceptableException ex) {
182-
assertEquals("Invalid supported producible media types",
182+
assertEquals("Invalid supported producible media types",
183183
Arrays.asList(new MediaType("application", "xml")), ex.getSupportedMediaTypes());
184184
}
185185
}
@@ -191,25 +191,49 @@ public void uriTemplateVariables() {
191191
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/1/2");
192192
String lookupPath = new UrlPathHelper().getLookupPathForRequest(request);
193193
this.mapping.handleMatch(key, lookupPath, request);
194-
194+
195195
@SuppressWarnings("unchecked")
196-
Map<String, String> uriVariables =
196+
Map<String, String> uriVariables =
197197
(Map<String, String>) request.getAttribute(
198198
HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
199-
199+
200200
assertNotNull(uriVariables);
201201
assertEquals("1", uriVariables.get("path1"));
202202
assertEquals("2", uriVariables.get("path2"));
203203
}
204204

205+
// SPR-9098
206+
207+
@Test
208+
public void uriTemplateVariablesDecode() {
209+
PatternsRequestCondition patterns = new PatternsRequestCondition("/{group}/{identifier}");
210+
RequestMappingInfo key = new RequestMappingInfo(patterns, null, null, null, null, null, null);
211+
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/group/a%2Fb");
212+
213+
UrlPathHelper pathHelper = new UrlPathHelper();
214+
pathHelper.setUrlDecode(false);
215+
String lookupPath = pathHelper.getLookupPathForRequest(request);
216+
217+
this.mapping.setUrlPathHelper(pathHelper);
218+
this.mapping.handleMatch(key, lookupPath, request);
219+
220+
@SuppressWarnings("unchecked")
221+
Map<String, String> uriVariables =
222+
(Map<String, String>) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
223+
224+
assertNotNull(uriVariables);
225+
assertEquals("group", uriVariables.get("group"));
226+
assertEquals("a/b", uriVariables.get("identifier"));
227+
}
228+
205229
@Test
206230
public void bestMatchingPatternAttribute() {
207231
PatternsRequestCondition patterns = new PatternsRequestCondition("/{path1}/2", "/**");
208232
RequestMappingInfo key = new RequestMappingInfo(patterns, null, null, null, null, null, null);
209233
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/1/2");
210234

211235
this.mapping.handleMatch(key, "/1/2", request);
212-
236+
213237
assertEquals("/{path1}/2", request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE));
214238
}
215239

@@ -220,7 +244,7 @@ public void bestMatchingPatternAttributeNoPatternsDefined() {
220244
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/1/2");
221245

222246
this.mapping.handleMatch(key, "/1/2", request);
223-
247+
224248
assertEquals("/1/2", request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE));
225249
}
226250

@@ -237,10 +261,10 @@ public void producibleMediaTypesAttribute() throws Exception {
237261
request.addHeader("Accept", "application/json");
238262
this.mapping.getHandler(request);
239263

240-
assertNull("Negated expression should not be listed as a producible type",
264+
assertNull("Negated expression should not be listed as a producible type",
241265
request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE));
242266
}
243-
267+
244268
@Test
245269
public void mappedInterceptors() throws Exception {
246270
String path = "/foo";
@@ -268,7 +292,7 @@ private static class Handler {
268292
@RequestMapping(value = "/foo", method = RequestMethod.GET)
269293
public void foo() {
270294
}
271-
295+
272296
@RequestMapping(value = "/foo", method = RequestMethod.GET, params="p")
273297
public void fooParam() {
274298
}
@@ -306,7 +330,7 @@ private static class TestRequestMappingInfoHandlerMapping extends RequestMapping
306330
public void registerHandler(Object handler) {
307331
super.detectHandlerMethods(handler);
308332
}
309-
333+
310334
@Override
311335
protected boolean isHandler(Class<?> beanType) {
312336
return AnnotationUtils.findAnnotation(beanType, RequestMapping.class) != null;
@@ -329,5 +353,5 @@ protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handler
329353
}
330354
}
331355
}
332-
356+
333357
}

src/dist/changelog.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Changes in version 3.2 M1
2727
* add CompositeRequestCondition for use with multiple custom request mapping conditions
2828
* add ability to configure custom MessageCodesResolver through the MVC Java config
2929
* add option in MappingJacksonJsonView for setting the Content-Length header
30+
* decode path variables when url decoding is turned off in AbstractHandlerMapping
3031

3132
Changes in version 3.1.1 (2012-02-16)
3233
-------------------------------------

0 commit comments

Comments
 (0)