diff --git a/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java b/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java
new file mode 100644
index 00000000000..a4a46ba2c0f
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java
@@ -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 Hierarchical URIs
+ */
+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 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 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(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 getPathSegments() {
+ return this.path.getPathSegments();
+ }
+
+ @Override
+ public String getQuery() {
+ if (!this.queryParams.isEmpty()) {
+ StringBuilder queryBuilder = new StringBuilder();
+ for (Map.Entry> entry : this.queryParams.entrySet()) {
+ String name = entry.getKey();
+ List 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 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 encodedQueryParams =
+ new LinkedMultiValueMap(this.queryParams.size());
+ for (Map.Entry> entry : this.queryParams.entrySet()) {
+ String encodedName = encodeUriComponent(entry.getKey(), encoding, Type.QUERY_PARAM);
+ List encodedValues = new ArrayList(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> 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 expandedQueryParams =
+ new LinkedMultiValueMap(this.queryParams.size());
+ for (Map.Entry> entry : this.queryParams.entrySet()) {
+ String expandedName = expandUriComponent(entry.getKey(), uriVariables);
+ List expandedValues = new ArrayList(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.
+ *
+ * Contains methods to indicate whether a given character is valid in a specific URI component.
+ *
+ * @author Arjen Poutsma
+ * @see RFC 3986
+ */
+ 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 RFC 3986, appendix 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 RFC 3986, appendix A
+ */
+ protected boolean isDigit(int c) {
+ return c >= '0' && c <= '9';
+ }
+
+ /**
+ * Indicates whether the given character is in the {@code gen-delims} set.
+ *
+ * @see RFC 3986, appendix 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 RFC 3986, appendix 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 RFC 3986, appendix A
+ */
+ protected boolean isReserved(char c) {
+ return isGenericDelimiter(c) || isReserved(c);
+ }
+
+ /**
+ * Indicates whether the given character is in the {@code unreserved} set.
+ *
+ * @see RFC 3986, appendix 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 RFC 3986, appendix A
+ */
+ protected boolean isPchar(int c) {
+ return isUnreserved(c) || isSubDelimiter(c) || ':' == c || '@' == c;
+ }
+
+ }
+
+ /**
+ * Defines the contract for path (segments).
+ */
+ interface PathComponent {
+
+ String getPath();
+
+ List 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 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 pathSegments;
+
+ PathSegmentComponent(List pathSegments) {
+ this.pathSegments = Collections.unmodifiableList(pathSegments);
+ }
+
+ public String getPath() {
+ StringBuilder pathBuilder = new StringBuilder();
+ pathBuilder.append(PATH_DELIMITER);
+ for (Iterator 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 getPathSegments() {
+ return this.pathSegments;
+ }
+
+ public PathComponent encode(String encoding) throws UnsupportedEncodingException {
+ List pathSegments = getPathSegments();
+ List encodedPathSegments = new ArrayList(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 pathSegments = getPathSegments();
+ List expandedPathSegments = new ArrayList(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 pathComponents;
+
+ PathComponentComposite(List 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 getPathSegments() {
+ List result = new ArrayList();
+ for (PathComponent pathComponent : this.pathComponents) {
+ result.addAll(pathComponent.getPathSegments());
+ }
+ return result;
+ }
+
+ public PathComponent encode(String encoding) throws UnsupportedEncodingException {
+ List encodedComponents = new ArrayList(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 expandedComponents = new ArrayList(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 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;
+ }
+
+ };
+
+}
diff --git a/spring-web/src/main/java/org/springframework/web/util/OpaqueUriComponents.java b/spring-web/src/main/java/org/springframework/web/util/OpaqueUriComponents.java
new file mode 100644
index 00000000000..fb07addf8ff
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/web/util/OpaqueUriComponents.java
@@ -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 Hierarchical vs Opaque URIs
+ */
+final class OpaqueUriComponents extends UriComponents {
+
+ private static final MultiValueMap QUERY_PARAMS_NONE = new LinkedMultiValueMap(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 getPathSegments() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public String getQuery() {
+ return null;
+ }
+
+ @Override
+ public MultiValueMap 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;
+ }
+
+}
diff --git a/spring-web/src/main/java/org/springframework/web/util/UriComponents.java b/spring-web/src/main/java/org/springframework/web/util/UriComponents.java
index 54cbcef437e..85605c972c0 100644
--- a/spring-web/src/main/java/org/springframework/web/util/UriComponents.java
+++ b/spring-web/src/main/java/org/springframework/web/util/UriComponents.java
@@ -16,13 +16,9 @@
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;
@@ -30,405 +26,176 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.util.Assert;
-import org.springframework.util.CollectionUtils;
-import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
-import org.springframework.util.StringUtils;
/**
- * Represents an immutable collection of URI components, mapping component type to string values. Contains convenience
- * getters for all components. Effectively similar to {@link URI}, but with more powerful encoding options and support
- * for URI template variables.
+ * Represents an immutable collection of URI components, mapping component type to string
+ * values. Contains convenience getters for all components. Effectively similar to {@link
+ * java.net.URI}, but with more powerful encoding options and support for URI template
+ * variables.
*
* @author Arjen Poutsma
- * @since 3.1
* @see UriComponentsBuilder
+ * @since 3.1
*/
-public final class UriComponents {
+public abstract class UriComponents {
- private static final String DEFAULT_ENCODING = "UTF-8";
-
- private static final char PATH_DELIMITER = '/';
+ private static final String DEFAULT_ENCODING = "UTF-8";
/** Captures URI template variable names. */
private static final Pattern NAMES_PATTERN = Pattern.compile("\\{([^/]+?)\\}");
private final String scheme;
- private final String userInfo;
-
- private final String host;
-
- private final int port;
-
- private final PathComponent path;
-
- private final MultiValueMap queryParams;
-
private final String fragment;
- private final boolean encoded;
-
- /**
- * Package-friendly constructor that creates a new {@code UriComponents} instance from the given parameters. All
- * parameters 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 component
- * @param queryParams the query parameters
- * @param fragment the fragment
- * @param encoded whether the components are encoded
- * @param verify whether the components need to be verified to determine whether they contain illegal characters
- */
- UriComponents(String scheme,
- String userInfo,
- String host,
- int port,
- PathComponent path,
- MultiValueMap queryParams,
- String fragment,
- boolean encoded,
- boolean verify) {
+ protected UriComponents(String scheme, String fragment) {
this.scheme = scheme;
- 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(0));
this.fragment = fragment;
- this.encoded = encoded;
- if (verify) {
- verify();
- }
}
// component getters
- /**
- * Returns the scheme.
- *
- * @return the scheme. Can be {@code null}.
- */
- public String getScheme() {
+ /**
+ * Returns the scheme.
+ *
+ * @return the scheme. Can be {@code null}.
+ */
+ public final String getScheme() {
return scheme;
- }
+ }
- /**
- * Returns the user info.
- *
- * @return the user info. Can be {@code null}.
- */
- public String getUserInfo() {
- return userInfo;
- }
+ /**
+ * Returns the scheme specific part.
+ *
+ * @retur the scheme specific part. Can be {@code null}.
+ */
+ public abstract String getSchemeSpecificPart();
- /**
- * Returns the host.
- *
- * @return the host. Can be {@code null}.
- */
- public String getHost() {
- return host;
- }
+ /**
+ * Returns the user info.
+ *
+ * @return the user info. Can be {@code null}.
+ */
+ public abstract String getUserInfo();
- /**
- * Returns the port. Returns {@code -1} if no port has been set.
- *
- * @return the port
- */
- public int getPort() {
- return port;
- }
+ /**
+ * Returns the host.
+ *
+ * @return the host. Can be {@code null}.
+ */
+ public abstract String getHost();
+
+ /**
+ * Returns the port. Returns {@code -1} if no port has been set.
+ *
+ * @return the port
+ */
+ public abstract int getPort();
/**
* Returns the path.
*
* @return the path. Can be {@code null}.
*/
- public String getPath() {
- return path.getPath();
- }
+ public abstract String getPath();
- /**
- * Returns the list of path segments.
- *
- * @return the path segments. Empty if no path has been set.
- */
- public List getPathSegments() {
- return path.getPathSegments();
- }
+ /**
+ * Returns the list of path segments.
+ *
+ * @return the path segments. Empty if no path has been set.
+ */
+ public abstract List getPathSegments();
/**
* Returns the query.
*
* @return the query. Can be {@code null}.
*/
- public String getQuery() {
- if (!queryParams.isEmpty()) {
- StringBuilder queryBuilder = new StringBuilder();
- for (Map.Entry> entry : queryParams.entrySet()) {
- String name = entry.getKey();
- List 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;
- }
- }
+ public abstract String getQuery();
/**
- * Returns the map of query parameters.
- *
- * @return the query parameters. Empty if no query has been set.
- */
- public MultiValueMap getQueryParams() {
- return queryParams;
- }
+ * Returns the map of query parameters.
+ *
+ * @return the query parameters. Empty if no query has been set.
+ */
+ public abstract MultiValueMap getQueryParams();
- /**
- * Returns the fragment.
- *
- * @return the fragment. Can be {@code null}.
- */
- public String getFragment() {
+ /**
+ * Returns the fragment.
+ *
+ * @return the fragment. Can be {@code null}.
+ */
+ public final String getFragment() {
return fragment;
- }
+ }
- // encoding
+ // encoding
/**
- * Encodes all URI components using their specific encoding rules, and returns the result as a new
- * {@code UriComponents} instance. This method uses UTF-8 to encode.
+ * Encodes all URI components using their specific encoding rules, and returns the result
+ * as a new {@code UriComponents} instance. This method uses UTF-8 to encode.
*
* @return the encoded uri components
*/
- public UriComponents encode() {
- try {
- return encode(DEFAULT_ENCODING);
- }
- catch (UnsupportedEncodingException e) {
- throw new InternalError("\"" + DEFAULT_ENCODING + "\" not supported");
- }
- }
-
- /**
- * 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
- */
- public UriComponents encode(String encoding) throws UnsupportedEncodingException {
- Assert.hasLength(encoding, "'encoding' must not be empty");
-
- if (encoded) {
- return this;
- }
-
- String encodedScheme = encodeUriComponent(this.scheme, encoding, Type.SCHEME);
- String encodedUserInfo = encodeUriComponent(this.userInfo, encoding, Type.USER_INFO);
- String encodedHost = encodeUriComponent(this.host, encoding, Type.HOST);
- PathComponent encodedPath = path.encode(encoding);
- MultiValueMap encodedQueryParams =
- new LinkedMultiValueMap(this.queryParams.size());
- for (Map.Entry> entry : this.queryParams.entrySet()) {
- String encodedName = encodeUriComponent(entry.getKey(), encoding, Type.QUERY_PARAM);
- List encodedValues = new ArrayList(entry.getValue().size());
- for (String value : entry.getValue()) {
- String encodedValue = encodeUriComponent(value, encoding, Type.QUERY_PARAM);
- encodedValues.add(encodedValue);
- }
- encodedQueryParams.put(encodedName, encodedValues);
+ public final UriComponents encode() {
+ try {
+ return encode(DEFAULT_ENCODING);
}
- String encodedFragment = encodeUriComponent(this.fragment, encoding, Type.FRAGMENT);
-
- return new UriComponents(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;
+ catch (UnsupportedEncodingException e) {
+ throw new InternalError("\"" + DEFAULT_ENCODING + "\" not supported");
}
-
- 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.
+ * Encodes all URI components using their specific encoding rules, and
+ * returns the result as a new {@code UriComponents} instance.
*
- * @throws IllegalArgumentException if any of the components contain illegal characters
+ * @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
*/
- private void verify() {
- if (!encoded) {
- return;
- }
- verifyUriComponent(scheme, Type.SCHEME);
- verifyUriComponent(userInfo, Type.USER_INFO);
- verifyUriComponent(host, Type.HOST);
- path.verify();
- for (Map.Entry> entry : queryParams.entrySet()) {
- verifyUriComponent(entry.getKey(), Type.QUERY_PARAM);
- for (String value : entry.getValue()) {
- verifyUriComponent(value, Type.QUERY_PARAM);
- }
- }
- verifyUriComponent(fragment, 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 + "\"");
- }
- }
- }
+ public abstract UriComponents encode(String encoding) throws UnsupportedEncodingException;
// expanding
/**
- * Replaces all URI template variables with the values from a given map. The map keys represent
- * variable names; the values variable values. The order of variables is not significant.
-
+ * Replaces all URI template variables with the values from a given map. The map keys
+ * represent variable names; the values variable values. The order of variables is not
+ * significant.
+ *
* @param uriVariables the map of URI variables
* @return the expanded uri components
*/
- public UriComponents expand(Map uriVariables) {
+ public final UriComponents expand(Map uriVariables) {
Assert.notNull(uriVariables, "'uriVariables' must not be null");
return expandInternal(new MapTemplateVariables(uriVariables));
}
/**
- * Replaces all URI template variables with the values from a given array. The array represent variable values.
- * The order of variables is significant.
-
+ * Replaces all URI template variables with the values from a given array. The array
+ * represent variable values. The order of variables is significant.
+ *
* @param uriVariableValues URI variable values
* @return the expanded uri components
*/
- public UriComponents expand(Object... uriVariableValues) {
+ public final UriComponents expand(Object... uriVariableValues) {
Assert.notNull(uriVariableValues, "'uriVariableValues' must not be null");
return expandInternal(new VarArgsTemplateVariables(uriVariableValues));
}
- private UriComponents expandInternal(UriTemplateVariables uriVariables) {
- Assert.state(!encoded, "Cannot expand an already encoded UriComponents object");
-
- String expandedScheme = expandUriComponent(this.scheme, uriVariables);
- String expandedUserInfo = expandUriComponent(this.userInfo, uriVariables);
- String expandedHost = expandUriComponent(this.host, uriVariables);
- PathComponent expandedPath = path.expand(uriVariables);
- MultiValueMap expandedQueryParams =
- new LinkedMultiValueMap(this.queryParams.size());
- for (Map.Entry> entry : this.queryParams.entrySet()) {
- String expandedName = expandUriComponent(entry.getKey(), uriVariables);
- List expandedValues = new ArrayList(entry.getValue().size());
- for (String value : entry.getValue()) {
- String expandedValue = expandUriComponent(value, uriVariables);
- expandedValues.add(expandedValue);
- }
- expandedQueryParams.put(expandedName, expandedValues);
- }
- String expandedFragment = expandUriComponent(this.fragment, uriVariables);
-
- return new UriComponents(expandedScheme, expandedUserInfo, expandedHost, this.port, expandedPath,
- expandedQueryParams, expandedFragment, false, false);
- }
+ /**
+ * Replaces all URI template variables with the values from the given {@link
+ * UriTemplateVariables}
+ *
+ * @param uriVariables URI template values
+ * @return the expanded uri components
+ */
+ abstract UriComponents expandInternal(UriTemplateVariables uriVariables);
- private static String expandUriComponent(String source, UriTemplateVariables uriVariables) {
+ static String expandUriComponent(String source, UriTemplateVariables uriVariables) {
if (source == null) {
return null;
}
@@ -458,536 +225,41 @@ public final class UriComponents {
return variableValue != null ? variableValue.toString() : "";
}
- /**
- * Normalize the path removing sequences like "path/..".
- * @see StringUtils#cleanPath(String)
- */
- public UriComponents normalize() {
- String normalizedPath = StringUtils.cleanPath(getPath());
- return new UriComponents(scheme, userInfo, host, this.port, new FullPathComponent(normalizedPath),
- queryParams, fragment, encoded, false);
- }
-
- // other functionality
-
- /**
- * Returns a URI string from this {@code UriComponents} instance.
- *
- * @return the URI string
- */
- public String toUriString() {
- StringBuilder uriBuilder = new StringBuilder();
-
- if (scheme != null) {
- uriBuilder.append(scheme);
- uriBuilder.append(':');
- }
-
- if (userInfo != null || host != null) {
- uriBuilder.append("//");
- if (userInfo != null) {
- uriBuilder.append(userInfo);
- uriBuilder.append('@');
- }
- if (host != null) {
- uriBuilder.append(host);
- }
- if (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 (fragment != null) {
- uriBuilder.append('#');
- uriBuilder.append(fragment);
- }
-
- return uriBuilder.toString();
- }
-
- /**
- * Returns a {@code URI} from this {@code UriComponents} instance.
- *
- * @return the URI
- */
- public URI toUri() {
- try {
- if (encoded) {
- return new URI(toUriString());
- }
- 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 UriComponents) {
- UriComponents other = (UriComponents) o;
-
- if (scheme != null ? !scheme.equals(other.scheme) : other.scheme != null) {
- return false;
- }
- if (userInfo != null ? !userInfo.equals(other.userInfo) : other.userInfo != null) {
- return false;
- }
- if (host != null ? !host.equals(other.host) : other.host != null) {
- return false;
- }
- if (port != other.port) {
- return false;
- }
- if (!path.equals(other.path)) {
- return false;
- }
- if (!queryParams.equals(other.queryParams)) {
- return false;
- }
- if (fragment != null ? !fragment.equals(other.fragment) : other.fragment != null) {
- return false;
- }
- return true;
- }
- else {
- return false;
- }
- }
-
- @Override
- public int hashCode() {
- int result = scheme != null ? scheme.hashCode() : 0;
- result = 31 * result + (userInfo != null ? userInfo.hashCode() : 0);
- result = 31 * result + (host != null ? host.hashCode() : 0);
- result = 31 * result + port;
- result = 31 * result + path.hashCode();
- result = 31 * result + queryParams.hashCode();
- result = 31 * result + (fragment != null ? fragment.hashCode() : 0);
- return result;
- }
-
- @Override
- public String toString() {
- return toUriString();
- }
-
- // inner types
-
- /**
- * Enumeration used to identify the parts of a URI.
- *
- * Contains methods to indicate whether a given character is valid in a specific URI component.
- *
- * @author Arjen Poutsma
- * @see RFC 3986
- */
- 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 RFC 3986, appendix 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 RFC 3986, appendix A
- */
- protected boolean isDigit(int c) {
- return c >= '0' && c <= '9';
- }
-
- /**
- * Indicates whether the given character is in the {@code gen-delims} set.
- *
- * @see RFC 3986, appendix 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 RFC 3986, appendix 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 RFC 3986, appendix A
- */
- protected boolean isReserved(char c) {
- return isGenericDelimiter(c) || isReserved(c);
- }
-
- /**
- * Indicates whether the given character is in the {@code unreserved} set.
- *
- * @see RFC 3986, appendix 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 RFC 3986, appendix A
- */
- protected boolean isPchar(int c) {
- return isUnreserved(c) || isSubDelimiter(c) || ':' == c || '@' == c;
- }
-
- }
-
/**
- * Defines the contract for path (segments).
+ * Returns a URI string from this {@code UriComponents} instance.
+ *
+ * @return the URI string
*/
- interface PathComponent {
-
- String getPath();
-
- List getPathSegments();
-
- PathComponent encode(String encoding) throws UnsupportedEncodingException;
-
- void verify();
-
- PathComponent expand(UriTemplateVariables uriVariables);
-
- }
+ public abstract String toUriString();
/**
- * 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 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(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 pathSegments;
-
- PathSegmentComponent(List pathSegments) {
- this.pathSegments = Collections.unmodifiableList(pathSegments);
- }
-
- public String getPath() {
- StringBuilder pathBuilder = new StringBuilder();
- pathBuilder.append(PATH_DELIMITER);
- for (Iterator iterator = pathSegments.iterator(); iterator.hasNext(); ) {
- String pathSegment = iterator.next();
- pathBuilder.append(pathSegment);
- if (iterator.hasNext()) {
- pathBuilder.append(PATH_DELIMITER);
- }
- }
- return pathBuilder.toString();
- }
-
- public List getPathSegments() {
- return pathSegments;
- }
-
- public PathComponent encode(String encoding) throws UnsupportedEncodingException {
- List pathSegments = getPathSegments();
- List encodedPathSegments = new ArrayList(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 pathSegments = getPathSegments();
- List expandedPathSegments = new ArrayList(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.
+ * Returns a {@code URI} from this {@code UriComponents} instance.
+ *
+ * @return the URI
*/
- final static class PathComponentComposite implements PathComponent {
-
- private final List pathComponents;
-
- PathComponentComposite(List pathComponents) {
- this.pathComponents = pathComponents;
- }
-
- public String getPath() {
- StringBuilder pathBuilder = new StringBuilder();
- for (PathComponent pathComponent : pathComponents) {
- pathBuilder.append(pathComponent.getPath());
- }
- return pathBuilder.toString();
- }
-
- public List getPathSegments() {
- List result = new ArrayList();
- for (PathComponent pathComponent : pathComponents) {
- result.addAll(pathComponent.getPathSegments());
- }
- return result;
- }
-
- public PathComponent encode(String encoding) throws UnsupportedEncodingException {
- List encodedComponents = new ArrayList(pathComponents.size());
- for (PathComponent pathComponent : pathComponents) {
- encodedComponents.add(pathComponent.encode(encoding));
- }
- return new PathComponentComposite(encodedComponents);
- }
+ public abstract URI toUri();
- public void verify() {
- for (PathComponent pathComponent : pathComponents) {
- pathComponent.verify();
- }
- }
-
- public PathComponent expand(UriTemplateVariables uriVariables) {
- List expandedComponents = new ArrayList(pathComponents.size());
- for (PathComponent pathComponent : pathComponents) {
- expandedComponents.add(pathComponent.expand(uriVariables));
- }
- return new PathComponentComposite(expandedComponents);
- }
+ @Override
+ public final String toString() {
+ return toUriString();
}
-
-
/**
- * Represents an empty path.
+ * Normalize the path removing sequences like "path/..".
+ *
+ * @see org.springframework.util.StringUtils#cleanPath(String)
*/
- final static PathComponent NULL_PATH_COMPONENT = new PathComponent() {
-
- public String getPath() {
- return null;
- }
-
- public List 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;
- }
-
- };
+ public abstract UriComponents normalize();
/**
* Defines the contract for URI Template variables
*
- * @see UriComponents#expand
+ * @see HierarchicalUriComponents#expand
*/
- private interface UriTemplateVariables {
+ interface UriTemplateVariables {
Object getValue(String name);
-
}
/**
@@ -1022,10 +294,12 @@ public final class UriComponents {
public Object getValue(String name) {
if (!valueIterator.hasNext()) {
- throw new IllegalArgumentException("Not enough variable values available to expand '" + name + "'");
+ throw new IllegalArgumentException(
+ "Not enough variable values available to expand '" + name + "'");
}
return valueIterator.next();
}
}
+
}
diff --git a/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java b/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java
index e54c05dba5d..bddba4c848f 100644
--- a/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java
+++ b/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java
@@ -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.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.path(m.group(9));
- builder.query(m.group(11));
- builder.fragment(m.group(13));
+ 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.getHost() != null) {
- this.host = uri.getHost();
- }
- if (uri.getPort() != -1) {
- this.port = uri.getPort();
+ if (uri.isOpaque()) {
+ this.ssp = uri.getRawSchemeSpecificPart();
+ resetHierarchicalComponents();
}
- 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 pathComponents =
- new ArrayList(pathComponentBuilders.size());
+ public HierarchicalUriComponents.PathComponent build() {
+ List pathComponents =
+ new ArrayList(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) {
diff --git a/spring-web/src/main/java/org/springframework/web/util/UriUtils.java b/spring-web/src/main/java/org/springframework/web/util/UriUtils.java
index 4d6c2a44997..9498f91a081 100644
--- a/spring-web/src/main/java/org/springframework/web/util/UriUtils.java
+++ b/spring-web/src/main/java/org/springframework/web/util/UriUtils.java
@@ -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);
}
diff --git a/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java b/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java
index 3fefe43ebc7..378c8704fbe 100644
--- a/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java
+++ b/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java
@@ -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 values = new HashMap();
+ values.put("user", "foo");
+ values.put("domain", "example.com");
+ UriComponentsBuilder.fromUriString("mailto:{user}@{domain}").buildAndExpand(values);
+ assertEquals("mailto:foo@example.com", result.toUriString());
+ }
+
}