Backported "Support opaque URIs in UriComponentsBuilder"

Issue: SPR-9798
Issue: SPR-9804
This commit is contained in:
Juergen Hoeller
2012-10-10 17:03:27 +02:00
parent 0047200b98
commit be2c2e9a86
6 changed files with 1442 additions and 1023 deletions

View File

@@ -0,0 +1,836 @@
/*
* Copyright 2002-2012 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.util;
import java.io.ByteArrayOutputStream;
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 org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* Extension of {@link UriComponents} for hierarchical URIs.
*
* @author Arjen Poutsma
* @since 3.2
* @see <a href="http://tools.ietf.org/html/rfc3986#section-1.2.3">Hierarchical URIs</a>
*/
final class HierarchicalUriComponents extends UriComponents {
private static final char PATH_DELIMITER = '/';
private final String userInfo;
private final String host;
private final int port;
private final PathComponent path;
private final MultiValueMap<String, String> queryParams;
private final boolean encoded;
/**
* Package-private constructor. All arguments are optional, and can be {@code null}.
* @param scheme the scheme
* @param userInfo the user info
* @param host the host
* @param port the port
* @param path the path
* @param queryParams the query parameters
* @param fragment the fragment
* @param encoded whether the components are already encoded
* @param verify whether the components need to be checked for illegal characters
*/
HierarchicalUriComponents(String scheme, String userInfo, String host, int port, PathComponent path,
MultiValueMap<String, String> queryParams, String fragment, boolean encoded, boolean verify) {
super(scheme, fragment);
this.userInfo = userInfo;
this.host = host;
this.port = port;
this.path = path != null ? path : NULL_PATH_COMPONENT;
this.queryParams = CollectionUtils.unmodifiableMultiValueMap(
queryParams != null ? queryParams : new LinkedMultiValueMap<String, String>(0));
this.encoded = encoded;
if (verify) {
verify();
}
}
// component getters
@Override
public String getSchemeSpecificPart() {
return null;
}
@Override
public String getUserInfo() {
return this.userInfo;
}
@Override
public String getHost() {
return this.host;
}
@Override
public int getPort() {
return this.port;
}
@Override
public String getPath() {
return this.path.getPath();
}
@Override
public List<String> getPathSegments() {
return this.path.getPathSegments();
}
@Override
public String getQuery() {
if (!this.queryParams.isEmpty()) {
StringBuilder queryBuilder = new StringBuilder();
for (Map.Entry<String, List<String>> entry : this.queryParams.entrySet()) {
String name = entry.getKey();
List<String> values = entry.getValue();
if (CollectionUtils.isEmpty(values)) {
if (queryBuilder.length() != 0) {
queryBuilder.append('&');
}
queryBuilder.append(name);
}
else {
for (Object value : values) {
if (queryBuilder.length() != 0) {
queryBuilder.append('&');
}
queryBuilder.append(name);
if (value != null) {
queryBuilder.append('=');
queryBuilder.append(value.toString());
}
}
}
}
return queryBuilder.toString();
}
else {
return null;
}
}
/**
* Returns the map of query parameters. Empty if no query has been set.
*/
@Override
public MultiValueMap<String, String> getQueryParams() {
return this.queryParams;
}
// encoding
/**
* Encodes all URI components using their specific encoding rules, and returns the result as a new
* {@code UriComponents} instance.
* @param encoding the encoding of the values contained in this map
* @return the encoded uri components
* @throws UnsupportedEncodingException if the given encoding is not supported
*/
@Override
public HierarchicalUriComponents encode(String encoding) throws UnsupportedEncodingException {
Assert.hasLength(encoding, "'encoding' must not be empty");
if (this.encoded) {
return this;
}
String encodedScheme = encodeUriComponent(this.getScheme(), encoding, Type.SCHEME);
String encodedUserInfo = encodeUriComponent(this.userInfo, encoding, Type.USER_INFO);
String encodedHost = encodeUriComponent(this.host, encoding, Type.HOST);
PathComponent encodedPath = this.path.encode(encoding);
MultiValueMap<String, String> encodedQueryParams =
new LinkedMultiValueMap<String, String>(this.queryParams.size());
for (Map.Entry<String, List<String>> entry : this.queryParams.entrySet()) {
String encodedName = encodeUriComponent(entry.getKey(), encoding, Type.QUERY_PARAM);
List<String> encodedValues = new ArrayList<String>(entry.getValue().size());
for (String value : entry.getValue()) {
String encodedValue = encodeUriComponent(value, encoding, Type.QUERY_PARAM);
encodedValues.add(encodedValue);
}
encodedQueryParams.put(encodedName, encodedValues);
}
String encodedFragment = encodeUriComponent(this.getFragment(), encoding, Type.FRAGMENT);
return new HierarchicalUriComponents(encodedScheme, encodedUserInfo, encodedHost, this.port, encodedPath,
encodedQueryParams, encodedFragment, true, false);
}
/**
* Encodes the given source into an encoded String using the rules specified
* by the given component and with the given options.
* @param source the source string
* @param encoding the encoding of the source string
* @param type the URI component for the source
* @return the encoded URI
* @throws IllegalArgumentException when the given uri parameter is not a valid URI
*/
static String encodeUriComponent(String source, String encoding, Type type)
throws UnsupportedEncodingException {
if (source == null) {
return null;
}
Assert.hasLength(encoding, "'encoding' must not be empty");
byte[] bytes = encodeBytes(source.getBytes(encoding), type);
return new String(bytes, "US-ASCII");
}
private static byte[] encodeBytes(byte[] source, Type type) {
Assert.notNull(source, "'source' must not be null");
Assert.notNull(type, "'type' must not be null");
ByteArrayOutputStream bos = new ByteArrayOutputStream(source.length);
for (int i = 0; i < source.length; i++) {
int b = source[i];
if (b < 0) {
b += 256;
}
if (type.isAllowed(b)) {
bos.write(b);
}
else {
bos.write('%');
char hex1 = Character.toUpperCase(Character.forDigit((b >> 4) & 0xF, 16));
char hex2 = Character.toUpperCase(Character.forDigit(b & 0xF, 16));
bos.write(hex1);
bos.write(hex2);
}
}
return bos.toByteArray();
}
// verifying
/**
* Verifies all URI components to determine whether they contain any illegal
* characters, throwing an {@code IllegalArgumentException} if so.
* @throws IllegalArgumentException if any component has illegal characters
*/
private void verify() {
if (!this.encoded) {
return;
}
verifyUriComponent(getScheme(), Type.SCHEME);
verifyUriComponent(userInfo, Type.USER_INFO);
verifyUriComponent(host, Type.HOST);
this.path.verify();
for (Map.Entry<String, List<String>> entry : queryParams.entrySet()) {
verifyUriComponent(entry.getKey(), Type.QUERY_PARAM);
for (String value : entry.getValue()) {
verifyUriComponent(value, Type.QUERY_PARAM);
}
}
verifyUriComponent(getFragment(), Type.FRAGMENT);
}
private static void verifyUriComponent(String source, Type type) {
if (source == null) {
return;
}
int length = source.length();
for (int i=0; i < length; i++) {
char ch = source.charAt(i);
if (ch == '%') {
if ((i + 2) < length) {
char hex1 = source.charAt(i + 1);
char hex2 = source.charAt(i + 2);
int u = Character.digit(hex1, 16);
int l = Character.digit(hex2, 16);
if (u == -1 || l == -1) {
throw new IllegalArgumentException("Invalid encoded sequence \"" + source.substring(i) + "\"");
}
i += 2;
}
else {
throw new IllegalArgumentException("Invalid encoded sequence \"" + source.substring(i) + "\"");
}
}
else if (!type.isAllowed(ch)) {
throw new IllegalArgumentException(
"Invalid character '" + ch + "' for " + type.name() + " in \"" + source + "\"");
}
}
}
// expanding
@Override
protected HierarchicalUriComponents expandInternal(UriTemplateVariables uriVariables) {
Assert.state(!encoded, "Cannot expand an already encoded UriComponents object");
String expandedScheme = expandUriComponent(this.getScheme(), uriVariables);
String expandedUserInfo = expandUriComponent(this.userInfo, uriVariables);
String expandedHost = expandUriComponent(this.host, uriVariables);
PathComponent expandedPath = this.path.expand(uriVariables);
MultiValueMap<String, String> expandedQueryParams =
new LinkedMultiValueMap<String, String>(this.queryParams.size());
for (Map.Entry<String, List<String>> entry : this.queryParams.entrySet()) {
String expandedName = expandUriComponent(entry.getKey(), uriVariables);
List<String> expandedValues = new ArrayList<String>(entry.getValue().size());
for (String value : entry.getValue()) {
String expandedValue = expandUriComponent(value, uriVariables);
expandedValues.add(expandedValue);
}
expandedQueryParams.put(expandedName, expandedValues);
}
String expandedFragment = expandUriComponent(this.getFragment(), uriVariables);
return new HierarchicalUriComponents(expandedScheme, expandedUserInfo, expandedHost, this.port, expandedPath,
expandedQueryParams, expandedFragment, false, false);
}
/**
* Normalize the path removing sequences like "path/..".
* @see StringUtils#cleanPath(String)
*/
@Override
public UriComponents normalize() {
String normalizedPath = StringUtils.cleanPath(getPath());
return new HierarchicalUriComponents(getScheme(), this.userInfo, this.host,
this.port, new FullPathComponent(normalizedPath), this.queryParams,
getFragment(), this.encoded, false);
}
// other functionality
/**
* Returns a URI string from this {@code UriComponents} instance.
*/
@Override
public String toUriString() {
StringBuilder uriBuilder = new StringBuilder();
if (getScheme() != null) {
uriBuilder.append(getScheme());
uriBuilder.append(':');
}
if (this.userInfo != null || this.host != null) {
uriBuilder.append("//");
if (this.userInfo != null) {
uriBuilder.append(this.userInfo);
uriBuilder.append('@');
}
if (this.host != null) {
uriBuilder.append(host);
}
if (this.port != -1) {
uriBuilder.append(':');
uriBuilder.append(port);
}
}
String path = getPath();
if (StringUtils.hasLength(path)) {
if (uriBuilder.length() != 0 && path.charAt(0) != PATH_DELIMITER) {
uriBuilder.append(PATH_DELIMITER);
}
uriBuilder.append(path);
}
String query = getQuery();
if (query != null) {
uriBuilder.append('?');
uriBuilder.append(query);
}
if (getFragment() != null) {
uriBuilder.append('#');
uriBuilder.append(getFragment());
}
return uriBuilder.toString();
}
/**
* Returns a {@code URI} from this {@code UriComponents} instance.
*/
@Override
public URI toUri() {
try {
if (this.encoded) {
return new URI(toString());
}
else {
String path = getPath();
if (StringUtils.hasLength(path) && path.charAt(0) != PATH_DELIMITER) {
path = PATH_DELIMITER + path;
}
return new URI(getScheme(), getUserInfo(), getHost(), getPort(), path, getQuery(),
getFragment());
}
}
catch (URISyntaxException ex) {
throw new IllegalStateException("Could not create URI object: " + ex.getMessage(), ex);
}
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof OpaqueUriComponents)) {
return false;
}
HierarchicalUriComponents other = (HierarchicalUriComponents) obj;
if (ObjectUtils.nullSafeEquals(getScheme(), other.getScheme())) {
return false;
}
if (ObjectUtils.nullSafeEquals(getUserInfo(), other.getUserInfo())) {
return false;
}
if (ObjectUtils.nullSafeEquals(getHost(), other.getHost())) {
return false;
}
if (this.port != other.port) {
return false;
}
if (!this.path.equals(other.path)) {
return false;
}
if (!this.queryParams.equals(other.queryParams)) {
return false;
}
if (ObjectUtils.nullSafeEquals(getFragment(), other.getFragment())) {
return false;
}
return true;
}
@Override
public int hashCode() {
int result = ObjectUtils.nullSafeHashCode(getScheme());
result = 31 * result + ObjectUtils.nullSafeHashCode(this.userInfo);
result = 31 * result + ObjectUtils.nullSafeHashCode(this.host);
result = 31 * result + this.port;
result = 31 * result + this.path.hashCode();
result = 31 * result + this.queryParams.hashCode();
result = 31 * result + ObjectUtils.nullSafeHashCode(getFragment());
return result;
}
// inner types
/**
* Enumeration used to identify the parts of a URI.
* <p>Contains methods to indicate whether a given character is valid in a specific URI component.
* @see <a href="http://www.ietf.org/rfc/rfc3986.txt">RFC 3986</a>
*/
static enum Type {
SCHEME {
@Override
public boolean isAllowed(int c) {
return isAlpha(c) || isDigit(c) || '+' == c || '-' == c || '.' == c;
}
},
AUTHORITY {
@Override
public boolean isAllowed(int c) {
return isUnreserved(c) || isSubDelimiter(c) || ':' == c || '@' == c;
}
},
USER_INFO {
@Override
public boolean isAllowed(int c) {
return isUnreserved(c) || isSubDelimiter(c) || ':' == c;
}
},
HOST {
@Override
public boolean isAllowed(int c) {
return isUnreserved(c) || isSubDelimiter(c);
}
},
PORT {
@Override
public boolean isAllowed(int c) {
return isDigit(c);
}
},
PATH {
@Override
public boolean isAllowed(int c) {
return isPchar(c) || '/' == c;
}
},
PATH_SEGMENT {
@Override
public boolean isAllowed(int c) {
return isPchar(c);
}
},
QUERY {
@Override
public boolean isAllowed(int c) {
return isPchar(c) || '/' == c || '?' == c;
}
},
QUERY_PARAM {
@Override
public boolean isAllowed(int c) {
if ('=' == c || '+' == c || '&' == c) {
return false;
}
else {
return isPchar(c) || '/' == c || '?' == c;
}
}
},
FRAGMENT {
@Override
public boolean isAllowed(int c) {
return isPchar(c) || '/' == c || '?' == c;
}
};
/**
* Indicates whether the given character is allowed in this URI component.
*
* @param c the character
* @return {@code true} if the character is allowed; {@code false} otherwise
*/
public abstract boolean isAllowed(int c);
/**
* Indicates whether the given character is in the {@code ALPHA} set.
*
* @see <a href="http://www.ietf.org/rfc/rfc3986.txt">RFC 3986, appendix A</a>
*/
protected boolean isAlpha(int c) {
return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z';
}
/**
* Indicates whether the given character is in the {@code DIGIT} set.
*
* @see <a href="http://www.ietf.org/rfc/rfc3986.txt">RFC 3986, appendix A</a>
*/
protected boolean isDigit(int c) {
return c >= '0' && c <= '9';
}
/**
* Indicates whether the given character is in the {@code gen-delims} set.
*
* @see <a href="http://www.ietf.org/rfc/rfc3986.txt">RFC 3986, appendix A</a>
*/
protected boolean isGenericDelimiter(int c) {
return ':' == c || '/' == c || '?' == c || '#' == c || '[' == c || ']' == c || '@' == c;
}
/**
* Indicates whether the given character is in the {@code sub-delims} set.
*
* @see <a href="http://www.ietf.org/rfc/rfc3986.txt">RFC 3986, appendix A</a>
*/
protected boolean isSubDelimiter(int c) {
return '!' == c || '$' == c || '&' == c || '\'' == c || '(' == c || ')' == c || '*' == c || '+' == c ||
',' == c || ';' == c || '=' == c;
}
/**
* Indicates whether the given character is in the {@code reserved} set.
*
* @see <a href="http://www.ietf.org/rfc/rfc3986.txt">RFC 3986, appendix A</a>
*/
protected boolean isReserved(char c) {
return isGenericDelimiter(c) || isReserved(c);
}
/**
* Indicates whether the given character is in the {@code unreserved} set.
*
* @see <a href="http://www.ietf.org/rfc/rfc3986.txt">RFC 3986, appendix A</a>
*/
protected boolean isUnreserved(int c) {
return isAlpha(c) || isDigit(c) || '-' == c || '.' == c || '_' == c || '~' == c;
}
/**
* Indicates whether the given character is in the {@code pchar} set.
*
* @see <a href="http://www.ietf.org/rfc/rfc3986.txt">RFC 3986, appendix A</a>
*/
protected boolean isPchar(int c) {
return isUnreserved(c) || isSubDelimiter(c) || ':' == c || '@' == c;
}
}
/**
* Defines the contract for path (segments).
*/
interface PathComponent {
String getPath();
List<String> getPathSegments();
PathComponent encode(String encoding) throws UnsupportedEncodingException;
void verify();
PathComponent expand(UriTemplateVariables uriVariables);
}
/**
* Represents a path backed by a string.
*/
final static class FullPathComponent implements PathComponent {
private final String path;
FullPathComponent(String path) {
this.path = path;
}
public String getPath() {
return path;
}
public List<String> getPathSegments() {
String delimiter = new String(new char[]{PATH_DELIMITER});
String[] pathSegments = StringUtils.tokenizeToStringArray(path, delimiter);
return Collections.unmodifiableList(Arrays.asList(pathSegments));
}
public PathComponent encode(String encoding) throws UnsupportedEncodingException {
String encodedPath = encodeUriComponent(getPath(),encoding, Type.PATH);
return new FullPathComponent(encodedPath);
}
public void verify() {
verifyUriComponent(this.path, Type.PATH);
}
public PathComponent expand(UriTemplateVariables uriVariables) {
String expandedPath = expandUriComponent(getPath(), uriVariables);
return new FullPathComponent(expandedPath);
}
@Override
public boolean equals(Object obj) {
return (this == obj || (obj instanceof FullPathComponent &&
getPath().equals(((FullPathComponent) obj).getPath())));
}
@Override
public int hashCode() {
return getPath().hashCode();
}
}
/**
* Represents a path backed by a string list (i.e. path segments).
*/
final static class PathSegmentComponent implements PathComponent {
private final List<String> pathSegments;
PathSegmentComponent(List<String> pathSegments) {
this.pathSegments = Collections.unmodifiableList(pathSegments);
}
public String getPath() {
StringBuilder pathBuilder = new StringBuilder();
pathBuilder.append(PATH_DELIMITER);
for (Iterator<String> iterator = this.pathSegments.iterator(); iterator.hasNext(); ) {
String pathSegment = iterator.next();
pathBuilder.append(pathSegment);
if (iterator.hasNext()) {
pathBuilder.append(PATH_DELIMITER);
}
}
return pathBuilder.toString();
}
public List<String> getPathSegments() {
return this.pathSegments;
}
public PathComponent encode(String encoding) throws UnsupportedEncodingException {
List<String> pathSegments = getPathSegments();
List<String> encodedPathSegments = new ArrayList<String>(pathSegments.size());
for (String pathSegment : pathSegments) {
String encodedPathSegment = encodeUriComponent(pathSegment, encoding, Type.PATH_SEGMENT);
encodedPathSegments.add(encodedPathSegment);
}
return new PathSegmentComponent(encodedPathSegments);
}
public void verify() {
for (String pathSegment : getPathSegments()) {
verifyUriComponent(pathSegment, Type.PATH_SEGMENT);
}
}
public PathComponent expand(UriTemplateVariables uriVariables) {
List<String> pathSegments = getPathSegments();
List<String> expandedPathSegments = new ArrayList<String>(pathSegments.size());
for (String pathSegment : pathSegments) {
String expandedPathSegment = expandUriComponent(pathSegment, uriVariables);
expandedPathSegments.add(expandedPathSegment);
}
return new PathSegmentComponent(expandedPathSegments);
}
@Override
public boolean equals(Object obj) {
return (this == obj || (obj instanceof PathSegmentComponent &&
getPathSegments().equals(((PathSegmentComponent) obj).getPathSegments())));
}
@Override
public int hashCode() {
return getPathSegments().hashCode();
}
}
/**
* Represents a collection of PathComponents.
*/
final static class PathComponentComposite implements PathComponent {
private final List<PathComponent> pathComponents;
PathComponentComposite(List<PathComponent> pathComponents) {
this.pathComponents = pathComponents;
}
public String getPath() {
StringBuilder pathBuilder = new StringBuilder();
for (PathComponent pathComponent : this.pathComponents) {
pathBuilder.append(pathComponent.getPath());
}
return pathBuilder.toString();
}
public List<String> getPathSegments() {
List<String> result = new ArrayList<String>();
for (PathComponent pathComponent : this.pathComponents) {
result.addAll(pathComponent.getPathSegments());
}
return result;
}
public PathComponent encode(String encoding) throws UnsupportedEncodingException {
List<PathComponent> encodedComponents = new ArrayList<PathComponent>(pathComponents.size());
for (PathComponent pathComponent : pathComponents) {
encodedComponents.add(pathComponent.encode(encoding));
}
return new PathComponentComposite(encodedComponents);
}
public void verify() {
for (PathComponent pathComponent : pathComponents) {
pathComponent.verify();
}
}
public PathComponent expand(UriTemplateVariables uriVariables) {
List<PathComponent> expandedComponents = new ArrayList<PathComponent>(this.pathComponents.size());
for (PathComponent pathComponent : this.pathComponents) {
expandedComponents.add(pathComponent.expand(uriVariables));
}
return new PathComponentComposite(expandedComponents);
}
}
/**
* Represents an empty path.
*/
final static PathComponent NULL_PATH_COMPONENT = new PathComponent() {
public String getPath() {
return null;
}
public List<String> getPathSegments() {
return Collections.emptyList();
}
public PathComponent encode(String encoding) throws UnsupportedEncodingException {
return this;
}
public void verify() {
}
public PathComponent expand(UriTemplateVariables uriVariables) {
return this;
}
@Override
public boolean equals(Object obj) {
return (this == obj);
}
@Override
public int hashCode() {
return 42;
}
};
}

