diff --git a/org.springframework.web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java b/org.springframework.web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java new file mode 100644 index 0000000000..c1476d053b --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java @@ -0,0 +1,836 @@ +/* + * Copyright 2002-2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.util; + +import java.io.ByteArrayOutputStream; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * Extension of {@link UriComponents} for hierarchical URIs. + * + * @author Arjen Poutsma + * @since 3.2 + * @see 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. 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. + */ + @Override + public String toUriString() { + StringBuilder uriBuilder = new StringBuilder(); + + if (getScheme() != null) { + uriBuilder.append(getScheme()); + uriBuilder.append(':'); + } + + if (this.userInfo != null || this.host != null) { + uriBuilder.append("//"); + if (this.userInfo != null) { + uriBuilder.append(this.userInfo); + uriBuilder.append('@'); + } + if (this.host != null) { + uriBuilder.append(host); + } + if (this.port != -1) { + uriBuilder.append(':'); + uriBuilder.append(port); + } + } + + String path = getPath(); + if (StringUtils.hasLength(path)) { + if (uriBuilder.length() != 0 && path.charAt(0) != PATH_DELIMITER) { + uriBuilder.append(PATH_DELIMITER); + } + uriBuilder.append(path); + } + + String query = getQuery(); + if (query != null) { + uriBuilder.append('?'); + uriBuilder.append(query); + } + + if (getFragment() != null) { + uriBuilder.append('#'); + uriBuilder.append(getFragment()); + } + + return uriBuilder.toString(); + } + + /** + * Returns a {@code URI} from this {@code UriComponents} instance. + */ + @Override + public URI toUri() { + try { + if (this.encoded) { + return new URI(toString()); + } + else { + String path = getPath(); + if (StringUtils.hasLength(path) && path.charAt(0) != PATH_DELIMITER) { + path = PATH_DELIMITER + path; + } + return new URI(getScheme(), getUserInfo(), getHost(), getPort(), path, getQuery(), + getFragment()); + } + } + catch (URISyntaxException ex) { + throw new IllegalStateException("Could not create URI object: " + ex.getMessage(), ex); + } + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof OpaqueUriComponents)) { + return false; + } + + HierarchicalUriComponents other = (HierarchicalUriComponents) obj; + + if (ObjectUtils.nullSafeEquals(getScheme(), other.getScheme())) { + return false; + } + if (ObjectUtils.nullSafeEquals(getUserInfo(), other.getUserInfo())) { + return false; + } + if (ObjectUtils.nullSafeEquals(getHost(), other.getHost())) { + return false; + } + if (this.port != other.port) { + return false; + } + if (!this.path.equals(other.path)) { + return false; + } + if (!this.queryParams.equals(other.queryParams)) { + return false; + } + if (ObjectUtils.nullSafeEquals(getFragment(), other.getFragment())) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + int result = ObjectUtils.nullSafeHashCode(getScheme()); + result = 31 * result + ObjectUtils.nullSafeHashCode(this.userInfo); + result = 31 * result + ObjectUtils.nullSafeHashCode(this.host); + result = 31 * result + this.port; + result = 31 * result + this.path.hashCode(); + result = 31 * result + this.queryParams.hashCode(); + result = 31 * result + ObjectUtils.nullSafeHashCode(getFragment()); + return result; + } + + + // inner types + + /** + * Enumeration used to identify the parts of a URI. + *

