Merge pull request #148 from poutsma/SPR-9798

* poutsma-SPR-9798:
  Support opaque URIs in UriComponentsBuilder
This commit is contained in:
Rossen Stoyanchev 2012-09-25 10:20:10 -04:00
commit 8a082e6e3b
6 changed files with 1312 additions and 899 deletions

View File

@ -0,0 +1,858 @@
/*
* 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.
*
* @return the 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.
*
* @return the URI string
*/
@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.
*
* @return the URI
*/
@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 o) {
if (this == o) {
return true;
}
if (!(o instanceof OpaqueUriComponents)) {
return false;
}
HierarchicalUriComponents other = (HierarchicalUriComponents) o;
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.
*
* @author Arjen Poutsma
* @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 o) {
if (this == o) {
return true;
} else if (o instanceof FullPathComponent) {
FullPathComponent other = (FullPathComponent) o;
return this.getPath().equals(other.getPath());
}
return false;
}
@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 o) {
if (this == o) {
return true;
} else if (o instanceof PathSegmentComponent) {
PathSegmentComponent other = (PathSegmentComponent) o;
return this.getPathSegments().equals(other.getPathSegments());
}
return false;
}
@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 o) {
return this == o;
}
@Override
public int hashCode() {
return 42;
}
};
}

View File

