diff --git a/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java b/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java index 5f543823dff..fa5fda06d87 100644 --- a/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java +++ b/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java @@ -22,11 +22,11 @@ import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.regex.Pattern; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -631,12 +631,26 @@ final class HierarchicalUriComponents extends UriComponents { */ static final class FullPathComponent implements PathComponent { + // Pattern used to split the path into segments: '/' if not preceded by '{', + // using negative look-behind + private static final Pattern DELIMITER_PATTERN = Pattern.compile("(? partialPaths; + private final String path; + public FullPathComponent(String path) { + this.partialPaths = PartialPath.parse(path); this.path = path; } + private FullPathComponent(List partialPaths) { + this.partialPaths = partialPaths; + this.path = PartialPath.getPath(partialPaths); + } + @Override public String getPath() { return this.path; @@ -644,25 +658,32 @@ final class HierarchicalUriComponents extends UriComponents { @Override public List getPathSegments() { - String delimiter = new String(new char[]{PATH_DELIMITER}); - String[] pathSegments = StringUtils.tokenizeToStringArray(path, delimiter); - return Collections.unmodifiableList(Arrays.asList(pathSegments)); + String[] pathSegments = DELIMITER_PATTERN.split(getPath()); + List result = new ArrayList(pathSegments.length); + for (String pathSegment : pathSegments) { + if (StringUtils.hasLength(pathSegment)) { + result.add(pathSegment); + } + } + return Collections.unmodifiableList(result); } @Override public PathComponent encode(String encoding) throws UnsupportedEncodingException { - String encodedPath = encodeUriComponent(getPath(),encoding, Type.PATH); + List encodedPath = + PartialPath.encode(this.partialPaths, encoding); return new FullPathComponent(encodedPath); } @Override public void verify() { - verifyUriComponent(this.path, Type.PATH); + verifyUriComponent(getPath(), Type.PATH); } @Override public PathComponent expand(UriTemplateVariables uriVariables) { - String expandedPath = expandUriComponent(getPath(), uriVariables); + List expandedPath = + PartialPath.expand(this.partialPaths, uriVariables); return new FullPathComponent(expandedPath); } @@ -681,6 +702,97 @@ final class HierarchicalUriComponents extends UriComponents { public int hashCode() { return getPath().hashCode(); } + + /** + * Represents a part of the full path, with a separate encoding type. + * Required because of {/...} uri variables, which need to encoded as PATH_SEGMENT + * rather than PATH. + */ + static final class PartialPath implements Serializable { + + final String value; + + final Type type; + + private PartialPath(String value, Type type) { + Assert.hasLength(value); + Assert.isTrue(Type.PATH == type || Type.PATH_SEGMENT == type); + this.value = value; + this.type = type; + } + + private PartialPath expand(UriTemplateVariables uriVariables) { + String expandedValue = expandUriComponent(this.value, uriVariables); + return new PartialPath(expandedValue, this.type); + } + + private PartialPath encode(String encoding) + throws UnsupportedEncodingException { + String encodedPath = encodeUriComponent(this.value, encoding, this.type); + return new PartialPath(encodedPath, this.type); + } + + @Override + public String toString() { + return value; + } + + public static List parse(String path) { + List result = new ArrayList(); + int startIdx; + int endIdx = 0; + while ((startIdx = path.indexOf("{/", endIdx)) != -1) { + if (startIdx > endIdx) { + String prevPart = path.substring(endIdx, startIdx); + result.add(new PartialPath(prevPart, Type.PATH)); + } + endIdx = path.indexOf('}', startIdx + 2) + 1; + if (endIdx == -1) { + throw new IllegalArgumentException("Path \"" + path + "\" has no " + + "closing \"}\" after \"{/\" at index " + startIdx); + } + String part = path.substring(startIdx, endIdx); + result.add(new PartialPath(part, Type.PATH_SEGMENT)); + } + if (endIdx < path.length()) { + String endPart = path.substring(endIdx); + result.add(new PartialPath(endPart, Type.PATH)); + } + return Collections.unmodifiableList(result); + } + + public static String getPath(List partialPaths) { + StringBuilder builder = new StringBuilder(); + for (PartialPath partialPath : partialPaths) { + builder.append(partialPath.value); + } + return builder.toString(); + } + + public static List expand( + List partialPaths, + UriTemplateVariables uriVariables) { + List result = new ArrayList(); + for (PartialPath partialPath : partialPaths) { + PartialPath expanded = partialPath.expand(uriVariables); + result.add(expanded); + } + return result; + } + + public static List encode( + List partialPaths, String encoding) + throws UnsupportedEncodingException { + List result = new ArrayList(); + for (PartialPath partialPath : partialPaths) { + PartialPath encoded = partialPath.encode(encoding); + result.add(encoded); + } + return result; + } + + } + } diff --git a/spring-web/src/main/java/org/springframework/web/util/UriComponents.java b/spring-web/src/main/java/org/springframework/web/util/UriComponents.java index 844267da96d..1c3c0f2e799 100644 --- a/spring-web/src/main/java/org/springframework/web/util/UriComponents.java +++ b/spring-web/src/main/java/org/springframework/web/util/UriComponents.java @@ -45,7 +45,7 @@ public abstract class UriComponents implements Serializable { private static final String DEFAULT_ENCODING = "UTF-8"; /** Captures URI template variable names. */ - private static final Pattern NAMES_PATTERN = Pattern.compile("\\{([^/]+?)\\}"); + private static final Pattern NAMES_PATTERN = Pattern.compile("\\{(/?[^/]+?)\\}"); private final String scheme; @@ -237,6 +237,9 @@ public abstract class UriComponents implements Serializable { } private static String getVariableName(String match) { + if (match.length() > 0 && match.charAt(0) == '/') { + match = match.substring(1); + } int colonIdx = match.indexOf(':'); return (colonIdx != -1 ? match.substring(0, colonIdx) : match); } diff --git a/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java b/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java index b5ee2eba692..791552db2da 100644 --- a/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java @@ -27,6 +27,7 @@ import static org.junit.Assert.assertTrue; import java.net.URI; import java.net.URISyntaxException; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -238,6 +239,24 @@ public class UriComponentsBuilderTests { assertEquals("bar@baz", result.getQueryParams().getFirst("foo")); } + //SPR-12750 + + @Test + public void fromUriStringWithSlashPrefixedVariable() { + UriComponents result = UriComponentsBuilder.fromUriString( + "http://example.com/part1/{/part2}/{var1}/url/{/urlvar}?foo=bar@baz&bar={barvalue}") + .build(); + assertTrue(StringUtils.isEmpty(result.getUserInfo())); + assertEquals("example.com", result.getHost()); + assertEquals("/part1/{/part2}/{var1}/url/{/urlvar}", result.getPath()); + assertEquals(Arrays.asList("part1", "{/part2}", "{var1}", "url", "{/urlvar}"), + result.getPathSegments()); + assertTrue(result.getQueryParams().containsKey("foo")); + assertEquals("bar@baz", result.getQueryParams().getFirst("foo")); + assertTrue(result.getQueryParams().containsKey("bar")); + assertEquals("{barvalue}", result.getQueryParams().getFirst("bar")); + } + @Test public void fromHttpRequest() throws URISyntaxException { MockHttpServletRequest request = new MockHttpServletRequest(); @@ -496,7 +515,7 @@ public class UriComponentsBuilderTests { UriComponents result = builder.build(); assertEquals("/foo/", result.getPath()); - assertEquals(Arrays.asList("foo"), result.getPathSegments()); + assertEquals(Collections.singletonList("foo"), result.getPathSegments()); } @Test diff --git a/spring-web/src/test/java/org/springframework/web/util/UriComponentsTests.java b/spring-web/src/test/java/org/springframework/web/util/UriComponentsTests.java index 7235609a8d7..ea89d04a6c5 100644 --- a/spring-web/src/test/java/org/springframework/web/util/UriComponentsTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/UriComponentsTests.java @@ -23,6 +23,7 @@ import java.io.ObjectOutputStream; import java.net.URI; import java.net.URISyntaxException; import java.util.Arrays; +import java.util.List; import org.junit.Test; @@ -165,4 +166,49 @@ public class UriComponentsTests { assertThat(uriComponents1, not(equalTo(uriComponents3))); } + @Test + public void partialPath() throws Exception { + List l; + + l = HierarchicalUriComponents.FullPathComponent.PartialPath.parse("x"); + assertEquals(1, l.size()); + assertEquals("x", l.get(0).value); + assertEquals(HierarchicalUriComponents.Type.PATH, l.get(0).type); + + l = HierarchicalUriComponents.FullPathComponent.PartialPath.parse("/foo"); + assertEquals(1, l.size()); + assertEquals("/foo", l.get(0).value); + assertEquals(HierarchicalUriComponents.Type.PATH, l.get(0).type); + + l = HierarchicalUriComponents.FullPathComponent.PartialPath.parse("{/foo}"); + assertEquals(1, l.size()); + assertEquals("{/foo}", l.get(0).value); + assertEquals(HierarchicalUriComponents.Type.PATH_SEGMENT, l.get(0).type); + + l = HierarchicalUriComponents.FullPathComponent.PartialPath.parse("/foo{/bar}"); + assertEquals(2, l.size()); + assertEquals("/foo", l.get(0).value); + assertEquals(HierarchicalUriComponents.Type.PATH, l.get(0).type); + assertEquals("{/bar}", l.get(1).value); + assertEquals(HierarchicalUriComponents.Type.PATH_SEGMENT, l.get(1).type); + + l = HierarchicalUriComponents.FullPathComponent.PartialPath.parse("{/foo}{/bar}"); + assertEquals(2, l.size()); + assertEquals("{/foo}", l.get(0).value); + assertEquals(HierarchicalUriComponents.Type.PATH_SEGMENT, l.get(0).type); + assertEquals("{/bar}", l.get(1).value); + assertEquals(HierarchicalUriComponents.Type.PATH_SEGMENT, l.get(1).type); + + l = HierarchicalUriComponents.FullPathComponent.PartialPath.parse("foo{/bar}baz"); + assertEquals(3, l.size()); + assertEquals("foo", l.get(0).value); + assertEquals(HierarchicalUriComponents.Type.PATH, l.get(0).type); + assertEquals("{/bar}", l.get(1).value); + assertEquals(HierarchicalUriComponents.Type.PATH_SEGMENT, l.get(1).type); + assertEquals("baz", l.get(2).value); + assertEquals(HierarchicalUriComponents.Type.PATH, l.get(2).type); + + + } + } diff --git a/spring-web/src/test/java/org/springframework/web/util/UriTemplateTests.java b/spring-web/src/test/java/org/springframework/web/util/UriTemplateTests.java index ebd3cea2174..802dd25ed02 100644 --- a/spring-web/src/test/java/org/springframework/web/util/UriTemplateTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/UriTemplateTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2015 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. @@ -63,6 +63,49 @@ public class UriTemplateTests { assertEquals("Invalid expanded template", new URI("http://example.com/hotels/1/bookings/42"), result); } + //SPR-12750 + + @Test + public void expandSlashPrefixedVariable() throws Exception { + Map uriVariables = new HashMap(2); + uriVariables.put("hotel", "1"); + uriVariables.put("publicpath", "pics/logo.png"); + uriVariables.put("scale", "150x150"); + UriTemplate template = new UriTemplate( + "http://example.com/hotels/{hotel}/pic/{/publicpath}/size/{scale}"); + URI result = template.expand(uriVariables); + assertEquals("Invalid expanded template", + new URI("http://example.com/hotels/1/pic/pics%2Flogo.png/size/150x150"), + result); + } + + @Test + public void expandSlashPrefixedVariableInBetween() throws Exception { + Map uriVariables = new HashMap(2); + uriVariables.put("var1", "foo/bar"); + uriVariables.put("var2", "baz"); + UriTemplate template = new UriTemplate( + "http://example.com/part1/before-{/var1}-after/{var2}"); + URI result = template.expand(uriVariables); + assertEquals("Invalid expanded template", + new URI("http://example.com/part1/before-foo%2Fbar-after/baz"), + result); + } + + @Test + public void expandSlashPrefixedVariableAfterNonPrefixedVariable() throws Exception { + Map uriVariables = new HashMap(2); + uriVariables.put("var1", "foo/bar"); + uriVariables.put("var2", "baz"); + uriVariables.put("var3", "qux"); + UriTemplate template = new UriTemplate( + "http://example.com/part1/before-{/var1}-{var2}-after/{var3}"); + URI result = template.expand(uriVariables); + assertEquals("Invalid expanded template", + new URI("http://example.com/part1/before-foo%2Fbar-baz-after/qux"), + result); + } + @Test public void expandMapDuplicateVariables() throws Exception { UriTemplate template = new UriTemplate("/order/{c}/{c}/{c}");