Protect against '//' in UriComponentsBuilder

Refactor UriComponentsBuilder to ensure that paths do not contain empty
segments.

For example, prior to this commit:

    fromUriString("http://example.com/abc/").path("/x/y/z")

would build the URL "http://example.com/abc//x/y/z" where as it will
now build "http://example.com/abc/x/y/z".

Issue: SPR-10270
This commit is contained in:
Phillip Webb 2013-02-14 21:09:28 -08:00
parent 953b2b60ad
commit 6e5cb7fbcd
2 changed files with 108 additions and 113 deletions

View File

@ -18,7 +18,7 @@ package org.springframework.web.util;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
@ -29,6 +29,7 @@ import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.util.HierarchicalUriComponents.PathComponent;
/**
* Builder for {@link UriComponents}.
@ -46,6 +47,7 @@ import org.springframework.util.StringUtils;
*
* @author Arjen Poutsma
* @author Rossen Stoyanchev
* @author Phillip Webb
* @since 3.1
* @see #newInstance()
* @see #fromPath(String)
@ -91,7 +93,7 @@ public class UriComponentsBuilder {
private int port = -1;
private PathComponentBuilder pathBuilder = NULL_PATH_COMPONENT_BUILDER;
private CompositePathComponentBuilder pathBuilder = new CompositePathComponentBuilder();
private final MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<String, String>();
@ -334,7 +336,7 @@ public class UriComponentsBuilder {
this.port = uri.getPort();
}
if (StringUtils.hasLength(uri.getRawPath())) {
this.pathBuilder = new FullPathComponentBuilder(uri.getRawPath());
this.pathBuilder = new CompositePathComponentBuilder(uri.getRawPath());
}
if (StringUtils.hasLength(uri.getRawQuery())) {
this.queryParams.clear();
@ -352,7 +354,7 @@ public class UriComponentsBuilder {
this.userInfo = null;
this.host = null;
this.port = -1;
this.pathBuilder = NULL_PATH_COMPONENT_BUILDER;
this.pathBuilder = new CompositePathComponentBuilder();
this.queryParams.clear();
}
@ -436,12 +438,7 @@ public class UriComponentsBuilder {
* @return this UriComponentsBuilder
*/
public UriComponentsBuilder path(String path) {
if (path != null) {
this.pathBuilder = this.pathBuilder.appendPath(path);
}
else {
this.pathBuilder = NULL_PATH_COMPONENT_BUILDER;
}
this.pathBuilder.addPath(path);
resetSchemeSpecificPart();
return this;
}
@ -453,22 +450,21 @@ public class UriComponentsBuilder {
* @return this UriComponentsBuilder
*/
public UriComponentsBuilder replacePath(String path) {
this.pathBuilder = NULL_PATH_COMPONENT_BUILDER;
path(path);
this.pathBuilder = new CompositePathComponentBuilder(path);
resetSchemeSpecificPart();
return this;
}
/**
* Appends the given path segments to the existing path of this builder. Each given path segments may contain URI
* template variables.
* Appends the given path segments to the existing path of this builder. Each given
* path segments may contain URI template variables.
*
* @param pathSegments the URI path segments
* @return this UriComponentsBuilder
*/
public UriComponentsBuilder pathSegment(String... pathSegments) throws IllegalArgumentException {
Assert.notNull(pathSegments, "'segments' must not be null");
this.pathBuilder = this.pathBuilder.appendPathSegments(pathSegments);
this.pathBuilder.addPathSegments(pathSegments);
resetSchemeSpecificPart();
return this;
}
@ -590,131 +586,122 @@ public class UriComponentsBuilder {
return this;
}
/**
* Represents a builder for {@link HierarchicalUriComponents.PathComponent}
*/
private interface PathComponentBuilder {
HierarchicalUriComponents.PathComponent build();
PathComponentBuilder appendPath(String path);
PathComponentBuilder appendPathSegments(String... pathSegments);
PathComponent build();
}
/**
* Represents a builder for full string paths.
*/
private static class FullPathComponentBuilder implements PathComponentBuilder {
private static class CompositePathComponentBuilder implements PathComponentBuilder {
private final StringBuilder path;
private LinkedList<PathComponentBuilder> componentBuilders = new LinkedList<PathComponentBuilder>();
private FullPathComponentBuilder(String path) {
this.path = new StringBuilder(path);
public CompositePathComponentBuilder() {
}
public HierarchicalUriComponents.PathComponent build() {
return new HierarchicalUriComponents.FullPathComponent(path.toString());
public CompositePathComponentBuilder(String path) {
addPath(path);
}
public PathComponentBuilder appendPath(String path) {
this.path.append(path);
return this;
public void addPathSegments(String... pathSegments) {
if (!ObjectUtils.isEmpty(pathSegments)) {
PathSegmentComponentBuilder psBuilder = getLastBuilder(PathSegmentComponentBuilder.class);
FullPathComponentBuilder fpBuilder = getLastBuilder(FullPathComponentBuilder.class);
if (psBuilder == null) {
psBuilder = new PathSegmentComponentBuilder();
this.componentBuilders.add(psBuilder);
if (fpBuilder != null) {
fpBuilder.removeTrailingSlash();
}
}
psBuilder.append(pathSegments);
}
}
public PathComponentBuilder appendPathSegments(String... pathSegments) {
PathComponentCompositeBuilder builder = new PathComponentCompositeBuilder(this);
builder.appendPathSegments(pathSegments);
return builder;
}
}
/**
* Represents a builder for paths segment paths.
*/
private static class PathSegmentComponentBuilder implements PathComponentBuilder {
private final List<String> pathSegments = new ArrayList<String>();
private PathSegmentComponentBuilder(String... pathSegments) {
this.pathSegments.addAll(removeEmptyPathSegments(pathSegments));
public void addPath(String path) {
if (StringUtils.hasText(path)) {
PathSegmentComponentBuilder psBuilder = getLastBuilder(PathSegmentComponentBuilder.class);
FullPathComponentBuilder fpBuilder = getLastBuilder(FullPathComponentBuilder.class);
if (psBuilder != null) {
path = path.startsWith("/") ? path : "/" + path;
}
if (fpBuilder == null) {
fpBuilder = new FullPathComponentBuilder();
this.componentBuilders.add(fpBuilder);
}
fpBuilder.append(path);
}
}
private Collection<String> removeEmptyPathSegments(String... pathSegments) {
List<String> result = new ArrayList<String>();
for (String segment : pathSegments) {
if (StringUtils.hasText(segment)) {
result.add(segment);
@SuppressWarnings("unchecked")
private <T> T getLastBuilder(Class<T> builderClass) {
if (!this.componentBuilders.isEmpty()) {
PathComponentBuilder last = this.componentBuilders.getLast();
if (builderClass.isInstance(last)) {
return (T) last;
}
}
return result;
return null;
}
public HierarchicalUriComponents.PathComponent build() {
return new HierarchicalUriComponents.PathSegmentComponent(pathSegments);
}
public PathComponentBuilder appendPath(String path) {
PathComponentCompositeBuilder builder = new PathComponentCompositeBuilder(this);
builder.appendPath(path);
return builder;
}
public PathComponentBuilder appendPathSegments(String... pathSegments) {
this.pathSegments.addAll(removeEmptyPathSegments(pathSegments));
return this;
}
}
/**
* Represents a builder for a collection of PathComponents.
*/
private static class PathComponentCompositeBuilder implements PathComponentBuilder {
private final List<PathComponentBuilder> pathComponentBuilders = new ArrayList<PathComponentBuilder>();
private PathComponentCompositeBuilder(PathComponentBuilder builder) {
pathComponentBuilders.add(builder);
}
public HierarchicalUriComponents.PathComponent build() {
List<HierarchicalUriComponents.PathComponent> pathComponents =
new ArrayList<HierarchicalUriComponents.PathComponent>(pathComponentBuilders.size());
for (PathComponentBuilder pathComponentBuilder : pathComponentBuilders) {
pathComponents.add(pathComponentBuilder.build());
public PathComponent build() {
int size = this.componentBuilders.size();
List<PathComponent> components = new ArrayList<PathComponent>(size);
for (int i = 0; i < size; i++) {
PathComponent pathComponent = this.componentBuilders.get(i).build();
if (pathComponent != null) {
components.add(pathComponent);
}
}
return new HierarchicalUriComponents.PathComponentComposite(pathComponents);
}
public PathComponentBuilder appendPath(String path) {
this.pathComponentBuilders.add(new FullPathComponentBuilder(path));
return this;
}
public PathComponentBuilder appendPathSegments(String... pathSegments) {
this.pathComponentBuilders.add(new PathSegmentComponentBuilder(pathSegments));
return this;
if (components.isEmpty()) {
return HierarchicalUriComponents.NULL_PATH_COMPONENT;
}
if (components.size() == 1) {
return components.get(0);
}
return new HierarchicalUriComponents.PathComponentComposite(components);
}
}
private static class FullPathComponentBuilder implements PathComponentBuilder {
/**
* Represents a builder for an empty path.
*/
private static PathComponentBuilder NULL_PATH_COMPONENT_BUILDER = new PathComponentBuilder() {
private StringBuilder path = new StringBuilder();
public HierarchicalUriComponents.PathComponent build() {
return HierarchicalUriComponents.NULL_PATH_COMPONENT;
public void append(String path) {
this.path.append(path);
}
public PathComponentBuilder appendPath(String path) {
return new FullPathComponentBuilder(path);
public PathComponent build() {
if (this.path.length() == 0) {
return null;
}
String path = this.path.toString().replace("//", "/");
return new HierarchicalUriComponents.FullPathComponent(path);
}
public PathComponentBuilder appendPathSegments(String... pathSegments) {
return new PathSegmentComponentBuilder(pathSegments);
public void removeTrailingSlash() {
int index = this.path.length() - 1;
if (this.path.charAt(index) == '/') {
this.path.deleteCharAt(index);
}
}
};
}
private static class PathSegmentComponentBuilder implements PathComponentBuilder {
private List<String> pathSegments = new LinkedList<String>();
public void append(String... pathSegments) {
for (String pathSegment : pathSegments) {
if (StringUtils.hasText(pathSegment)) {
this.pathSegments.add(pathSegment);
}
}
}
public PathComponent build() {
return this.pathSegments.isEmpty() ?
null : new HierarchicalUriComponents.PathSegmentComponent(this.pathSegments);
}
}
}

View File

@ -346,4 +346,12 @@ public class UriComponentsBuilderTests {
assertThat(UriComponentsBuilder.fromUriString("http://example.com").path("foo/../bar").build().toUriString(), equalTo("http://example.com/foo/../bar"));
assertThat(UriComponentsBuilder.fromUriString("http://example.com").path("foo/../bar").build().toUri().getPath(), equalTo("/foo/../bar"));
}
@Test
public void emptySegments() throws Exception {
assertThat(UriComponentsBuilder.fromUriString("http://example.com/abc/").path("/x/y/z").build().toString(), equalTo("http://example.com/abc/x/y/z"));
assertThat(UriComponentsBuilder.fromUriString("http://example.com/abc/").pathSegment("x", "y", "z").build().toString(), equalTo("http://example.com/abc/x/y/z"));
assertThat(UriComponentsBuilder.fromUriString("http://example.com/abc/").path("/x/").path("/y/z").build().toString(), equalTo("http://example.com/abc/x/y/z"));
assertThat(UriComponentsBuilder.fromUriString("http://example.com/abc/").pathSegment("x").path("y").build().toString(), equalTo("http://example.com/abc/x/y"));
}
}