Support {/...} patterns in UriComponents(Builder)
This commit introduces support for "Path Segment URI Variable expansion", see https://tools.ietf.org/html/rfc6570#section-3.2.6. In practice, this means that URI template variables prefixed with a '/' are treated like path segments and - as such - will encode any '/' found. For example: {/foo} expanded with "bar/baz" with result in "bar%2F". Issue: SPR-12750
This commit is contained in:
parent
f926f6cb3e
commit
7668ea1549
|
|
@ -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("(?<!\\{)/");
|
||||
|
||||
|
||||
private final List<PartialPath> partialPaths;
|
||||
|
||||
private final String path;
|
||||
|
||||
|
||||
public FullPathComponent(String path) {
|
||||
this.partialPaths = PartialPath.parse(path);
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
private FullPathComponent(List<PartialPath> 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<String> 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<String> result = new ArrayList<String>(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<PartialPath> 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<PartialPath> 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<PartialPath> parse(String path) {
|
||||
List<PartialPath> result = new ArrayList<PartialPath>();
|
||||
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<PartialPath> partialPaths) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (PartialPath partialPath : partialPaths) {
|
||||
builder.append(partialPath.value);
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
public static List<PartialPath> expand(
|
||||
List<PartialPath> partialPaths,
|
||||
UriTemplateVariables uriVariables) {
|
||||
List<PartialPath> result = new ArrayList<PartialPath>();
|
||||
for (PartialPath partialPath : partialPaths) {
|
||||
PartialPath expanded = partialPath.expand(uriVariables);
|
||||
result.add(expanded);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static List<PartialPath> encode(
|
||||
List<PartialPath> partialPaths, String encoding)
|
||||
throws UnsupportedEncodingException {
|
||||
List<PartialPath> result = new ArrayList<PartialPath>();
|
||||
for (PartialPath partialPath : partialPaths) {
|
||||
PartialPath encoded = partialPath.encode(encoding);
|
||||
result.add(encoded);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<HierarchicalUriComponents.FullPathComponent.PartialPath> 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);
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String, String> uriVariables = new HashMap<String, String>(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<String, String> uriVariables = new HashMap<String, String>(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<String, String> uriVariables = new HashMap<String, String>(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}");
|
||||
|
|
|
|||
Loading…
Reference in New Issue