View File

@@ -0,0 +1,169 @@
/*
* Copyright 2002-2012 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.util;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collections;
import java.util.List;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ObjectUtils;
/**
* Extension of {@link UriComponents} for opaque URIs.
*
* @author Arjen Poutsma
* @since 3.2
* @see <a href="http://tools.ietf.org/html/rfc3986#section-1.2.3">Hierarchical vs Opaque URIs</a>
*/
final class OpaqueUriComponents extends UriComponents {
private static final MultiValueMap<String, String> QUERY_PARAMS_NONE = new LinkedMultiValueMap<String, String>(0);
private final String ssp;
OpaqueUriComponents(String scheme, String schemeSpecificPart, String fragment) {
super(scheme, fragment);
this.ssp = schemeSpecificPart;
}
@Override
public String getSchemeSpecificPart() {
return this.ssp;
}
@Override
public String getUserInfo() {
return null;
}
@Override
public String getHost() {
return null;
}
@Override
public int getPort() {
return -1;
}
@Override
public String getPath() {
return null;
}
@Override
public List<String> getPathSegments() {
return Collections.emptyList();
}
@Override
public String getQuery() {
return null;
}
@Override
public MultiValueMap<String, String> getQueryParams() {
return QUERY_PARAMS_NONE;
}
@Override
public UriComponents encode(String encoding) throws UnsupportedEncodingException {
return this;
}
@Override
protected UriComponents expandInternal(UriTemplateVariables uriVariables) {
String expandedScheme = expandUriComponent(this.getScheme(), uriVariables);
String expandedSSp = expandUriComponent(this.ssp, uriVariables);
String expandedFragment = expandUriComponent(this.getFragment(), uriVariables);
return new OpaqueUriComponents(expandedScheme, expandedSSp, expandedFragment);
}
@Override
public String toUriString() {
StringBuilder uriBuilder = new StringBuilder();
if (getScheme() != null) {
uriBuilder.append(getScheme());
uriBuilder.append(':');
}
if (this.ssp != null) {
uriBuilder.append(this.ssp);
}
if (getFragment() != null) {
uriBuilder.append('#');
uriBuilder.append(getFragment());
}
return uriBuilder.toString();
}
@Override
public URI toUri() {
try {
return new URI(getScheme(), this.ssp, getFragment());
}
catch (URISyntaxException ex) {
throw new IllegalStateException("Could not create URI object: " + ex.getMessage(), ex);
}
}
@Override
public UriComponents normalize() {
return this;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof OpaqueUriComponents)) {
return false;
}
OpaqueUriComponents other = (OpaqueUriComponents) obj;
if (ObjectUtils.nullSafeEquals(getScheme(), other.getScheme())) {
return false;
}
if (ObjectUtils.nullSafeEquals(this.ssp, other.ssp)) {
return false;
}
if (ObjectUtils.nullSafeEquals(getFragment(), other.getFragment())) {
return false;
}
return true;
}
@Override
public int hashCode() {
int result = ObjectUtils.nullSafeHashCode(getScheme());
result = 31 * result + ObjectUtils.nullSafeHashCode(this.ssp);
result = 31 * result + ObjectUtils.nullSafeHashCode(getFragment());
return result;
}
}

