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()); + } + }