Contains methods to indicate whether a given character is valid in a specific URI component. + * @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 obj) { + return (this == obj || (obj instanceof FullPathComponent && + getPath().equals(((FullPathComponent) obj).getPath()))); + } + + @Override + public int hashCode() { + return getPath().hashCode(); + } + } + + /** + * Represents a path backed by a string list (i.e. path segments). + */ + final static class PathSegmentComponent implements PathComponent { + + private final List 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 obj) { + return (this == obj || (obj instanceof PathSegmentComponent && + getPathSegments().equals(((PathSegmentComponent) obj).getPathSegments()))); + } + + @Override + public int hashCode() { + return getPathSegments().hashCode(); + } + + } + + /** + * Represents a collection of PathComponents. + */ + final static class PathComponentComposite implements PathComponent { + + private final List 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 obj) { + return (this == obj); + } + + @Override + public int hashCode() { + return 42; + } + + }; + +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/util/OpaqueUriComponents.java b/org.springframework.web/src/main/java/org/springframework/web/util/OpaqueUriComponents.java new file mode 100644 index 0000000000..d896518b29 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/util/OpaqueUriComponents.java @@ -0,0 +1,169 @@ +/* + * Copyright 2002-2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.util; + +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collections; +import java.util.List; + +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.ObjectUtils; + +/** + * Extension of {@link UriComponents} for opaque URIs. + * + * @author Arjen Poutsma + * @since 3.2 + * @see 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 obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof OpaqueUriComponents)) { + return false; + } + + OpaqueUriComponents other = (OpaqueUriComponents) obj; + + if (ObjectUtils.nullSafeEquals(getScheme(), other.getScheme())) { + return false; + } + if (ObjectUtils.nullSafeEquals(this.ssp, other.ssp)) { + return false; + } + if (ObjectUtils.nullSafeEquals(getFragment(), other.getFragment())) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + int result = ObjectUtils.nullSafeHashCode(getScheme()); + result = 31 * result + ObjectUtils.nullSafeHashCode(this.ssp); + result = 31 * result + ObjectUtils.nullSafeHashCode(getFragment()); + return result; + } + +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/util/UriComponents.java b/org.springframework.web/src/main/java/org/springframework/web/util/UriComponents.java index 54cbcef437..f04c5f695b 100644 --- a/org.springframework.web/src/main/java/org/springframework/web/util/UriComponents.java +++ b/org.springframework.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,154 @@ 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 */ -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. 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 host. - * - * @return the host. Can be {@code null}. - */ - public String getHost() { - return host; - } - - /** - * Returns the port. Returns {@code -1} if no port has been set. - * - * @return the port - */ - public int getPort() { - return port; - } + } /** - * Returns the path. - * - * @return the path. Can be {@code null}. + * Returns the scheme specific part. Can be {@code null}. */ - public String getPath() { - return path.getPath(); - } - - /** - * Returns the list of path segments. - * - * @return the path segments. Empty if no path has been set. - */ - public List getPathSegments() { - return path.getPathSegments(); - } + public abstract String getSchemeSpecificPart(); /** - * Returns the query. - * - * @return the query. Can be {@code null}. + * Returns the user info. 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); + public abstract String getUserInfo(); - if (value != null) { - queryBuilder.append('='); - queryBuilder.append(value.toString()); - } - } - } - } - return queryBuilder.toString(); - } - else { - return null; - } + /** + * Returns the host. Can be {@code null}. + */ + public abstract String getHost(); + + /** + * Returns the port. Returns {@code -1} if no port has been set. + */ + public abstract int getPort(); + + /** + * Returns the path. Can be {@code null}. + */ + public abstract String getPath(); + + /** + * Returns the list of path segments. Empty if no path has been set. + */ + public abstract List getPathSegments(); + + /** + * Returns the query. Can be {@code null}. + */ + public abstract String getQuery(); + + /** + * Returns the map of query parameters. Empty if no query has been set. + */ + public abstract MultiValueMap getQueryParams(); + + /** + * Returns the fragment. Can be {@code null}. + */ + public final String getFragment() { + return this.fragment; } - /** - * Returns the map of query parameters. - * - * @return the query parameters. Empty if no query has been set. - */ - public MultiValueMap getQueryParams() { - return queryParams; - } - /** - * Returns the fragment. - * - * @return the fragment. Can be {@code null}. - */ - public 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. - * + * Encode 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. - * - * @throws IllegalArgumentException if any of the components contain illegal characters + * Encode 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 */ - 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); - } + public abstract UriComponents encode(String encoding) throws UnsupportedEncodingException; - 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 /** - * 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"); + /** + * 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); - 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); - } - - private static String expandUriComponent(String source, UriTemplateVariables uriVariables) { + static String expandUriComponent(String source, UriTemplateVariables uriVariables) { if (source == null) { return null; } @@ -458,536 +203,36 @@ 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); - } + /** + * Returns a URI string from this {@code UriComponents} instance. + */ + public abstract String toUriString(); - // 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); - } - } + /** + * Returns a {@code URI} from this {@code UriComponents} instance. + */ + public abstract URI toUri(); @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() { + public final 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). - */ - 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. + * Normalize the path removing sequences like "path/..". + * @see org.springframework.util.StringUtils#cleanPath(String) */ - final static class FullPathComponent implements PathComponent { + public abstract UriComponents normalize(); - 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. - */ - 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 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); - } - } - - - - /** - * 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; - } - - }; /** * Defines the contract for URI Template variables - * - * @see UriComponents#expand + * @see HierarchicalUriComponents#expand */ - private interface UriTemplateVariables { + interface UriTemplateVariables { Object getValue(String name); - } /** @@ -1009,6 +254,7 @@ public final class UriComponents { } } + /** * URI template variables backed by a variable argument array. */ @@ -1022,7 +268,8 @@ 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/org.springframework.web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java b/org.springframework.web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java index d309acb954..b6709200bc 100644 --- a/org.springframework.web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java +++ b/org.springframework.web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java @@ -32,8 +32,8 @@ import org.springframework.util.StringUtils; /** * Builder for {@link UriComponents}. - *

- * Typical usage involves: + * + *