@ -0,0 +1,168 @@
/*
* 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 o) {
if (this == o) {
return true;
}
if (!(o instanceof OpaqueUriComponents)) {
return false;
}
OpaqueUriComponents other = (OpaqueUriComponents) o;
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

@ -83,6 +83,8 @@ public class UriComponentsBuilder {
private String scheme;
private String ssp;
private String userInfo;
private String host;
@ -164,16 +166,43 @@ public class UriComponentsBuilder {
if (m.matches()) {
UriComponentsBuilder builder = new UriComponentsBuilder();
builder.scheme(m.group(2));
builder.userInfo(m.group(5));
builder.host(m.group(6));
String scheme = m.group(2);
String userInfo = m.group(5);
String host = m.group(6);
String port = m.group(8);
if (StringUtils.hasLength(port)) {
builder.port(Integer.parseInt(port));
String path = m.group(9);
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.fragment(m.group(13));
builder.scheme(scheme);
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;
}
@ -244,7 +273,13 @@ public class UriComponentsBuilder {
* @return the URI components
*/
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);
}
}
/**
@ -281,25 +316,31 @@ public class UriComponentsBuilder {
*/
public UriComponentsBuilder uri(URI uri) {
Assert.notNull(uri, "'uri' must not be null");
Assert.isTrue(!uri.isOpaque(), "Opaque URI [" + uri + "] not supported");
this.scheme = uri.getScheme();
if (uri.getRawUserInfo() != null) {
this.userInfo = uri.getRawUserInfo();
if (uri.isOpaque()) {
this.ssp = uri.getRawSchemeSpecificPart();
resetHierarchicalComponents();
}
if (uri.getHost() != null) {
this.host = uri.getHost();
}
if (uri.getPort() != -1) {
this.port = uri.getPort();
}
if (StringUtils.hasLength(uri.getRawPath())) {
this.pathBuilder = new FullPathComponentBuilder(uri.getRawPath());
}
if (StringUtils.hasLength(uri.getRawQuery())) {
this.queryParams.clear();
query(uri.getRawQuery());
else {
if (uri.getRawUserInfo() != null) {
this.userInfo = uri.getRawUserInfo();
}
if (uri.getHost() != null) {
this.host = uri.getHost();
}
if (uri.getPort() != -1) {
this.port = uri.getPort();
}
if (StringUtils.hasLength(uri.getRawPath())) {
this.pathBuilder = new FullPathComponentBuilder(uri.getRawPath());
}
if (StringUtils.hasLength(uri.getRawQuery())) {
this.queryParams.clear();
query(uri.getRawQuery());
}
resetSchemeSpecificPart();
}
if (uri.getRawFragment() != null) {
this.fragment = uri.getRawFragment();
@ -307,6 +348,18 @@ public class UriComponentsBuilder {
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 scheme of this builder.
@ -320,17 +373,32 @@ public class UriComponentsBuilder {
return this;
}
/**
* Set the URI scheme-specific-part. When invoked, this method overwrites
* {@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
*/
public UriComponentsBuilder userInfo(String userInfo) {
this.userInfo = userInfo;
resetSchemeSpecificPart();
return this;
}
@ -338,12 +406,12 @@ public class UriComponentsBuilder {
* Sets the URI host. The given host may contain URI template variables, and
* 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
*/
public UriComponentsBuilder host(String host) {
this.host = host;
resetSchemeSpecificPart();
return this;
}
@ -356,6 +424,7 @@ public class UriComponentsBuilder {
public UriComponentsBuilder port(int port) {
Assert.isTrue(port >= -1, "'port' must not be < -1");
this.port = port;
resetSchemeSpecificPart();
return this;
}
@ -363,8 +432,7 @@ public class UriComponentsBuilder {
* 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
*/
public UriComponentsBuilder path(String path) {
@ -374,6 +442,7 @@ public class UriComponentsBuilder {
else {
this.pathBuilder = NULL_PATH_COMPONENT_BUILDER;
}
resetSchemeSpecificPart();
return this;
}
@ -386,6 +455,7 @@ public class UriComponentsBuilder {
public UriComponentsBuilder replacePath(String path) {
this.pathBuilder = NULL_PATH_COMPONENT_BUILDER;
path(path);
resetSchemeSpecificPart();
return this;
}
@ -399,6 +469,7 @@ public class UriComponentsBuilder {
public UriComponentsBuilder pathSegment(String... pathSegments) throws IllegalArgumentException {
Assert.notNull(pathSegments, "'segments' must not be null");
this.pathBuilder = this.pathBuilder.appendPathSegments(pathSegments);
resetSchemeSpecificPart();
return this;
}
@ -432,6 +503,7 @@ public class UriComponentsBuilder {
else {
this.queryParams.clear();
}
resetSchemeSpecificPart();
return this;
}
@ -444,6 +516,7 @@ public class UriComponentsBuilder {
public UriComponentsBuilder replaceQuery(String query) {
this.queryParams.clear();
query(query);
resetSchemeSpecificPart();
return this;
}
@ -470,6 +543,7 @@ public class UriComponentsBuilder {
else {
this.queryParams.add(name, null);
}
resetSchemeSpecificPart();
return this;
}
@ -490,6 +564,7 @@ public class UriComponentsBuilder {
if (!ObjectUtils.isEmpty(values)) {
queryParam(name, values);
}
resetSchemeSpecificPart();
return this;
}
@ -514,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 {
UriComponents.PathComponent build();
HierarchicalUriComponents.PathComponent build();
PathComponentBuilder appendPath(String path);
@ -536,8 +611,8 @@ public class UriComponentsBuilder {
this.path = new StringBuilder(path);
}
public UriComponents.PathComponent build() {
return new UriComponents.FullPathComponent(path.toString());
public HierarchicalUriComponents.PathComponent build() {
return new HierarchicalUriComponents.FullPathComponent(path.toString());
}
public PathComponentBuilder appendPath(String path) {
@ -573,8 +648,8 @@ public class UriComponentsBuilder {
return result;
}
public UriComponents.PathComponent build() {
return new UriComponents.PathSegmentComponent(pathSegments);
public HierarchicalUriComponents.PathComponent build() {
return new HierarchicalUriComponents.PathSegmentComponent(pathSegments);
}
public PathComponentBuilder appendPath(String path) {
@ -600,14 +675,14 @@ public class UriComponentsBuilder {
pathComponentBuilders.add(builder);
}
public UriComponents.PathComponent build() {
List<UriComponents.PathComponent> pathComponents =
new ArrayList<UriComponents.PathComponent>(pathComponentBuilders.size());
public HierarchicalUriComponents.PathComponent build() {
List<HierarchicalUriComponents.PathComponent> pathComponents =
new ArrayList<HierarchicalUriComponents.PathComponent>(pathComponentBuilders.size());
for (PathComponentBuilder pathComponentBuilder : pathComponentBuilders) {
pathComponents.add(pathComponentBuilder.build());
}
return new UriComponents.PathComponentComposite(pathComponents);
return new HierarchicalUriComponents.PathComponentComposite(pathComponents);
}
public PathComponentBuilder appendPath(String path) {
@ -627,8 +702,8 @@ public class UriComponentsBuilder {
*/
private static PathComponentBuilder NULL_PATH_COMPONENT_BUILDER = new PathComponentBuilder() {
public UriComponents.PathComponent build() {
return UriComponents.NULL_PATH_COMPONENT;
public HierarchicalUriComponents.PathComponent build() {
return HierarchicalUriComponents.NULL_PATH_COMPONENT;
}
public PathComponentBuilder appendPath(String path) {

View File

@ -83,6 +83,7 @@ public abstract class UriUtils {
* @throws UnsupportedEncodingException when the given encoding parameter is not supported
* @deprecated in favor of {@link UriComponentsBuilder}; see note about query param encoding
*/
@Deprecated
public static String encodeUri(String uri, String encoding) throws UnsupportedEncodingException {
Assert.notNull(uri, "'uri' must not be null");
Assert.hasLength(encoding, "'encoding' must not be empty");
@ -123,6 +124,7 @@ public abstract class UriUtils {
* @throws UnsupportedEncodingException when the given encoding parameter is not supported
* @deprecated in favor of {@link UriComponentsBuilder}; see note about query param encoding
*/
@Deprecated
public static String encodeHttpUrl(String httpUrl, String encoding) throws UnsupportedEncodingException {
Assert.notNull(httpUrl, "'httpUrl' must not be null");
Assert.hasLength(encoding, "'encoding' must not be empty");
@ -160,6 +162,7 @@ public abstract class UriUtils {
* @throws UnsupportedEncodingException when the given encoding parameter is not supported
* @deprecated in favor of {@link UriComponentsBuilder}
*/
@Deprecated
public static String encodeUriComponents(String scheme, String authority, String userInfo,
String host, String port, String path, String query, String fragment, String encoding)
throws UnsupportedEncodingException {
@ -213,7 +216,8 @@ public abstract class UriUtils {
* @throws UnsupportedEncodingException when the given encoding parameter is not supported
*/
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 +228,8 @@ public abstract class UriUtils {
* @throws UnsupportedEncodingException when the given encoding parameter is not supported
*/
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 +240,8 @@ public abstract class UriUtils {
* @throws UnsupportedEncodingException when the given encoding parameter is not supported
*/
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 +252,8 @@ public abstract class UriUtils {
* @throws UnsupportedEncodingException when the given encoding parameter is not supported
*/
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 +264,8 @@ public abstract class UriUtils {
* @throws UnsupportedEncodingException when the given encoding parameter is not supported
*/
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 +276,8 @@ public abstract class UriUtils {
* @throws UnsupportedEncodingException when the given encoding parameter is not supported
*/
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 +288,8 @@ public abstract class UriUtils {
* @throws UnsupportedEncodingException when the given encoding parameter is not supported
*/
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 +300,8 @@ public abstract class UriUtils {
* @throws UnsupportedEncodingException when the given encoding parameter is not supported
*/
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 +312,8 @@ public abstract class UriUtils {
* @throws UnsupportedEncodingException when the given encoding parameter is not supported
*/
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 +324,8 @@ public abstract class UriUtils {
* @throws UnsupportedEncodingException when the given encoding parameter is not supported
*/
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);
}

View File

@ -64,7 +64,7 @@ public class UriComponentsBuilderTests {
}
@Test
public void fromUri() throws URISyntaxException {
public void fromHierarchicalUri() throws URISyntaxException {
URI uri = new URI("http://example.com/foo?bar#baz");
UriComponents result = UriComponentsBuilder.fromUri(uri).build();
assertEquals("http", result.getScheme());
@ -76,6 +76,17 @@ public class UriComponentsBuilderTests {
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());
}
// SPR-9317
@Test
@ -113,14 +124,15 @@ public class UriComponentsBuilderTests {
assertEquals(expectedQueryParams, result.getQueryParams());
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());
assertNull(result.getUserInfo());
assertNull(result.getHost());
assertEquals(-1, result.getPort());
assertEquals("java-net@java.sun.com", result.getPathSegments().get(0));
assertEquals("java-net@java.sun.com", result.getSchemeSpecificPart());
assertNull(result.getPath());
assertNull(result.getQuery());
assertNull(result.getFragment());
assertEquals("baz", result.getFragment());
result = UriComponentsBuilder.fromUriString("docs/guide/collections/designfaq.html#28").build();
assertNull(result.getScheme());
@ -265,7 +277,7 @@ public class UriComponentsBuilderTests {
}
@Test
public void buildAndExpand() {
public void buildAndExpandHierarchical() {
UriComponents result = UriComponentsBuilder.fromPath("/{foo}").buildAndExpand("fooValue");
assertEquals("/fooValue", result.toUriString());
@ -275,4 +287,17 @@ public class UriComponentsBuilderTests {
result = UriComponentsBuilder.fromPath("/{foo}/{bar}").buildAndExpand(values);
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());
}
}