View File

@@ -32,8 +32,8 @@ import org.springframework.util.StringUtils;
/** /**
* Builder for {@link UriComponents}. * Builder for {@link UriComponents}.
* <p/> *
* Typical usage involves: * <p></p>Typical usage involves:
* <ol> * <ol>
* <li>Create a {@code UriComponentsBuilder} with one of the static factory methods (such as * <li>Create a {@code UriComponentsBuilder} with one of the static factory methods (such as
* {@link #fromPath(String)} or {@link #fromUri(URI)})</li> * {@link #fromPath(String)} or {@link #fromUri(URI)})</li>
@@ -46,10 +46,10 @@ import org.springframework.util.StringUtils;
* *
* @author Arjen Poutsma * @author Arjen Poutsma
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @since 3.1
* @see #newInstance() * @see #newInstance()
* @see #fromPath(String) * @see #fromPath(String)
* @see #fromUri(URI) * @see #fromUri(URI)
* @since 3.1
*/ */
public class UriComponentsBuilder { public class UriComponentsBuilder {
@@ -83,6 +83,8 @@ public class UriComponentsBuilder {
private String scheme; private String scheme;
private String ssp;
private String userInfo; private String userInfo;
private String host; private String host;
@@ -105,7 +107,7 @@ public class UriComponentsBuilder {
protected UriComponentsBuilder() { protected UriComponentsBuilder() {
} }
// Factory methods // Factory methods
/** /**
* Returns a new, empty builder. * Returns a new, empty builder.
@@ -143,7 +145,19 @@ public class UriComponentsBuilder {
/** /**
* Returns a builder that is initialized with the given URI string. * Returns a builder that is initialized with the given URI string.
* *
* @param uri the URI string to initialize with * <p><strong>Note:</strong> The presence of reserved characters can prevent
* correct parsing of the URI string. For example if a query parameter
* contains {@code '='} or {@code '&'} characters, the query string cannot
* be parsed unambiguously. Such values should be substituted for URI
* variables to enable correct parsing:
*
* <pre>
* String uriString = &quot;/hotels/42?filter={value}&quot;;
* UriComponentsBuilder.fromUriString(uriString).buildAndExpand(&quot;hot&amp;cold&quot;);
* </pre>
*
* @param uri
* the URI string to initialize with
* @return the new {@code UriComponentsBuilder} * @return the new {@code UriComponentsBuilder}
*/ */
public static UriComponentsBuilder fromUriString(String uri) { public static UriComponentsBuilder fromUriString(String uri) {
@@ -152,16 +166,43 @@ public class UriComponentsBuilder {
if (m.matches()) { if (m.matches()) {
UriComponentsBuilder builder = new UriComponentsBuilder(); UriComponentsBuilder builder = new UriComponentsBuilder();
builder.scheme(m.group(2)); String scheme = m.group(2);
builder.userInfo(m.group(5)); String userInfo = m.group(5);
builder.host(m.group(6)); String host = m.group(6);
String port = m.group(8); String port = m.group(8);
if (StringUtils.hasLength(port)) { String path = m.group(9);
builder.port(Integer.parseInt(port)); String query = m.group(11);
String fragment = m.group(13);
boolean opaque = false;
if (StringUtils.hasLength(scheme)) {
String s = uri.substring(scheme.length());
if (!s.startsWith(":/")) {
opaque = true;
}
} }
builder.path(m.group(9));
builder.query(m.group(11)); builder.scheme(scheme);
builder.fragment(m.group(13));
if (opaque) {
String ssp = uri.substring(scheme.length()).substring(1);
if (StringUtils.hasLength(fragment)) {
ssp = ssp.substring(0, ssp.length() - (fragment.length() + 1));
}
builder.schemeSpecificPart(ssp);
}
else {
builder.userInfo(userInfo);
builder.host(host);
if (StringUtils.hasLength(port)) {
builder.port(Integer.parseInt(port));
}
builder.path(path);
builder.query(query);
}
builder.fragment(fragment);
return builder; return builder;
} }
@@ -173,6 +214,17 @@ public class UriComponentsBuilder {
/** /**
* Creates a new {@code UriComponents} object from the string HTTP URL. * Creates a new {@code UriComponents} object from the string HTTP URL.
* *
* <p><strong>Note:</strong> The presence of reserved characters can prevent
* correct parsing of the URI string. For example if a query parameter
* contains {@code '='} or {@code '&'} characters, the query string cannot
* be parsed unambiguously. Such values should be substituted for URI
* variables to enable correct parsing:
*
* <pre>
* String uriString = &quot;/hotels/42?filter={value}&quot;;
* UriComponentsBuilder.fromUriString(uriString).buildAndExpand(&quot;hot&amp;cold&quot;);
* </pre>
*
* @param httpUrl the source URI * @param httpUrl the source URI
* @return the URI components of the URI * @return the URI components of the URI
*/ */
@@ -201,7 +253,7 @@ public class UriComponentsBuilder {
// build methods // build methods
/** /**
* Builds a {@code UriComponents} instance from the various components contained in this builder. * Builds a {@code UriComponents} instance from the various components contained in this builder.
@@ -213,13 +265,21 @@ public class UriComponentsBuilder {
} }
/** /**
* Builds a {@code UriComponents} instance from the various components contained in this builder. * Builds a {@code UriComponents} instance from the various components
* contained in this builder.
* *
* @param encoded whether all the components set in this builder are encoded ({@code true}) or not ({@code false}). * @param encoded whether all the components set in this builder are
* encoded ({@code true}) or not ({@code false}).
* @return the URI components * @return the URI components
*/ */
public UriComponents build(boolean encoded) { public UriComponents build(boolean encoded) {
return new UriComponents(scheme, userInfo, host, port, pathBuilder.build(), queryParams, fragment, encoded, true); if (ssp != null) {
return new OpaqueUriComponents(scheme, ssp, fragment);
}
else {
return new HierarchicalUriComponents(
scheme, userInfo, host, port, pathBuilder.build(), queryParams, fragment, encoded, true);
}
} }
/** /**
@@ -246,7 +306,7 @@ public class UriComponentsBuilder {
return build(false).expand(uriVariableValues); return build(false).expand(uriVariableValues);
} }
// URI components methods // URI components methods
/** /**
* Initializes all components of this URI builder with the components of the given URI. * Initializes all components of this URI builder with the components of the given URI.
@@ -256,25 +316,31 @@ public class UriComponentsBuilder {
*/ */
public UriComponentsBuilder uri(URI uri) { public UriComponentsBuilder uri(URI uri) {
Assert.notNull(uri, "'uri' must not be null"); Assert.notNull(uri, "'uri' must not be null");
Assert.isTrue(!uri.isOpaque(), "Opaque URI [" + uri + "] not supported");
this.scheme = uri.getScheme(); this.scheme = uri.getScheme();
if (uri.getRawUserInfo() != null) { if (uri.isOpaque()) {
this.userInfo = uri.getRawUserInfo(); this.ssp = uri.getRawSchemeSpecificPart();
resetHierarchicalComponents();
} }
if (uri.getHost() != null) { else {
this.host = uri.getHost(); if (uri.getRawUserInfo() != null) {
} this.userInfo = uri.getRawUserInfo();
if (uri.getPort() != -1) { }
this.port = uri.getPort(); if (uri.getHost() != null) {
} this.host = uri.getHost();
if (StringUtils.hasLength(uri.getRawPath())) { }
this.pathBuilder = new FullPathComponentBuilder(uri.getRawPath()); if (uri.getPort() != -1) {
} this.port = uri.getPort();
if (StringUtils.hasLength(uri.getRawQuery())) { }
this.queryParams.clear(); if (StringUtils.hasLength(uri.getRawPath())) {
query(uri.getRawQuery()); this.pathBuilder = new FullPathComponentBuilder(uri.getRawPath());
}
if (StringUtils.hasLength(uri.getRawQuery())) {
this.queryParams.clear();
query(uri.getRawQuery());
}
resetSchemeSpecificPart();
} }
if (uri.getRawFragment() != null) { if (uri.getRawFragment() != null) {
this.fragment = uri.getRawFragment(); this.fragment = uri.getRawFragment();
@@ -282,11 +348,24 @@ public class UriComponentsBuilder {
return this; return this;
} }
private void resetHierarchicalComponents() {
this.userInfo = null;
this.host = null;
this.port = -1;
this.pathBuilder = NULL_PATH_COMPONENT_BUILDER;
this.queryParams.clear();
}
private void resetSchemeSpecificPart() {
this.ssp = null;
}
/** /**
* Sets the URI scheme. The given scheme may contain URI template variables, and may also be {@code null} to clear the * Sets the URI scheme. The given scheme may contain URI template variables,
* scheme of this builder. * and may also be {@code null} to clear the scheme of this builder.
* *
* @param scheme the URI scheme * @param scheme
* the URI scheme
* @return this UriComponentsBuilder * @return this UriComponentsBuilder
*/ */
public UriComponentsBuilder scheme(String scheme) { public UriComponentsBuilder scheme(String scheme) {
@@ -295,26 +374,44 @@ public class UriComponentsBuilder {
} }
/** /**
* Sets the URI user info. The given user info may contain URI template variables, and may also be {@code null} to * Set the URI scheme-specific-part. When invoked, this method overwrites
* clear the user info of this builder. * {@linkplain #userInfo(String) user-info}, {@linkplain #host(String) host},
* {@linkplain #port(int) port}, {@linkplain #path(String) path}, and
* {@link #query(String) query}.
*
* @param ssp the URI scheme-specific-part, may contain URI template parameters
* @return this UriComponentsBuilder
*/
public UriComponentsBuilder schemeSpecificPart(String ssp) {
this.ssp = ssp;
resetHierarchicalComponents();
return this;
}
/**
* Sets the URI user info. The given user info may contain URI template
* variables, and may also be {@code null} to clear the user info of this
* builder.
* *
* @param userInfo the URI user info * @param userInfo the URI user info
* @return this UriComponentsBuilder * @return this UriComponentsBuilder
*/ */
public UriComponentsBuilder userInfo(String userInfo) { public UriComponentsBuilder userInfo(String userInfo) {
this.userInfo = userInfo; this.userInfo = userInfo;
resetSchemeSpecificPart();
return this; return this;
} }
/** /**
* Sets the URI host. The given host may contain URI template variables, and may also be {@code null} to clear the host * Sets the URI host. The given host may contain URI template variables, and
* of this builder. * may also be {@code null} to clear the host of this builder.
* *
* @param host the URI host * @param host the URI host
* @return this UriComponentsBuilder * @return this UriComponentsBuilder
*/ */
public UriComponentsBuilder host(String host) { public UriComponentsBuilder host(String host) {
this.host = host; this.host = host;
resetSchemeSpecificPart();
return this; return this;
} }
@@ -327,11 +424,13 @@ public class UriComponentsBuilder {
public UriComponentsBuilder port(int port) { public UriComponentsBuilder port(int port) {
Assert.isTrue(port >= -1, "'port' must not be < -1"); Assert.isTrue(port >= -1, "'port' must not be < -1");
this.port = port; this.port = port;
resetSchemeSpecificPart();
return this; return this;
} }
/** /**
* Appends the given path to the existing path of this builder. The given path may contain URI template variables. * Appends the given path to the existing path of this builder. The given
* path may contain URI template variables.
* *
* @param path the URI path * @param path the URI path
* @return this UriComponentsBuilder * @return this UriComponentsBuilder
@@ -343,6 +442,7 @@ public class UriComponentsBuilder {
else { else {
this.pathBuilder = NULL_PATH_COMPONENT_BUILDER; this.pathBuilder = NULL_PATH_COMPONENT_BUILDER;
} }
resetSchemeSpecificPart();
return this; return this;
} }
@@ -355,6 +455,7 @@ public class UriComponentsBuilder {
public UriComponentsBuilder replacePath(String path) { public UriComponentsBuilder replacePath(String path) {
this.pathBuilder = NULL_PATH_COMPONENT_BUILDER; this.pathBuilder = NULL_PATH_COMPONENT_BUILDER;
path(path); path(path);
resetSchemeSpecificPart();
return this; return this;
} }
@@ -368,11 +469,24 @@ public class UriComponentsBuilder {
public UriComponentsBuilder pathSegment(String... pathSegments) throws IllegalArgumentException { public UriComponentsBuilder pathSegment(String... pathSegments) throws IllegalArgumentException {
Assert.notNull(pathSegments, "'segments' must not be null"); Assert.notNull(pathSegments, "'segments' must not be null");
this.pathBuilder = this.pathBuilder.appendPathSegments(pathSegments); this.pathBuilder = this.pathBuilder.appendPathSegments(pathSegments);
resetSchemeSpecificPart();
return this; return this;
} }
/** /**
* Appends the given query to the existing query of this builder. The given query may contain URI template variables. * Appends the given query to the existing query of this builder.
* The given query may contain URI template variables.
*
* <p><strong>Note:</strong> The presence of reserved characters can prevent
* correct parsing of the URI string. For example if a query parameter
* contains {@code '='} or {@code '&'} characters, the query string cannot
* be parsed unambiguously. Such values should be substituted for URI
* variables to enable correct parsing:
*
* <pre>
* String uriString = &quot;/hotels/42?filter={value}&quot;;
* UriComponentsBuilder.fromUriString(uriString).buildAndExpand(&quot;hot&amp;cold&quot;);
* </pre>
* *
* @param query the query string * @param query the query string
* @return this UriComponentsBuilder * @return this UriComponentsBuilder
@@ -389,6 +503,7 @@ public class UriComponentsBuilder {
else { else {
this.queryParams.clear(); this.queryParams.clear();
} }
resetSchemeSpecificPart();
return this; return this;
} }
@@ -401,16 +516,20 @@ public class UriComponentsBuilder {
public UriComponentsBuilder replaceQuery(String query) { public UriComponentsBuilder replaceQuery(String query) {
this.queryParams.clear(); this.queryParams.clear();
query(query); query(query);
resetSchemeSpecificPart();
return this; return this;
} }
/** /**
* Appends the given query parameter to the existing query parameters. The given name or any of the values may contain * Appends the given query parameter to the existing query parameters. The
* URI template variables. If no values are given, the resulting URI will contain the query parameter name only (i.e. * given name or any of the values may contain URI template variables. If no
* {@code ?foo} instead of {@code ?foo=bar}. * values are given, the resulting URI will contain the query parameter name
* only (i.e. {@code ?foo} instead of {@code ?foo=bar}.
* *
* @param name the query parameter name * @param name
* @param values the query parameter values * the query parameter name
* @param values
* the query parameter values
* @return this UriComponentsBuilder * @return this UriComponentsBuilder
*/ */
public UriComponentsBuilder queryParam(String name, Object... values) { public UriComponentsBuilder queryParam(String name, Object... values) {
@@ -424,15 +543,19 @@ public class UriComponentsBuilder {
else { else {
this.queryParams.add(name, null); this.queryParams.add(name, null);
} }
resetSchemeSpecificPart();
return this; return this;
} }
/** /**
* Sets the query parameter values overriding all existing query values for the same parameter. * Sets the query parameter values overriding all existing query values for
* If no values are given, the query parameter is removed. * the same parameter. If no values are given, the query parameter is
* removed.
* *
* @param name the query parameter name * @param name
* @param values the query parameter values * the query parameter name
* @param values
* the query parameter values
* @return this UriComponentsBuilder * @return this UriComponentsBuilder
*/ */
public UriComponentsBuilder replaceQueryParam(String name, Object... values) { public UriComponentsBuilder replaceQueryParam(String name, Object... values) {
@@ -441,14 +564,17 @@ public class UriComponentsBuilder {
if (!ObjectUtils.isEmpty(values)) { if (!ObjectUtils.isEmpty(values)) {
queryParam(name, values); queryParam(name, values);
} }
resetSchemeSpecificPart();
return this; return this;
} }
/** /**
* Sets the URI fragment. The given fragment may contain URI template variables, and may also be {@code null} to clear * Sets the URI fragment. The given fragment may contain URI template
* the fragment of this builder. * variables, and may also be {@code null} to clear the fragment of this
* builder.
* *
* @param fragment the URI fragment * @param fragment
* the URI fragment
* @return this UriComponentsBuilder * @return this UriComponentsBuilder
*/ */
public UriComponentsBuilder fragment(String fragment) { public UriComponentsBuilder fragment(String fragment) {
@@ -463,11 +589,11 @@ public class UriComponentsBuilder {
} }
/** /**
* Represents a builder for {@link org.springframework.web.util.UriComponents.PathComponent} * Represents a builder for {@link HierarchicalUriComponents.PathComponent}
*/ */
private interface PathComponentBuilder { private interface PathComponentBuilder {
UriComponents.PathComponent build(); HierarchicalUriComponents.PathComponent build();
PathComponentBuilder appendPath(String path); PathComponentBuilder appendPath(String path);
@@ -485,8 +611,8 @@ public class UriComponentsBuilder {
this.path = new StringBuilder(path); this.path = new StringBuilder(path);
} }
public UriComponents.PathComponent build() { public HierarchicalUriComponents.PathComponent build() {
return new UriComponents.FullPathComponent(path.toString()); return new HierarchicalUriComponents.FullPathComponent(path.toString());
} }
public PathComponentBuilder appendPath(String path) { public PathComponentBuilder appendPath(String path) {
@@ -522,8 +648,8 @@ public class UriComponentsBuilder {
return result; return result;
} }
public UriComponents.PathComponent build() { public HierarchicalUriComponents.PathComponent build() {
return new UriComponents.PathSegmentComponent(pathSegments); return new HierarchicalUriComponents.PathSegmentComponent(pathSegments);
} }
public PathComponentBuilder appendPath(String path) { public PathComponentBuilder appendPath(String path) {
@@ -549,14 +675,14 @@ public class UriComponentsBuilder {
pathComponentBuilders.add(builder); pathComponentBuilders.add(builder);
} }
public UriComponents.PathComponent build() { public HierarchicalUriComponents.PathComponent build() {
List<UriComponents.PathComponent> pathComponents = List<HierarchicalUriComponents.PathComponent> pathComponents =
new ArrayList<UriComponents.PathComponent>(pathComponentBuilders.size()); new ArrayList<HierarchicalUriComponents.PathComponent>(pathComponentBuilders.size());
for (PathComponentBuilder pathComponentBuilder : pathComponentBuilders) { for (PathComponentBuilder pathComponentBuilder : pathComponentBuilders) {
pathComponents.add(pathComponentBuilder.build()); pathComponents.add(pathComponentBuilder.build());
} }
return new UriComponents.PathComponentComposite(pathComponents); return new HierarchicalUriComponents.PathComponentComposite(pathComponents);
} }
public PathComponentBuilder appendPath(String path) { public PathComponentBuilder appendPath(String path) {
@@ -576,8 +702,8 @@ public class UriComponentsBuilder {
*/ */
private static PathComponentBuilder NULL_PATH_COMPONENT_BUILDER = new PathComponentBuilder() { private static PathComponentBuilder NULL_PATH_COMPONENT_BUILDER = new PathComponentBuilder() {
public UriComponents.PathComponent build() { public HierarchicalUriComponents.PathComponent build() {
return UriComponents.NULL_PATH_COMPONENT; return HierarchicalUriComponents.NULL_PATH_COMPONENT;
} }
public PathComponentBuilder appendPath(String path) { public PathComponentBuilder appendPath(String path) {

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2011 the original author or authors. * Copyright 2002-2012 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -29,9 +29,9 @@ import org.springframework.util.Assert;
* *
* <p>All {@code encode*(String, String} methods in this class operate in a similar way: * <p>All {@code encode*(String, String} methods in this class operate in a similar way:
* <ul> * <ul>
* <li>Valid characters for the specific URI component as defined in RFC 3986 stay the same.</li> * <li>Valid characters for the specific URI component as defined in RFC 3986 stay the same.</li>
* <li>All other characters are converted into one or more bytes in the given encoding scheme. Each of the * <li>All other characters are converted into one or more bytes in the given encoding scheme. Each of the
* resulting bytes is written as a hexadecimal string in the "<code>%<i>xy</i></code>" format.</li> * resulting bytes is written as a hexadecimal string in the "<code>%<i>xy</i></code>" format.</li>
* </ul> * </ul>
* *
* @author Arjen Poutsma * @author Arjen Poutsma
@@ -64,6 +64,7 @@ public abstract class UriUtils {
private static final Pattern HTTP_URL_PATTERN = Pattern.compile( private static final Pattern HTTP_URL_PATTERN = Pattern.compile(
"^" + HTTP_PATTERN + "(//(" + USERINFO_PATTERN + "@)?" + HOST_PATTERN + "(:" + PORT_PATTERN + ")?" + ")?" + "^" + HTTP_PATTERN + "(//(" + USERINFO_PATTERN + "@)?" + HOST_PATTERN + "(:" + PORT_PATTERN + ")?" + ")?" +
PATH_PATTERN + "(\\?" + LAST_PATTERN + ")?"); PATH_PATTERN + "(\\?" + LAST_PATTERN + ")?");
// encoding // encoding
/** /**
@@ -73,8 +74,8 @@ public abstract class UriUtils {
* characters in query parameter names and query parameter values because they cannot * characters in query parameter names and query parameter values because they cannot
* be parsed in a reliable way. Instead use: * be parsed in a reliable way. Instead use:
* <pre> * <pre>
* UriComponents uriComponents = UriComponentsBuilder.fromUri("/path?name={value}").buildAndExpand("a=b"); * UriComponents uriComponents = UriComponentsBuilder.fromUri("/path?name={value}").buildAndExpand("a=b");
* String encodedUri = uriComponents.encode().toUriString(); * String encodedUri = uriComponents.encode().toUriString();
* </pre> * </pre>
* @param uri the URI to be encoded * @param uri the URI to be encoded
* @param encoding the character encoding to encode to * @param encoding the character encoding to encode to
@@ -83,6 +84,7 @@ public abstract class UriUtils {
* @throws UnsupportedEncodingException when the given encoding parameter is not supported * @throws UnsupportedEncodingException when the given encoding parameter is not supported
* @deprecated in favor of {@link UriComponentsBuilder}; see note about query param encoding * @deprecated in favor of {@link UriComponentsBuilder}; see note about query param encoding
*/ */
@Deprecated
public static String encodeUri(String uri, String encoding) throws UnsupportedEncodingException { public static String encodeUri(String uri, String encoding) throws UnsupportedEncodingException {
Assert.notNull(uri, "'uri' must not be null"); Assert.notNull(uri, "'uri' must not be null");
Assert.hasLength(encoding, "'encoding' must not be empty"); Assert.hasLength(encoding, "'encoding' must not be empty");
@@ -113,8 +115,8 @@ public abstract class UriUtils {
* characters in query parameter names and query parameter values because they cannot * characters in query parameter names and query parameter values because they cannot
* be parsed in a reliable way. Instead use: * be parsed in a reliable way. Instead use:
* <pre> * <pre>
* UriComponents uriComponents = UriComponentsBuilder.fromHttpUrl("/path?name={value}").buildAndExpand("a=b"); * UriComponents uriComponents = UriComponentsBuilder.fromHttpUrl("/path?name={value}").buildAndExpand("a=b");
* String encodedUri = uriComponents.encode().toUriString(); * String encodedUri = uriComponents.encode().toUriString();
* </pre> * </pre>
* @param httpUrl the HTTP URL to be encoded * @param httpUrl the HTTP URL to be encoded
* @param encoding the character encoding to encode to * @param encoding the character encoding to encode to
@@ -123,6 +125,7 @@ public abstract class UriUtils {
* @throws UnsupportedEncodingException when the given encoding parameter is not supported * @throws UnsupportedEncodingException when the given encoding parameter is not supported
* @deprecated in favor of {@link UriComponentsBuilder}; see note about query param encoding * @deprecated in favor of {@link UriComponentsBuilder}; see note about query param encoding
*/ */
@Deprecated
public static String encodeHttpUrl(String httpUrl, String encoding) throws UnsupportedEncodingException { public static String encodeHttpUrl(String httpUrl, String encoding) throws UnsupportedEncodingException {
Assert.notNull(httpUrl, "'httpUrl' must not be null"); Assert.notNull(httpUrl, "'httpUrl' must not be null");
Assert.hasLength(encoding, "'encoding' must not be empty"); Assert.hasLength(encoding, "'encoding' must not be empty");
@@ -160,46 +163,47 @@ public abstract class UriUtils {
* @throws UnsupportedEncodingException when the given encoding parameter is not supported * @throws UnsupportedEncodingException when the given encoding parameter is not supported
* @deprecated in favor of {@link UriComponentsBuilder} * @deprecated in favor of {@link UriComponentsBuilder}
*/ */
@Deprecated
public static String encodeUriComponents(String scheme, String authority, String userInfo, public static String encodeUriComponents(String scheme, String authority, String userInfo,
String host, String port, String path, String query, String fragment, String encoding) String host, String port, String path, String query, String fragment, String encoding)
throws UnsupportedEncodingException { throws UnsupportedEncodingException {
Assert.hasLength(encoding, "'encoding' must not be empty"); Assert.hasLength(encoding, "'encoding' must not be empty");
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
if (scheme != null) { if (scheme != null) {
sb.append(encodeScheme(scheme, encoding)); sb.append(encodeScheme(scheme, encoding));
sb.append(':'); sb.append(':');
} }
if (authority != null) { if (authority != null) {
sb.append("//"); sb.append("//");
if (userInfo != null) { if (userInfo != null) {
sb.append(encodeUserInfo(userInfo, encoding)); sb.append(encodeUserInfo(userInfo, encoding));
sb.append('@'); sb.append('@');
} }
if (host != null) { if (host != null) {
sb.append(encodeHost(host, encoding)); sb.append(encodeHost(host, encoding));
} }
if (port != null) { if (port != null) {
sb.append(':'); sb.append(':');
sb.append(encodePort(port, encoding)); sb.append(encodePort(port, encoding));
} }
} }
sb.append(encodePath(path, encoding)); sb.append(encodePath(path, encoding));
if (query != null) { if (query != null) {
sb.append('?'); sb.append('?');
sb.append(encodeQuery(query, encoding)); sb.append(encodeQuery(query, encoding));
} }
if (fragment != null) { if (fragment != null) {
sb.append('#'); sb.append('#');
sb.append(encodeFragment(fragment, encoding)); sb.append(encodeFragment(fragment, encoding));
} }
return sb.toString(); return sb.toString();
} }
@@ -213,7 +217,8 @@ public abstract class UriUtils {
* @throws UnsupportedEncodingException when the given encoding parameter is not supported * @throws UnsupportedEncodingException when the given encoding parameter is not supported
*/ */
public static String encodeScheme(String scheme, String encoding) throws UnsupportedEncodingException { public static String encodeScheme(String scheme, String encoding) throws UnsupportedEncodingException {
return UriComponents.encodeUriComponent(scheme, encoding, UriComponents.Type.SCHEME); return HierarchicalUriComponents.encodeUriComponent(scheme, encoding,
HierarchicalUriComponents.Type.SCHEME);
} }
/** /**
@@ -224,7 +229,8 @@ public abstract class UriUtils {
* @throws UnsupportedEncodingException when the given encoding parameter is not supported * @throws UnsupportedEncodingException when the given encoding parameter is not supported
*/ */
public static String encodeAuthority(String authority, String encoding) throws UnsupportedEncodingException { public static String encodeAuthority(String authority, String encoding) throws UnsupportedEncodingException {
return UriComponents.encodeUriComponent(authority, encoding, UriComponents.Type.AUTHORITY); return HierarchicalUriComponents.encodeUriComponent(authority, encoding,
HierarchicalUriComponents.Type.AUTHORITY);
} }
/** /**
@@ -235,7 +241,8 @@ public abstract class UriUtils {
* @throws UnsupportedEncodingException when the given encoding parameter is not supported * @throws UnsupportedEncodingException when the given encoding parameter is not supported
*/ */
public static String encodeUserInfo(String userInfo, String encoding) throws UnsupportedEncodingException { public static String encodeUserInfo(String userInfo, String encoding) throws UnsupportedEncodingException {
return UriComponents.encodeUriComponent(userInfo, encoding, UriComponents.Type.USER_INFO); return HierarchicalUriComponents.encodeUriComponent(userInfo, encoding,
HierarchicalUriComponents.Type.USER_INFO);
} }
/** /**
@@ -246,7 +253,8 @@ public abstract class UriUtils {
* @throws UnsupportedEncodingException when the given encoding parameter is not supported * @throws UnsupportedEncodingException when the given encoding parameter is not supported
*/ */
public static String encodeHost(String host, String encoding) throws UnsupportedEncodingException { public static String encodeHost(String host, String encoding) throws UnsupportedEncodingException {
return UriComponents.encodeUriComponent(host, encoding, UriComponents.Type.HOST); return HierarchicalUriComponents
.encodeUriComponent(host, encoding, HierarchicalUriComponents.Type.HOST);
} }
/** /**
@@ -257,7 +265,8 @@ public abstract class UriUtils {
* @throws UnsupportedEncodingException when the given encoding parameter is not supported * @throws UnsupportedEncodingException when the given encoding parameter is not supported
*/ */
public static String encodePort(String port, String encoding) throws UnsupportedEncodingException { public static String encodePort(String port, String encoding) throws UnsupportedEncodingException {
return UriComponents.encodeUriComponent(port, encoding, UriComponents.Type.PORT); return HierarchicalUriComponents
.encodeUriComponent(port, encoding, HierarchicalUriComponents.Type.PORT);
} }
/** /**
@@ -268,7 +277,8 @@ public abstract class UriUtils {
* @throws UnsupportedEncodingException when the given encoding parameter is not supported * @throws UnsupportedEncodingException when the given encoding parameter is not supported
*/ */
public static String encodePath(String path, String encoding) throws UnsupportedEncodingException { public static String encodePath(String path, String encoding) throws UnsupportedEncodingException {
return UriComponents.encodeUriComponent(path, encoding, UriComponents.Type.PATH); return HierarchicalUriComponents
.encodeUriComponent(path, encoding, HierarchicalUriComponents.Type.PATH);
} }
/** /**
@@ -279,7 +289,8 @@ public abstract class UriUtils {
* @throws UnsupportedEncodingException when the given encoding parameter is not supported * @throws UnsupportedEncodingException when the given encoding parameter is not supported
*/ */
public static String encodePathSegment(String segment, String encoding) throws UnsupportedEncodingException { public static String encodePathSegment(String segment, String encoding) throws UnsupportedEncodingException {
return UriComponents.encodeUriComponent(segment, encoding, UriComponents.Type.PATH_SEGMENT); return HierarchicalUriComponents.encodeUriComponent(segment, encoding,
HierarchicalUriComponents.Type.PATH_SEGMENT);
} }
/** /**
@@ -290,7 +301,8 @@ public abstract class UriUtils {
* @throws UnsupportedEncodingException when the given encoding parameter is not supported * @throws UnsupportedEncodingException when the given encoding parameter is not supported
*/ */
public static String encodeQuery(String query, String encoding) throws UnsupportedEncodingException { public static String encodeQuery(String query, String encoding) throws UnsupportedEncodingException {
return UriComponents.encodeUriComponent(query, encoding, UriComponents.Type.QUERY); return HierarchicalUriComponents
.encodeUriComponent(query, encoding, HierarchicalUriComponents.Type.QUERY);
} }
/** /**
@@ -301,7 +313,8 @@ public abstract class UriUtils {
* @throws UnsupportedEncodingException when the given encoding parameter is not supported * @throws UnsupportedEncodingException when the given encoding parameter is not supported
*/ */
public static String encodeQueryParam(String queryParam, String encoding) throws UnsupportedEncodingException { public static String encodeQueryParam(String queryParam, String encoding) throws UnsupportedEncodingException {
return UriComponents.encodeUriComponent(queryParam, encoding, UriComponents.Type.QUERY_PARAM); return HierarchicalUriComponents.encodeUriComponent(queryParam, encoding,
HierarchicalUriComponents.Type.QUERY_PARAM);
} }
/** /**
@@ -312,7 +325,8 @@ public abstract class UriUtils {
* @throws UnsupportedEncodingException when the given encoding parameter is not supported * @throws UnsupportedEncodingException when the given encoding parameter is not supported
*/ */
public static String encodeFragment(String fragment, String encoding) throws UnsupportedEncodingException { public static String encodeFragment(String fragment, String encoding) throws UnsupportedEncodingException {
return UriComponents.encodeUriComponent(fragment, encoding, UriComponents.Type.FRAGMENT); return HierarchicalUriComponents.encodeUriComponent(fragment, encoding,
HierarchicalUriComponents.Type.FRAGMENT);
} }
@@ -321,11 +335,11 @@ public abstract class UriUtils {
/** /**
* Decodes the given encoded source String into an URI. Based on the following rules: * Decodes the given encoded source String into an URI. Based on the following rules:
* <ul> * <ul>
* <li>Alphanumeric characters {@code "a"} through {@code "z"}, {@code "A"} through {@code "Z"}, and * <li>Alphanumeric characters {@code "a"} through {@code "z"}, {@code "A"} through {@code "Z"}, and
* {@code "0"} through {@code "9"} stay the same.</li> * {@code "0"} through {@code "9"} stay the same.</li>
* <li>Special characters {@code "-"}, {@code "_"}, {@code "."}, and {@code "*"} stay the same.</li> * <li>Special characters {@code "-"}, {@code "_"}, {@code "."}, and {@code "*"} stay the same.</li>
* <li>A sequence "<code>%<i>xy</i></code>" is interpreted as a hexadecimal representation of the character.</li> * <li>A sequence "<code>%<i>xy</i></code>" is interpreted as a hexadecimal representation of the character.</li>
* </ul> * </ul>
* @param source the source string * @param source the source string
* @param encoding the encoding * @param encoding the encoding
* @return the decoded URI * @return the decoded URI

View File

@@ -16,9 +16,6 @@
package org.springframework.web.util; package org.springframework.web.util;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.util.Arrays; import java.util.Arrays;
@@ -26,21 +23,26 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
import org.junit.Test; import org.junit.Test;
import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
/** @author Arjen Poutsma */ import static org.junit.Assert.*;
/**
* @author Arjen Poutsma
*/
public class UriComponentsBuilderTests { public class UriComponentsBuilderTests {
@Test @Test
public void plain() throws URISyntaxException { public void plain() throws URISyntaxException {
UriComponentsBuilder builder = UriComponentsBuilder.newInstance(); UriComponentsBuilder builder = UriComponentsBuilder.newInstance();
UriComponents result = builder.scheme("http").host("example.com").path("foo").queryParam("bar").fragment("baz").build(); UriComponents result = builder.scheme("http").host("example.com").path("foo").queryParam("bar").fragment("baz").build();
assertEquals("http", result.getScheme()); assertEquals("http", result.getScheme());
assertEquals("example.com", result.getHost()); assertEquals("example.com", result.getHost());
assertEquals("foo", result.getPath()); assertEquals("foo", result.getPath());
assertEquals("bar", result.getQuery()); assertEquals("bar", result.getQuery());
assertEquals("baz", result.getFragment()); assertEquals("baz", result.getFragment());
URI expected = new URI("http://example.com/foo?bar#baz"); URI expected = new URI("http://example.com/foo?bar#baz");
assertEquals("Invalid result URI", expected, result.toUri()); assertEquals("Invalid result URI", expected, result.toUri());
@@ -49,43 +51,54 @@ public class UriComponentsBuilderTests {
@Test @Test
public void fromPath() throws URISyntaxException { public void fromPath() throws URISyntaxException {
UriComponents result = UriComponentsBuilder.fromPath("foo").queryParam("bar").fragment("baz").build(); UriComponents result = UriComponentsBuilder.fromPath("foo").queryParam("bar").fragment("baz").build();
assertEquals("foo", result.getPath()); assertEquals("foo", result.getPath());
assertEquals("bar", result.getQuery()); assertEquals("bar", result.getQuery());
assertEquals("baz", result.getFragment()); assertEquals("baz", result.getFragment());
URI expected = new URI("/foo?bar#baz"); URI expected = new URI("/foo?bar#baz");
assertEquals("Invalid result URI", expected, result.toUri()); assertEquals("Invalid result URI", expected, result.toUri());
result = UriComponentsBuilder.fromPath("/foo").build(); result = UriComponentsBuilder.fromPath("/foo").build();
assertEquals("/foo", result.getPath()); assertEquals("/foo", result.getPath());
expected = new URI("/foo"); expected = new URI("/foo");
assertEquals("Invalid result URI", expected, result.toUri()); assertEquals("Invalid result URI", expected, result.toUri());
} }
@Test @Test
public void fromUri() throws URISyntaxException { public void fromHierarchicalUri() throws URISyntaxException {
URI uri = new URI("http://example.com/foo?bar#baz"); URI uri = new URI("http://example.com/foo?bar#baz");
UriComponents result = UriComponentsBuilder.fromUri(uri).build(); UriComponents result = UriComponentsBuilder.fromUri(uri).build();
assertEquals("http", result.getScheme()); assertEquals("http", result.getScheme());
assertEquals("example.com", result.getHost()); assertEquals("example.com", result.getHost());
assertEquals("/foo", result.getPath()); assertEquals("/foo", result.getPath());
assertEquals("bar", result.getQuery()); assertEquals("bar", result.getQuery());
assertEquals("baz", result.getFragment()); assertEquals("baz", result.getFragment());
assertEquals("Invalid result URI", uri, result.toUri());
}
@Test
public void fromOpaqueUri() throws URISyntaxException {
URI uri = new URI("mailto:foo@bar.com#baz");
UriComponents result = UriComponentsBuilder.fromUri(uri).build();
assertEquals("mailto", result.getScheme());
assertEquals("foo@bar.com", result.getSchemeSpecificPart());
assertEquals("baz", result.getFragment());
assertEquals("Invalid result URI", uri, result.toUri()); assertEquals("Invalid result URI", uri, result.toUri());
} }
// SPR-9317 // SPR-9317
@Test @Test
public void fromUriEncodedQuery() throws URISyntaxException { public void fromUriEncodedQuery() throws URISyntaxException {
URI uri = new URI("http://www.example.org/?param=aGVsbG9Xb3JsZA%3D%3D"); URI uri = new URI("http://www.example.org/?param=aGVsbG9Xb3JsZA%3D%3D");
String fromUri = UriComponentsBuilder.fromUri(uri).build().getQueryParams().get("param").get(0); String fromUri = UriComponentsBuilder.fromUri(uri).build().getQueryParams().get("param").get(0);
String fromUriString = UriComponentsBuilder.fromUriString(uri.toString()).build().getQueryParams().get("param").get(0); String fromUriString = UriComponentsBuilder.fromUriString(uri.toString()).build().getQueryParams().get("param").get(0);
assertEquals(fromUri, fromUriString); assertEquals(fromUri, fromUriString);
} }
@Test @Test
public void fromUriString() { public void fromUriString() {
@@ -102,34 +115,35 @@ public class UriComponentsBuilderTests {
result = UriComponentsBuilder.fromUriString( result = UriComponentsBuilder.fromUriString(
"http://arjen:foobar@java.sun.com:80/javase/6/docs/api/java/util/BitSet.html?foo=bar#and(java.util.BitSet)") "http://arjen:foobar@java.sun.com:80/javase/6/docs/api/java/util/BitSet.html?foo=bar#and(java.util.BitSet)")
.build(); .build();
assertEquals("http", result.getScheme()); assertEquals("http", result.getScheme());
assertEquals("arjen:foobar", result.getUserInfo()); assertEquals("arjen:foobar", result.getUserInfo());
assertEquals("java.sun.com", result.getHost()); assertEquals("java.sun.com", result.getHost());
assertEquals(80, result.getPort()); assertEquals(80, result.getPort());
assertEquals("/javase/6/docs/api/java/util/BitSet.html", result.getPath()); assertEquals("/javase/6/docs/api/java/util/BitSet.html", result.getPath());
assertEquals("foo=bar", result.getQuery()); assertEquals("foo=bar", result.getQuery());
MultiValueMap<String, String> expectedQueryParams = new LinkedMultiValueMap<String, String>(1); MultiValueMap<String, String> expectedQueryParams = new LinkedMultiValueMap<String, String>(1);
expectedQueryParams.add("foo", "bar"); expectedQueryParams.add("foo", "bar");
assertEquals(expectedQueryParams, result.getQueryParams()); assertEquals(expectedQueryParams, result.getQueryParams());
assertEquals("and(java.util.BitSet)", result.getFragment()); assertEquals("and(java.util.BitSet)", result.getFragment());
result = UriComponentsBuilder.fromUriString("mailto:java-net@java.sun.com").build(); result = UriComponentsBuilder.fromUriString("mailto:java-net@java.sun.com#baz").build();
assertEquals("mailto", result.getScheme()); assertEquals("mailto", result.getScheme());
assertNull(result.getUserInfo()); assertNull(result.getUserInfo());
assertNull(result.getHost()); assertNull(result.getHost());
assertEquals(-1, result.getPort()); assertEquals(-1, result.getPort());
assertEquals("java-net@java.sun.com", result.getPathSegments().get(0)); assertEquals("java-net@java.sun.com", result.getSchemeSpecificPart());
assertNull(result.getQuery()); assertNull(result.getPath());
assertNull(result.getFragment()); assertNull(result.getQuery());
assertEquals("baz", result.getFragment());
result = UriComponentsBuilder.fromUriString("docs/guide/collections/designfaq.html#28").build(); result = UriComponentsBuilder.fromUriString("docs/guide/collections/designfaq.html#28").build();
assertNull(result.getScheme()); assertNull(result.getScheme());
assertNull(result.getUserInfo()); assertNull(result.getUserInfo());
assertNull(result.getHost()); assertNull(result.getHost());
assertEquals(-1, result.getPort()); assertEquals(-1, result.getPort());
assertEquals("docs/guide/collections/designfaq.html", result.getPath()); assertEquals("docs/guide/collections/designfaq.html", result.getPath());
assertNull(result.getQuery()); assertNull(result.getQuery());
assertEquals("28", result.getFragment()); assertEquals("28", result.getFragment());
} }
@@ -265,7 +279,7 @@ public class UriComponentsBuilderTests {
} }
@Test @Test
public void buildAndExpand() { public void buildAndExpandHierarchical() {
UriComponents result = UriComponentsBuilder.fromPath("/{foo}").buildAndExpand("fooValue"); UriComponents result = UriComponentsBuilder.fromPath("/{foo}").buildAndExpand("fooValue");
assertEquals("/fooValue", result.toUriString()); assertEquals("/fooValue", result.toUriString());
@@ -275,4 +289,17 @@ public class UriComponentsBuilderTests {
result = UriComponentsBuilder.fromPath("/{foo}/{bar}").buildAndExpand(values); result = UriComponentsBuilder.fromPath("/{foo}/{bar}").buildAndExpand(values);
assertEquals("/fooValue/barValue", result.toUriString()); assertEquals("/fooValue/barValue", result.toUriString());
} }
@Test
public void buildAndExpandOpaque() {
UriComponents result = UriComponentsBuilder.fromUriString("mailto:{user}@{domain}").buildAndExpand("foo", "example.com");
assertEquals("mailto:foo@example.com", result.toUriString());
Map<String, String> values = new HashMap<String, String>();
values.put("user", "foo");
values.put("domain", "example.com");
UriComponentsBuilder.fromUriString("mailto:{user}@{domain}").buildAndExpand(values);
assertEquals("mailto:foo@example.com", result.toUriString());
}
} }