Typical usage involves: *
    *
  1. Create a {@code UriComponentsBuilder} with one of the static factory methods (such as * {@link #fromPath(String)} or {@link #fromUri(URI)})
  2. @@ -46,10 +46,10 @@ import org.springframework.util.StringUtils; * * @author Arjen Poutsma * @author Rossen Stoyanchev + * @since 3.1 * @see #newInstance() * @see #fromPath(String) * @see #fromUri(URI) - * @since 3.1 */ public class UriComponentsBuilder { @@ -83,6 +83,8 @@ public class UriComponentsBuilder { private String scheme; + private String ssp; + private String userInfo; private String host; @@ -105,7 +107,7 @@ public class UriComponentsBuilder { protected UriComponentsBuilder() { } - // Factory methods + // Factory methods /** * Returns a new, empty builder. @@ -143,7 +145,19 @@ public class UriComponentsBuilder { /** * Returns a builder that is initialized with the given URI string. * - * @param uri the URI string to initialize with + *

    Note: The presence of reserved characters can prevent + * correct parsing of the URI string. For example if a query parameter + * contains {@code '='} or {@code '&'} characters, the query string cannot + * be parsed unambiguously. Such values should be substituted for URI + * variables to enable correct parsing: + * + *

    +	 * String uriString = "/hotels/42?filter={value}";
    +	 * UriComponentsBuilder.fromUriString(uriString).buildAndExpand("hot&cold");
    +	 * 
    + * + * @param uri + * the URI string to initialize with * @return the new {@code UriComponentsBuilder} */ public static UriComponentsBuilder fromUriString(String uri) { @@ -152,16 +166,43 @@ public class UriComponentsBuilder { if (m.matches()) { UriComponentsBuilder builder = new UriComponentsBuilder(); - builder.scheme(m.group(2)); - builder.userInfo(m.group(5)); - builder.host(m.group(6)); + String scheme = m.group(2); + String userInfo = m.group(5); + String host = m.group(6); String port = m.group(8); - if (StringUtils.hasLength(port)) { - builder.port(Integer.parseInt(port)); + String path = m.group(9); + String query = m.group(11); + String fragment = m.group(13); + + boolean opaque = false; + + if (StringUtils.hasLength(scheme)) { + String s = uri.substring(scheme.length()); + if (!s.startsWith(":/")) { + opaque = true; + } } - builder.path(m.group(9)); - builder.query(m.group(11)); - builder.fragment(m.group(13)); + + builder.scheme(scheme); + + + if (opaque) { + String ssp = uri.substring(scheme.length()).substring(1); + if (StringUtils.hasLength(fragment)) { + ssp = ssp.substring(0, ssp.length() - (fragment.length() + 1)); + } + builder.schemeSpecificPart(ssp); + } + else { + builder.userInfo(userInfo); + builder.host(host); + if (StringUtils.hasLength(port)) { + builder.port(Integer.parseInt(port)); + } + builder.path(path); + builder.query(query); + } + builder.fragment(fragment); return builder; } @@ -173,6 +214,17 @@ public class UriComponentsBuilder { /** * Creates a new {@code UriComponents} object from the string HTTP URL. * + *

    Note: The presence of reserved characters can prevent + * correct parsing of the URI string. For example if a query parameter + * contains {@code '='} or {@code '&'} characters, the query string cannot + * be parsed unambiguously. Such values should be substituted for URI + * variables to enable correct parsing: + * + *

    +	 * String uriString = "/hotels/42?filter={value}";
    +	 * UriComponentsBuilder.fromUriString(uriString).buildAndExpand("hot&cold");
    +	 * 
    + * * @param httpUrl the source URI * @return the URI components of the URI */ @@ -201,7 +253,7 @@ public class UriComponentsBuilder { - // build methods + // build methods /** * Builds a {@code UriComponents} instance from the various components contained in this builder. @@ -213,13 +265,21 @@ public class UriComponentsBuilder { } /** - * Builds a {@code UriComponents} instance from the various components contained in this builder. + * Builds a {@code UriComponents} instance from the various components + * contained in this builder. * - * @param encoded whether all the components set in this builder are encoded ({@code true}) or not ({@code false}). + * @param encoded whether all the components set in this builder are + * encoded ({@code true}) or not ({@code false}). * @return the URI components */ public UriComponents build(boolean encoded) { - return new UriComponents(scheme, userInfo, host, port, pathBuilder.build(), queryParams, fragment, encoded, true); + if (ssp != null) { + return new OpaqueUriComponents(scheme, ssp, fragment); + } + else { + return new HierarchicalUriComponents( + scheme, userInfo, host, port, pathBuilder.build(), queryParams, fragment, encoded, true); + } } /** @@ -246,7 +306,7 @@ public class UriComponentsBuilder { return build(false).expand(uriVariableValues); } - // URI components methods + // URI components methods /** * Initializes all components of this URI builder with the components of the given URI. @@ -256,25 +316,31 @@ public class UriComponentsBuilder { */ public UriComponentsBuilder uri(URI uri) { Assert.notNull(uri, "'uri' must not be null"); - Assert.isTrue(!uri.isOpaque(), "Opaque URI [" + uri + "] not supported"); this.scheme = uri.getScheme(); - if (uri.getRawUserInfo() != null) { - this.userInfo = uri.getRawUserInfo(); + if (uri.isOpaque()) { + this.ssp = uri.getRawSchemeSpecificPart(); + resetHierarchicalComponents(); } - if (uri.getHost() != null) { - this.host = uri.getHost(); - } - if (uri.getPort() != -1) { - this.port = uri.getPort(); - } - if (StringUtils.hasLength(uri.getRawPath())) { - this.pathBuilder = new FullPathComponentBuilder(uri.getRawPath()); - } - if (StringUtils.hasLength(uri.getRawQuery())) { - this.queryParams.clear(); - query(uri.getRawQuery()); + else { + if (uri.getRawUserInfo() != null) { + this.userInfo = uri.getRawUserInfo(); + } + if (uri.getHost() != null) { + this.host = uri.getHost(); + } + if (uri.getPort() != -1) { + this.port = uri.getPort(); + } + if (StringUtils.hasLength(uri.getRawPath())) { + this.pathBuilder = new FullPathComponentBuilder(uri.getRawPath()); + } + if (StringUtils.hasLength(uri.getRawQuery())) { + this.queryParams.clear(); + query(uri.getRawQuery()); + } + resetSchemeSpecificPart(); } if (uri.getRawFragment() != null) { this.fragment = uri.getRawFragment(); @@ -282,11 +348,24 @@ 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. + * 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. * - * @param scheme the URI scheme + * @param scheme + * the URI scheme * @return this UriComponentsBuilder */ public UriComponentsBuilder scheme(String scheme) { @@ -295,26 +374,44 @@ public class UriComponentsBuilder { } /** - * Sets the URI user info. The given user info may contain URI template variables, and may also be {@code null} to - * clear the user info of this builder. + * 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 * @return this UriComponentsBuilder */ public UriComponentsBuilder userInfo(String userInfo) { this.userInfo = userInfo; + resetSchemeSpecificPart(); return this; } /** - * 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. + * 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 * @return this UriComponentsBuilder */ public UriComponentsBuilder host(String host) { this.host = host; + resetSchemeSpecificPart(); return this; } @@ -327,11 +424,13 @@ public class UriComponentsBuilder { public UriComponentsBuilder port(int port) { Assert.isTrue(port >= -1, "'port' must not be < -1"); this.port = port; + resetSchemeSpecificPart(); return this; } /** - * Appends the given path to the existing path of this builder. The given path may contain URI template variables. + * Appends the given path to the existing path of this builder. The given + * path may contain URI template variables. * * @param path the URI path * @return this UriComponentsBuilder @@ -343,6 +442,7 @@ public class UriComponentsBuilder { else { this.pathBuilder = NULL_PATH_COMPONENT_BUILDER; } + resetSchemeSpecificPart(); return this; } @@ -355,6 +455,7 @@ public class UriComponentsBuilder { public UriComponentsBuilder replacePath(String path) { this.pathBuilder = NULL_PATH_COMPONENT_BUILDER; path(path); + resetSchemeSpecificPart(); return this; } @@ -368,11 +469,24 @@ 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; } /** - * Appends the given query to the existing query of this builder. The given query may contain URI template variables. + * Appends the given query to the existing query of this builder. + * The given query may contain URI template variables. + * + *

    Note: The presence of reserved characters can prevent + * correct parsing of the URI string. For example if a query parameter + * contains {@code '='} or {@code '&'} characters, the query string cannot + * be parsed unambiguously. Such values should be substituted for URI + * variables to enable correct parsing: + * + *

    +	 * String uriString = "/hotels/42?filter={value}";
    +	 * UriComponentsBuilder.fromUriString(uriString).buildAndExpand("hot&cold");
    +	 * 
    * * @param query the query string * @return this UriComponentsBuilder @@ -389,6 +503,7 @@ public class UriComponentsBuilder { else { this.queryParams.clear(); } + resetSchemeSpecificPart(); return this; } @@ -401,16 +516,20 @@ public class UriComponentsBuilder { public UriComponentsBuilder replaceQuery(String query) { this.queryParams.clear(); query(query); + resetSchemeSpecificPart(); return this; } /** - * Appends the given query parameter to the existing query parameters. The given name or any of the values may contain - * URI template variables. If no values are given, the resulting URI will contain the query parameter name only (i.e. - * {@code ?foo} instead of {@code ?foo=bar}. + * Appends the given query parameter to the existing query parameters. The + * given name or any of the values may contain URI template variables. If no + * values are given, the resulting URI will contain the query parameter name + * only (i.e. {@code ?foo} instead of {@code ?foo=bar}. * - * @param name the query parameter name - * @param values the query parameter values + * @param name + * the query parameter name + * @param values + * the query parameter values * @return this UriComponentsBuilder */ public UriComponentsBuilder queryParam(String name, Object... values) { @@ -424,15 +543,19 @@ public class UriComponentsBuilder { else { this.queryParams.add(name, null); } + resetSchemeSpecificPart(); return this; } /** - * Sets the query parameter values overriding all existing query values for the same parameter. - * If no values are given, the query parameter is removed. + * Sets the query parameter values overriding all existing query values for + * the same parameter. If no values are given, the query parameter is + * removed. * - * @param name the query parameter name - * @param values the query parameter values + * @param name + * the query parameter name + * @param values + * the query parameter values * @return this UriComponentsBuilder */ public UriComponentsBuilder replaceQueryParam(String name, Object... values) { @@ -441,14 +564,17 @@ public class UriComponentsBuilder { if (!ObjectUtils.isEmpty(values)) { queryParam(name, values); } + resetSchemeSpecificPart(); return this; } /** - * Sets the URI fragment. The given fragment may contain URI template variables, and may also be {@code null} to clear - * the fragment of this builder. + * Sets the URI fragment. The given fragment may contain URI template + * variables, and may also be {@code null} to clear the fragment of this + * builder. * - * @param fragment the URI fragment + * @param fragment + * the URI fragment * @return this UriComponentsBuilder */ public UriComponentsBuilder fragment(String fragment) { @@ -463,11 +589,11 @@ public class UriComponentsBuilder { } /** - * Represents a builder for {@link org.springframework.web.util.UriComponents.PathComponent} + * Represents a builder for {@link HierarchicalUriComponents.PathComponent} */ private interface PathComponentBuilder { - UriComponents.PathComponent build(); + HierarchicalUriComponents.PathComponent build(); PathComponentBuilder appendPath(String path); @@ -485,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) { @@ -522,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) { @@ -549,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) { @@ -576,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/org.springframework.web/src/main/java/org/springframework/web/util/UriUtils.java b/org.springframework.web/src/main/java/org/springframework/web/util/UriUtils.java index 4d6c2a4499..e976a43b54 100644 --- a/org.springframework.web/src/main/java/org/springframework/web/util/UriUtils.java +++ b/org.springframework.web/src/main/java/org/springframework/web/util/UriUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-2012 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,9 +29,9 @@ import org.springframework.util.Assert; * *

    All {@code encode*(String, String} methods in this class operate in a similar way: *

      - *
    • Valid characters for the specific URI component as defined in RFC 3986 stay the same.
    • - *
    • All other characters are converted into one or more bytes in the given encoding scheme. Each of the - * resulting bytes is written as a hexadecimal string in the "%xy" format.
    • + *
    • Valid characters for the specific URI component as defined in RFC 3986 stay the same.
    • + *
    • All other characters are converted into one or more bytes in the given encoding scheme. Each of the + * resulting bytes is written as a hexadecimal string in the "%xy" format.
    • *
    * * @author Arjen Poutsma @@ -64,6 +64,7 @@ public abstract class UriUtils { private static final Pattern HTTP_URL_PATTERN = Pattern.compile( "^" + HTTP_PATTERN + "(//(" + USERINFO_PATTERN + "@)?" + HOST_PATTERN + "(:" + PORT_PATTERN + ")?" + ")?" + PATH_PATTERN + "(\\?" + LAST_PATTERN + ")?"); + // encoding /** @@ -73,8 +74,8 @@ public abstract class UriUtils { * characters in query parameter names and query parameter values because they cannot * be parsed in a reliable way. Instead use: *
    -	 *  UriComponents uriComponents = UriComponentsBuilder.fromUri("/path?name={value}").buildAndExpand("a=b");
    -	 *  String encodedUri = uriComponents.encode().toUriString();
    +	 * UriComponents uriComponents = UriComponentsBuilder.fromUri("/path?name={value}").buildAndExpand("a=b");
    +	 * String encodedUri = uriComponents.encode().toUriString();
     	 * 
    * @param uri the URI to be encoded * @param encoding the character encoding to encode to @@ -83,6 +84,7 @@ public abstract class UriUtils { * @throws UnsupportedEncodingException when the given encoding parameter is not supported * @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"); @@ -113,8 +115,8 @@ public abstract class UriUtils { * characters in query parameter names and query parameter values because they cannot * be parsed in a reliable way. Instead use: *
    -	 *  UriComponents uriComponents = UriComponentsBuilder.fromHttpUrl("/path?name={value}").buildAndExpand("a=b");
    -	 *  String encodedUri = uriComponents.encode().toUriString();
    +	 * UriComponents uriComponents = UriComponentsBuilder.fromHttpUrl("/path?name={value}").buildAndExpand("a=b");
    +	 * String encodedUri = uriComponents.encode().toUriString();
     	 * 
    * @param httpUrl the HTTP URL to be encoded * @param encoding the character encoding to encode to @@ -123,6 +125,7 @@ public abstract class UriUtils { * @throws UnsupportedEncodingException when the given encoding parameter is not supported * @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,46 +163,47 @@ 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 { - Assert.hasLength(encoding, "'encoding' must not be empty"); - StringBuilder sb = new StringBuilder(); + Assert.hasLength(encoding, "'encoding' must not be empty"); + StringBuilder sb = new StringBuilder(); - if (scheme != null) { - sb.append(encodeScheme(scheme, encoding)); - sb.append(':'); - } + if (scheme != null) { + sb.append(encodeScheme(scheme, encoding)); + sb.append(':'); + } - if (authority != null) { - sb.append("//"); - if (userInfo != null) { - sb.append(encodeUserInfo(userInfo, encoding)); - sb.append('@'); - } - if (host != null) { - sb.append(encodeHost(host, encoding)); - } - if (port != null) { - sb.append(':'); - sb.append(encodePort(port, encoding)); - } - } + if (authority != null) { + sb.append("//"); + if (userInfo != null) { + sb.append(encodeUserInfo(userInfo, encoding)); + sb.append('@'); + } + if (host != null) { + sb.append(encodeHost(host, encoding)); + } + if (port != null) { + sb.append(':'); + sb.append(encodePort(port, encoding)); + } + } - sb.append(encodePath(path, encoding)); + sb.append(encodePath(path, encoding)); - if (query != null) { - sb.append('?'); - sb.append(encodeQuery(query, encoding)); - } + if (query != null) { + sb.append('?'); + sb.append(encodeQuery(query, encoding)); + } - if (fragment != null) { - sb.append('#'); - sb.append(encodeFragment(fragment, encoding)); - } + if (fragment != null) { + sb.append('#'); + sb.append(encodeFragment(fragment, encoding)); + } - return sb.toString(); + return sb.toString(); } @@ -213,7 +217,8 @@ public abstract class UriUtils { * @throws UnsupportedEncodingException when the given encoding parameter is not supported */ public static String encodeScheme(String scheme, String encoding) throws UnsupportedEncodingException { - return UriComponents.encodeUriComponent(scheme, encoding, UriComponents.Type.SCHEME); + return HierarchicalUriComponents.encodeUriComponent(scheme, encoding, + HierarchicalUriComponents.Type.SCHEME); } /** @@ -224,7 +229,8 @@ public abstract class UriUtils { * @throws UnsupportedEncodingException when the given encoding parameter is not supported */ public static String encodeAuthority(String authority, String encoding) throws UnsupportedEncodingException { - return UriComponents.encodeUriComponent(authority, encoding, UriComponents.Type.AUTHORITY); + return HierarchicalUriComponents.encodeUriComponent(authority, encoding, + HierarchicalUriComponents.Type.AUTHORITY); } /** @@ -235,7 +241,8 @@ public abstract class UriUtils { * @throws UnsupportedEncodingException when the given encoding parameter is not supported */ public static String encodeUserInfo(String userInfo, String encoding) throws UnsupportedEncodingException { - return UriComponents.encodeUriComponent(userInfo, encoding, UriComponents.Type.USER_INFO); + return HierarchicalUriComponents.encodeUriComponent(userInfo, encoding, + HierarchicalUriComponents.Type.USER_INFO); } /** @@ -246,7 +253,8 @@ public abstract class UriUtils { * @throws UnsupportedEncodingException when the given encoding parameter is not supported */ public static String encodeHost(String host, String encoding) throws UnsupportedEncodingException { - return UriComponents.encodeUriComponent(host, encoding, UriComponents.Type.HOST); + return HierarchicalUriComponents + .encodeUriComponent(host, encoding, HierarchicalUriComponents.Type.HOST); } /** @@ -257,7 +265,8 @@ public abstract class UriUtils { * @throws UnsupportedEncodingException when the given encoding parameter is not supported */ public static String encodePort(String port, String encoding) throws UnsupportedEncodingException { - return UriComponents.encodeUriComponent(port, encoding, UriComponents.Type.PORT); + return HierarchicalUriComponents + .encodeUriComponent(port, encoding, HierarchicalUriComponents.Type.PORT); } /** @@ -268,7 +277,8 @@ public abstract class UriUtils { * @throws UnsupportedEncodingException when the given encoding parameter is not supported */ public static String encodePath(String path, String encoding) throws UnsupportedEncodingException { - return UriComponents.encodeUriComponent(path, encoding, UriComponents.Type.PATH); + return HierarchicalUriComponents + .encodeUriComponent(path, encoding, HierarchicalUriComponents.Type.PATH); } /** @@ -279,7 +289,8 @@ public abstract class UriUtils { * @throws UnsupportedEncodingException when the given encoding parameter is not supported */ public static String encodePathSegment(String segment, String encoding) throws UnsupportedEncodingException { - return UriComponents.encodeUriComponent(segment, encoding, UriComponents.Type.PATH_SEGMENT); + return HierarchicalUriComponents.encodeUriComponent(segment, encoding, + HierarchicalUriComponents.Type.PATH_SEGMENT); } /** @@ -290,7 +301,8 @@ public abstract class UriUtils { * @throws UnsupportedEncodingException when the given encoding parameter is not supported */ public static String encodeQuery(String query, String encoding) throws UnsupportedEncodingException { - return UriComponents.encodeUriComponent(query, encoding, UriComponents.Type.QUERY); + return HierarchicalUriComponents + .encodeUriComponent(query, encoding, HierarchicalUriComponents.Type.QUERY); } /** @@ -301,7 +313,8 @@ public abstract class UriUtils { * @throws UnsupportedEncodingException when the given encoding parameter is not supported */ public static String encodeQueryParam(String queryParam, String encoding) throws UnsupportedEncodingException { - return UriComponents.encodeUriComponent(queryParam, encoding, UriComponents.Type.QUERY_PARAM); + return HierarchicalUriComponents.encodeUriComponent(queryParam, encoding, + HierarchicalUriComponents.Type.QUERY_PARAM); } /** @@ -312,7 +325,8 @@ public abstract class UriUtils { * @throws UnsupportedEncodingException when the given encoding parameter is not supported */ public static String encodeFragment(String fragment, String encoding) throws UnsupportedEncodingException { - return UriComponents.encodeUriComponent(fragment, encoding, UriComponents.Type.FRAGMENT); + return HierarchicalUriComponents.encodeUriComponent(fragment, encoding, + HierarchicalUriComponents.Type.FRAGMENT); } @@ -321,11 +335,11 @@ public abstract class UriUtils { /** * Decodes the given encoded source String into an URI. Based on the following rules: *
      - *
    • Alphanumeric characters {@code "a"} through {@code "z"}, {@code "A"} through {@code "Z"}, and - * {@code "0"} through {@code "9"} stay the same.
    • - *
    • Special characters {@code "-"}, {@code "_"}, {@code "."}, and {@code "*"} stay the same.
    • - *
    • A sequence "%xy" is interpreted as a hexadecimal representation of the character.
    • - *
    + *
  3. Alphanumeric characters {@code "a"} through {@code "z"}, {@code "A"} through {@code "Z"}, and + * {@code "0"} through {@code "9"} stay the same.
  4. + *
  5. Special characters {@code "-"}, {@code "_"}, {@code "."}, and {@code "*"} stay the same.
  6. + *
  7. A sequence "%xy" is interpreted as a hexadecimal representation of the character.
  8. + * * @param source the source string * @param encoding the encoding * @return the decoded URI diff --git a/org.springframework.web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java b/org.springframework.web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java index 3fefe43ebc..be0eebbc9c 100644 --- a/org.springframework.web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java +++ b/org.springframework.web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java @@ -16,9 +16,6 @@ package org.springframework.web.util; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; - import java.net.URI; import java.net.URISyntaxException; import java.util.Arrays; @@ -26,21 +23,26 @@ import java.util.HashMap; import java.util.Map; import org.junit.Test; + import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; -/** @author Arjen Poutsma */ +import static org.junit.Assert.*; + +/** + * @author Arjen Poutsma + */ public class UriComponentsBuilderTests { @Test public void plain() throws URISyntaxException { UriComponentsBuilder builder = UriComponentsBuilder.newInstance(); UriComponents result = builder.scheme("http").host("example.com").path("foo").queryParam("bar").fragment("baz").build(); - assertEquals("http", result.getScheme()); - assertEquals("example.com", result.getHost()); - assertEquals("foo", result.getPath()); - assertEquals("bar", result.getQuery()); - assertEquals("baz", result.getFragment()); + assertEquals("http", result.getScheme()); + assertEquals("example.com", result.getHost()); + assertEquals("foo", result.getPath()); + assertEquals("bar", result.getQuery()); + assertEquals("baz", result.getFragment()); URI expected = new URI("http://example.com/foo?bar#baz"); assertEquals("Invalid result URI", expected, result.toUri()); @@ -49,43 +51,54 @@ public class UriComponentsBuilderTests { @Test public void fromPath() throws URISyntaxException { UriComponents result = UriComponentsBuilder.fromPath("foo").queryParam("bar").fragment("baz").build(); - assertEquals("foo", result.getPath()); - assertEquals("bar", result.getQuery()); - assertEquals("baz", result.getFragment()); + assertEquals("foo", result.getPath()); + assertEquals("bar", result.getQuery()); + assertEquals("baz", result.getFragment()); URI expected = new URI("/foo?bar#baz"); assertEquals("Invalid result URI", expected, result.toUri()); result = UriComponentsBuilder.fromPath("/foo").build(); - assertEquals("/foo", result.getPath()); + assertEquals("/foo", result.getPath()); - expected = new URI("/foo"); + expected = new URI("/foo"); assertEquals("Invalid result URI", expected, result.toUri()); } @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()); - assertEquals("example.com", result.getHost()); - assertEquals("/foo", result.getPath()); - assertEquals("bar", result.getQuery()); - assertEquals("baz", result.getFragment()); + UriComponents result = UriComponentsBuilder.fromUri(uri).build(); + assertEquals("http", result.getScheme()); + assertEquals("example.com", result.getHost()); + assertEquals("/foo", result.getPath()); + assertEquals("bar", result.getQuery()); + assertEquals("baz", result.getFragment()); + + assertEquals("Invalid result URI", uri, result.toUri()); + } + + @Test + public void fromOpaqueUri() throws URISyntaxException { + URI uri = new URI("mailto:foo@bar.com#baz"); + UriComponents result = UriComponentsBuilder.fromUri(uri).build(); + assertEquals("mailto", result.getScheme()); + assertEquals("foo@bar.com", result.getSchemeSpecificPart()); + assertEquals("baz", result.getFragment()); assertEquals("Invalid result URI", uri, result.toUri()); } // SPR-9317 - @Test - public void fromUriEncodedQuery() throws URISyntaxException { - URI uri = new URI("http://www.example.org/?param=aGVsbG9Xb3JsZA%3D%3D"); - String fromUri = UriComponentsBuilder.fromUri(uri).build().getQueryParams().get("param").get(0); - String fromUriString = UriComponentsBuilder.fromUriString(uri.toString()).build().getQueryParams().get("param").get(0); + @Test + public void fromUriEncodedQuery() throws URISyntaxException { + URI uri = new URI("http://www.example.org/?param=aGVsbG9Xb3JsZA%3D%3D"); + String fromUri = UriComponentsBuilder.fromUri(uri).build().getQueryParams().get("param").get(0); + String fromUriString = UriComponentsBuilder.fromUriString(uri.toString()).build().getQueryParams().get("param").get(0); - assertEquals(fromUri, fromUriString); - } + assertEquals(fromUri, fromUriString); + } @Test public void fromUriString() { @@ -102,34 +115,35 @@ public class UriComponentsBuilderTests { result = UriComponentsBuilder.fromUriString( "http://arjen:foobar@java.sun.com:80/javase/6/docs/api/java/util/BitSet.html?foo=bar#and(java.util.BitSet)") .build(); - assertEquals("http", result.getScheme()); - assertEquals("arjen:foobar", result.getUserInfo()); - assertEquals("java.sun.com", result.getHost()); - assertEquals(80, result.getPort()); - assertEquals("/javase/6/docs/api/java/util/BitSet.html", result.getPath()); - assertEquals("foo=bar", result.getQuery()); + assertEquals("http", result.getScheme()); + assertEquals("arjen:foobar", result.getUserInfo()); + assertEquals("java.sun.com", result.getHost()); + assertEquals(80, result.getPort()); + assertEquals("/javase/6/docs/api/java/util/BitSet.html", result.getPath()); + assertEquals("foo=bar", result.getQuery()); MultiValueMap expectedQueryParams = new LinkedMultiValueMap(1); expectedQueryParams.add("foo", "bar"); assertEquals(expectedQueryParams, result.getQueryParams()); - assertEquals("and(java.util.BitSet)", result.getFragment()); + assertEquals("and(java.util.BitSet)", result.getFragment()); - result = UriComponentsBuilder.fromUriString("mailto:java-net@java.sun.com").build(); - assertEquals("mailto", result.getScheme()); - assertNull(result.getUserInfo()); - assertNull(result.getHost()); - assertEquals(-1, result.getPort()); - assertEquals("java-net@java.sun.com", result.getPathSegments().get(0)); - assertNull(result.getQuery()); - assertNull(result.getFragment()); + 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.getSchemeSpecificPart()); + assertNull(result.getPath()); + assertNull(result.getQuery()); + assertEquals("baz", result.getFragment()); - result = UriComponentsBuilder.fromUriString("docs/guide/collections/designfaq.html#28").build(); - assertNull(result.getScheme()); - assertNull(result.getUserInfo()); - assertNull(result.getHost()); - assertEquals(-1, result.getPort()); - assertEquals("docs/guide/collections/designfaq.html", result.getPath()); - assertNull(result.getQuery()); - assertEquals("28", result.getFragment()); + result = UriComponentsBuilder.fromUriString("docs/guide/collections/designfaq.html#28").build(); + assertNull(result.getScheme()); + assertNull(result.getUserInfo()); + assertNull(result.getHost()); + assertEquals(-1, result.getPort()); + assertEquals("docs/guide/collections/designfaq.html", result.getPath()); + assertNull(result.getQuery()); + assertEquals("28", result.getFragment()); } @@ -265,7 +279,7 @@ public class UriComponentsBuilderTests { } @Test - public void buildAndExpand() { + public void buildAndExpandHierarchical() { UriComponents result = UriComponentsBuilder.fromPath("/{foo}").buildAndExpand("fooValue"); assertEquals("/fooValue", result.toUriString()); @@ -275,4 +289,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()); + } + }