diff --git a/spring-web/src/main/java/org/springframework/web/util/DefaultUriTemplateHandler.java b/spring-web/src/main/java/org/springframework/web/util/DefaultUriTemplateHandler.java index 00d62a624ad..3eb27c3009a 100644 --- a/spring-web/src/main/java/org/springframework/web/util/DefaultUriTemplateHandler.java +++ b/spring-web/src/main/java/org/springframework/web/util/DefaultUriTemplateHandler.java @@ -16,8 +16,10 @@ package org.springframework.web.util; +import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -36,6 +38,8 @@ public class DefaultUriTemplateHandler implements UriTemplateHandler { private boolean parsePath; + private boolean strictEncoding; + /** * Configure a base URL to prepend URI templates with. The base URL must @@ -83,19 +87,45 @@ public class DefaultUriTemplateHandler implements UriTemplateHandler { return this.parsePath; } + /** + * Whether to encode characters outside the unreserved set as defined in + * RFC 3986 Section 2. + * This ensures a URI variable value will not contain any characters with a + * reserved purpose. + *

By default this is set to {@code false} in which case only characters + * illegal for the given URI component are encoded. For example when expanding + * a URI variable into a path segment the "/" character is illegal and + * encoded. The ";" character however is legal and not encoded even though + * it has a reserved purpose. + *

Note: this property supersedes the need to also set + * the {@link #setParsePath parsePath} property. + * @param strictEncoding whether to perform strict encoding + * @since 4.3 + */ + public void setStrictEncoding(boolean strictEncoding) { + this.strictEncoding = strictEncoding; + } + + /** + * Whether to strictly encode any character outside the unreserved set. + */ + public boolean isStrictEncoding() { + return this.strictEncoding; + } + @Override public URI expand(String uriTemplate, Map uriVariables) { - UriComponentsBuilder builder = initUriComponentsBuilder(uriTemplate); - UriComponents url = builder.build().expand(uriVariables).encode(); - return insertBaseUrl(url); + UriComponentsBuilder uriComponentsBuilder = initUriComponentsBuilder(uriTemplate); + UriComponents uriComponents = expandAndEncode(uriComponentsBuilder, uriVariables); + return insertBaseUrl(uriComponents); } @Override public URI expand(String uriTemplate, Object... uriVariables) { - UriComponentsBuilder builder = initUriComponentsBuilder(uriTemplate); - UriComponents url = builder.build().expand(uriVariables).encode(); - return insertBaseUrl(url); + UriComponentsBuilder uriComponentsBuilder = initUriComponentsBuilder(uriTemplate); + UriComponents uriComponents = expandAndEncode(uriComponentsBuilder, uriVariables); + return insertBaseUrl(uriComponents); } /** @@ -105,7 +135,7 @@ public class DefaultUriTemplateHandler implements UriTemplateHandler { */ protected UriComponentsBuilder initUriComponentsBuilder(String uriTemplate) { UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(uriTemplate); - if (shouldParsePath()) { + if (shouldParsePath() && !isStrictEncoding()) { List pathSegments = builder.build().getPathSegments(); builder.replacePath(null); for (String pathSegment : pathSegments) { @@ -115,6 +145,43 @@ public class DefaultUriTemplateHandler implements UriTemplateHandler { return builder; } + protected UriComponents expandAndEncode(UriComponentsBuilder builder, Map uriVariables) { + if (!isStrictEncoding()) { + return builder.build().expand(uriVariables).encode(); + } + else { + Map encodedUriVars = new HashMap(uriVariables.size()); + for (Map.Entry entry : uriVariables.entrySet()) { + encodedUriVars.put(entry.getKey(), encodeValue(entry.getValue())); + } + return builder.build().expand(encodedUriVars); + } + } + + protected UriComponents expandAndEncode(UriComponentsBuilder builder, Object[] uriVariables) { + if (!isStrictEncoding()) { + return builder.build().expand(uriVariables).encode(); + } + else { + Object[] encodedUriVars = new Object[uriVariables.length]; + for (int i = 0; i < uriVariables.length; i++) { + encodedUriVars[i] = encodeValue(uriVariables[i]); + } + return builder.build().expand(encodedUriVars); + } + } + + private String encodeValue(Object value) { + String stringValue = (value != null ? value.toString() : ""); + try { + return UriUtils.encode(stringValue, "UTF-8"); + } + catch (UnsupportedEncodingException ex) { + // Should never happen + throw new IllegalStateException("Failed to encode URI variable", ex); + } + } + /** * Invoked after the URI template has been expanded and encoded to prepend * the configured {@link #setBaseUrl(String) baseUrl} if any. @@ -122,10 +189,10 @@ public class DefaultUriTemplateHandler implements UriTemplateHandler { * @return the final URI */ protected URI insertBaseUrl(UriComponents uriComponents) { - if (getBaseUrl() == null || uriComponents.getHost() != null) { - return uriComponents.toUri(); + String url = uriComponents.toUriString(); + if (getBaseUrl() != null && uriComponents.getHost() == null) { + url = getBaseUrl() + url; } - String url = getBaseUrl() + uriComponents.toUriString(); try { return new URI(url); } diff --git a/spring-web/src/test/java/org/springframework/web/util/DefaultUriTemplateHandlerTests.java b/spring-web/src/test/java/org/springframework/web/util/DefaultUriTemplateHandlerTests.java index dd6c67c3d50..691d6da285f 100644 --- a/spring-web/src/test/java/org/springframework/web/util/DefaultUriTemplateHandlerTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/DefaultUriTemplateHandlerTests.java @@ -49,7 +49,8 @@ public class DefaultUriTemplateHandlerTests { } @Test - public void expandWithFullPath() throws Exception { + public void parsePathOff() throws Exception { + this.handler.setParsePath(false); Map vars = new HashMap<>(2); vars.put("hotel", "1"); vars.put("publicpath", "pics/logo.png"); @@ -60,7 +61,7 @@ public class DefaultUriTemplateHandlerTests { } @Test - public void expandWithFullPathAndParsePathEnabled() throws Exception { + public void parsePathOn() throws Exception { this.handler.setParsePath(true); Map vars = new HashMap<>(2); vars.put("hotel", "1"); @@ -72,4 +73,35 @@ public class DefaultUriTemplateHandlerTests { assertEquals(expected, actual); } + @Test + public void strictEncodingOff() throws Exception { + this.handler.setStrictEncoding(false); + Map vars = new HashMap<>(2); + vars.put("userId", "john;doe"); + String template = "http://www.example.com/user/{userId}/dashboard"; + URI actual = this.handler.expand(template, vars); + URI expected = new URI("http://www.example.com/user/john;doe/dashboard"); + assertEquals(expected, actual); + } + + @Test + public void strictEncodingOnWithMap() throws Exception { + this.handler.setStrictEncoding(true); + Map vars = new HashMap<>(2); + vars.put("userId", "john;doe"); + String template = "http://www.example.com/user/{userId}/dashboard"; + URI actual = this.handler.expand(template, vars); + URI expected = new URI("http://www.example.com/user/john%3Bdoe/dashboard"); + assertEquals(expected, actual); + } + + @Test + public void strictEncodingOnWithArray() throws Exception { + this.handler.setStrictEncoding(true); + String template = "http://www.example.com/user/{userId}/dashboard"; + URI actual = this.handler.expand(template, "john;doe"); + URI expected = new URI("http://www.example.com/user/john%3Bdoe/dashboard"); + assertEquals(expected, actual); + } + }