SPR-5973: Cleaning it up

This commit is contained in:
Arjen Poutsma 2011-09-08 11:01:56 +00:00
parent 5f208936ec
commit b6c1e88e4a
4 changed files with 286 additions and 258 deletions

View File

@ -19,6 +19,7 @@ package org.springframework.web.util;
import java.net.URI; import java.net.URI;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.EnumSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -120,56 +121,27 @@ public class UriBuilder {
* @return the resulting URI * @return the resulting URI
*/ */
public URI build() { public URI build() {
StringBuilder uriBuilder = new StringBuilder(); String port = portAsString();
String path = null;
if (scheme != null) {
uriBuilder.append(scheme);
uriBuilder.append(':');
}
if (userInfo != null || host != null || port != -1) {
uriBuilder.append("//");
if (StringUtils.hasLength(userInfo)) {
uriBuilder.append(userInfo);
uriBuilder.append('@');
}
if (host != null) {
uriBuilder.append(host);
}
if (port != -1) {
uriBuilder.append(':');
uriBuilder.append(port);
}
}
if (!pathSegments.isEmpty()) { if (!pathSegments.isEmpty()) {
StringBuilder pathBuilder = new StringBuilder();
for (String pathSegment : pathSegments) { for (String pathSegment : pathSegments) {
boolean startsWithSlash = pathSegment.charAt(0) == '/'; boolean startsWithSlash = pathSegment.charAt(0) == '/';
boolean endsWithSlash = uriBuilder.length() > 0 && uriBuilder.charAt(uriBuilder.length() - 1) == '/'; boolean endsWithSlash = pathBuilder.length() > 0 && pathBuilder.charAt(pathBuilder.length() - 1) == '/';
if (!endsWithSlash && !startsWithSlash) { if (!endsWithSlash && !startsWithSlash) {
uriBuilder.append('/'); pathBuilder.append('/');
} }
else if (endsWithSlash && startsWithSlash) { else if (endsWithSlash && startsWithSlash) {
pathSegment = pathSegment.substring(1); pathSegment = pathSegment.substring(1);
} }
uriBuilder.append(pathSegment); pathBuilder.append(pathSegment);
} }
path = pathBuilder.toString();
} }
String query = queryAsString();
if (queryBuilder.length() > 0) { String uri = UriUtils.buildUri(scheme, null, userInfo, host, port, path, query, fragment);
uriBuilder.append('?');
uriBuilder.append(queryBuilder);
}
if (StringUtils.hasLength(fragment)) {
uriBuilder.append('#');
uriBuilder.append(fragment);
}
String uri = uriBuilder.toString();
uri = StringUtils.replace(uri, "{", "%7B"); uri = StringUtils.replace(uri, "{", "%7B");
uri = StringUtils.replace(uri, "}", "%7D"); uri = StringUtils.replace(uri, "}", "%7D");
@ -186,7 +158,7 @@ public class UriBuilder {
* @return the resulting URI * @return the resulting URI
*/ */
public URI build(Map<String, ?> uriVariables) { public URI build(Map<String, ?> uriVariables) {
return buildFromMap(true, uriVariables); return buildFromMap(uriVariables, true);
} }
/** /**
@ -197,72 +169,54 @@ public class UriBuilder {
* @return the resulting URI * @return the resulting URI
*/ */
public URI buildFromEncoded(Map<String, ?> uriVariables) { public URI buildFromEncoded(Map<String, ?> uriVariables) {
return buildFromMap(false, uriVariables); return buildFromMap(uriVariables, false);
} }
private URI buildFromMap(boolean encodeUriVariableValues, Map<String, ?> uriVariables) { private URI buildFromMap(Map<String, ?> uriVariables, boolean encodeUriVariableValues) {
if (CollectionUtils.isEmpty(uriVariables)) { if (CollectionUtils.isEmpty(uriVariables)) {
return build(); return build();
} }
String scheme = expand(this.scheme, UriComponent.SCHEME, uriVariables, encodeUriVariableValues);
StringBuilder uriBuilder = new StringBuilder(); String userInfo = expand(this.userInfo, UriComponent.USER_INFO, uriVariables, encodeUriVariableValues);
String host = expand(this.host, UriComponent.HOST, uriVariables, encodeUriVariableValues);
UriTemplate template; String port = expand(this.portAsString(), UriComponent.PORT, uriVariables, encodeUriVariableValues);
String path = null;
if (scheme != null) { if (!this.pathSegments.isEmpty()) {
template = new UriComponentTemplate(scheme, UriComponent.SCHEME, encodeUriVariableValues); StringBuilder pathBuilder = new StringBuilder();
uriBuilder.append(template.expandAsString(uriVariables)); for (String pathSegment : this.pathSegments) {
uriBuilder.append(':');
}
if (userInfo != null || host != null || port != -1) {
uriBuilder.append("//");
if (StringUtils.hasLength(userInfo)) {
template = new UriComponentTemplate(userInfo, UriComponent.USER_INFO, encodeUriVariableValues);
uriBuilder.append(template.expandAsString(uriVariables));
uriBuilder.append('@');
}
if (host != null) {
template = new UriComponentTemplate(host, UriComponent.HOST, encodeUriVariableValues);
uriBuilder.append(template.expandAsString(uriVariables));
}
if (port != -1) {
uriBuilder.append(':');
uriBuilder.append(port);
}
}
if (!pathSegments.isEmpty()) {
for (String pathSegment : pathSegments) {
boolean startsWithSlash = pathSegment.charAt(0) == '/'; boolean startsWithSlash = pathSegment.charAt(0) == '/';
boolean endsWithSlash = uriBuilder.length() > 0 && uriBuilder.charAt(uriBuilder.length() - 1) == '/'; boolean endsWithSlash = pathBuilder.length() > 0 && pathBuilder.charAt(pathBuilder.length() - 1) == '/';
if (!endsWithSlash && !startsWithSlash) { if (!endsWithSlash && !startsWithSlash) {
uriBuilder.append('/'); pathBuilder.append('/');
} }
else if (endsWithSlash && startsWithSlash) { else if (endsWithSlash && startsWithSlash) {
pathSegment = pathSegment.substring(1); pathSegment = pathSegment.substring(1);
} }
template = new UriComponentTemplate(pathSegment, UriComponent.PATH_SEGMENT, encodeUriVariableValues); pathSegment = expand(pathSegment, UriComponent.PATH_SEGMENT, uriVariables, encodeUriVariableValues);
uriBuilder.append(template.expandAsString(uriVariables)); pathBuilder.append(pathSegment);
} }
path = pathBuilder.toString();
} }
if (queryBuilder.length() > 0) { String query = expand(this.queryAsString(), UriComponent.QUERY, uriVariables, encodeUriVariableValues);
uriBuilder.append('?'); String fragment = expand(this.fragment, UriComponent.FRAGMENT, uriVariables, encodeUriVariableValues);
template = new UriComponentTemplate(queryBuilder.toString(), UriComponent.QUERY, encodeUriVariableValues);
uriBuilder.append(template.expandAsString(uriVariables)); String uri = UriUtils.buildUri(scheme, null, userInfo, host, port, path, query, fragment);
return URI.create(uri);
} }
if (StringUtils.hasLength(fragment)) { private String expand(String source,
uriBuilder.append('#'); UriComponent uriComponent,
template = new UriComponentTemplate(fragment, UriComponent.FRAGMENT, encodeUriVariableValues); Map<String, ?> uriVariables,
uriBuilder.append(template.expandAsString(uriVariables)); boolean encodeUriVariableValues) {
if (source == null) {
return null;
} }
if (source.indexOf('{') == -1) {
return URI.create(uriBuilder.toString()); return source;
}
UriTemplate template = new UriComponentTemplate(source, uriComponent, encodeUriVariableValues);
return template.expandAsString(uriVariables);
} }
/** /**
@ -403,7 +357,7 @@ public class UriBuilder {
public UriBuilder scheme(String scheme) { public UriBuilder scheme(String scheme) {
if (scheme != null) { if (scheme != null) {
Assert.hasLength(scheme, "'scheme' must not be empty"); Assert.hasLength(scheme, "'scheme' must not be empty");
this.scheme = UriUtils.encode(scheme, UriComponent.SCHEME, true); this.scheme = encodeUriComponent(scheme, UriComponent.SCHEME);
} }
else { else {
this.scheme = null; this.scheme = null;
@ -421,7 +375,7 @@ public class UriBuilder {
public UriBuilder userInfo(String userInfo) { public UriBuilder userInfo(String userInfo) {
if (userInfo != null) { if (userInfo != null) {
Assert.hasLength(userInfo, "'userInfo' must not be empty"); Assert.hasLength(userInfo, "'userInfo' must not be empty");
this.userInfo = UriUtils.encode(userInfo, UriComponent.USER_INFO, true); this.userInfo = encodeUriComponent(userInfo, UriComponent.USER_INFO);
} }
else { else {
this.userInfo = null; this.userInfo = null;
@ -439,7 +393,7 @@ public class UriBuilder {
public UriBuilder host(String host) { public UriBuilder host(String host) {
if (host != null) { if (host != null) {
Assert.hasLength(host, "'host' must not be empty"); Assert.hasLength(host, "'host' must not be empty");
this.host = UriUtils.encode(host, UriComponent.HOST, true); this.host = encodeUriComponent(host, UriComponent.HOST);
} }
else { else {
this.host = null; this.host = null;
@ -459,6 +413,10 @@ public class UriBuilder {
return this; return this;
} }
private String portAsString() {
return this.port != -1 ? Integer.toString(this.port) : null;
}
/** /**
* 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.
* *
@ -482,7 +440,7 @@ public class UriBuilder {
public UriBuilder pathSegment(String... segments) throws IllegalArgumentException { public UriBuilder pathSegment(String... segments) throws IllegalArgumentException {
Assert.notNull(segments, "'segments' must not be null"); Assert.notNull(segments, "'segments' must not be null");
for (String segment : segments) { for (String segment : segments) {
this.pathSegments.add(UriUtils.encode(segment, UriComponent.PATH_SEGMENT, true)); this.pathSegments.add(encodeUriComponent(segment, UriComponent.PATH_SEGMENT));
} }
return this; return this;
@ -500,7 +458,7 @@ public class UriBuilder {
public UriBuilder queryParam(String name, Object... values) { public UriBuilder queryParam(String name, Object... values) {
Assert.notNull(name, "'name' must not be null"); Assert.notNull(name, "'name' must not be null");
String encodedName = UriUtils.encode(name, UriComponent.QUERY_PARAM, true); String encodedName = encodeUriComponent(name, UriComponent.QUERY_PARAM);
if (ObjectUtils.isEmpty(values)) { if (ObjectUtils.isEmpty(values)) {
if (queryBuilder.length() != 0) { if (queryBuilder.length() != 0) {
@ -518,7 +476,7 @@ public class UriBuilder {
String valueAsString = value != null ? value.toString() : ""; String valueAsString = value != null ? value.toString() : "";
if (valueAsString.length() != 0) { if (valueAsString.length() != 0) {
queryBuilder.append('='); queryBuilder.append('=');
queryBuilder.append(UriUtils.encode(valueAsString, UriComponent.QUERY_PARAM, true)); queryBuilder.append(encodeUriComponent(valueAsString, UriComponent.QUERY_PARAM));
} }
} }
@ -526,6 +484,10 @@ public class UriBuilder {
return this; return this;
} }
private String queryAsString() {
return queryBuilder.length() != 0 ? queryBuilder.toString() : null;
}
/** /**
* Sets the URI fragment. The given fragment may contain URI template variables, and may also be {@code null} to clear * Sets the URI fragment. The given fragment may contain URI template variables, and may also be {@code null} to clear
* the fragment of this builder. * the fragment of this builder.
@ -536,7 +498,7 @@ public class UriBuilder {
public UriBuilder fragment(String fragment) { public UriBuilder fragment(String fragment) {
if (fragment != null) { if (fragment != null) {
Assert.hasLength(fragment, "'fragment' must not be empty"); Assert.hasLength(fragment, "'fragment' must not be empty");
this.fragment = UriUtils.encode(fragment, UriComponent.FRAGMENT, true); this.fragment = encodeUriComponent(fragment, UriComponent.FRAGMENT);
} }
else { else {
this.fragment = null; this.fragment = null;
@ -545,4 +507,9 @@ public class UriBuilder {
} }
private String encodeUriComponent(String source, UriComponent uriComponent) {
return UriUtils.encodeUriComponent(source, uriComponent, EnumSet.of(UriUtils.EncodingOption.ALLOW_TEMPLATE_VARS));
}
} }

View File

@ -40,7 +40,7 @@ class UriComponentTemplate extends UriTemplate {
@Override @Override
protected String getVariableValueAsString(Object variableValue) { protected String getVariableValueAsString(Object variableValue) {
String variableValueString = super.getVariableValueAsString(variableValue); String variableValueString = super.getVariableValueAsString(variableValue);
return encodeUriVariableValues ? UriUtils.encode(variableValueString, uriComponent, false) : return encodeUriVariableValues ? UriUtils.encodeUriComponent(variableValueString, uriComponent) :
variableValueString; variableValueString;
} }
} }

View File

@ -18,8 +18,10 @@ package org.springframework.web.util;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.util.Collections;
import java.util.EnumMap; import java.util.EnumMap;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -29,10 +31,12 @@ import org.springframework.util.Assert;
* Utility class for URI encoding and decoding based on RFC 3986. Offers encoding methods for the various URI * Utility class for URI encoding and decoding based on RFC 3986. Offers encoding methods for the various URI
* components. * components.
* *
* <p>All {@code encode*(String, String} methods in this class operate in a similar way: <ul> <li>Valid characters for * <p>All {@code encode*(String, String} methods in this class operate in a similar way:
* the specific URI component as defined in RFC 3986 stay the same. <li>All other characters are converted into one or * <ul>
* more bytes in the given encoding scheme. Each of the resulting bytes is written as a hexadecimal string in the * <li>Valid characters for the specific URI component as defined in RFC 3986 stay the same.</li>
* "<code>%<i>xy</i></code>" format. </ul> * <li>All other characters are converted into one or more bytes in the given encoding scheme. Each of the
* resulting bytes is written as a hexadecimal string in the "<code>%<i>xy</i></code>" format.</li>
* </ul>
* *
* @author Arjen Poutsma * @author Arjen Poutsma
* @see <a href="http://www.ietf.org/rfc/rfc3986.txt">RFC 3986</a> * @see <a href="http://www.ietf.org/rfc/rfc3986.txt">RFC 3986</a>
@ -127,15 +131,13 @@ public abstract class UriUtils {
* <li>{@link UriComponent#PORT}</li> * <li>{@link UriComponent#PORT}</li>
* <li>{@link UriComponent#PATH}</li> * <li>{@link UriComponent#PATH}</li>
* <li>{@link UriComponent#QUERY}</li> * <li>{@link UriComponent#QUERY}</li>
* <li>{@link UriComponent#FRAGMENT}</li>
* </ul> * </ul>
* though the values assigned to these keys is {@code null} if they do not occur in the given source URI. * though the values assigned to these keys is {@code null} if they do not occur in the given source URI.
* *
* <p><strong>Note</strong> that the returned map will never contain mappings for {@link UriComponent#PATH_SEGMENT}, * <p><strong>Note</strong> that the returned map will never contain mappings for {@link UriComponent#PATH_SEGMENT},
* nor {@link UriComponent#QUERY_PARAM}, since those components can occur multiple times in the URI. * nor {@link UriComponent#QUERY_PARAM}, since those components can occur multiple times in the URI. Nor does it
* * contain a mapping for {@link UriComponent#FRAGMENT}, as fragments are not supposed to be sent to the server, but
* <p><strong>Note</strong> that this method does not support fragments ({@code #}), as these are not supposed to be * retained by the client.
* sent to the server, but retained by the client.
* *
* @param httpUrl the source URI * @param httpUrl the source URI
* @return the URI components of the URI * @return the URI components of the URI
@ -292,11 +294,12 @@ public abstract class UriUtils {
public static String encodeUriComponents(Map<UriComponent, String> uriComponents, public static String encodeUriComponents(Map<UriComponent, String> uriComponents,
String encoding) throws UnsupportedEncodingException { String encoding) throws UnsupportedEncodingException {
Assert.notEmpty(uriComponents, "'uriComponents' must not be empty"); Assert.notEmpty(uriComponents, "'uriComponents' must not be empty");
Assert.hasLength(encoding, "'encoding' must not be empty");
Map<UriComponent, String> encodedUriComponents = new EnumMap<UriComponent, String>(UriComponent.class); Map<UriComponent, String> encodedUriComponents = new EnumMap<UriComponent, String>(UriComponent.class);
for (Map.Entry<UriComponent, String> entry : uriComponents.entrySet()) { for (Map.Entry<UriComponent, String> entry : uriComponents.entrySet()) {
if (entry.getValue() != null) { if (entry.getValue() != null) {
String encodedValue = encode(entry.getValue(), encoding, entry.getKey(), false); String encodedValue = encodeUriComponent(entry.getValue(), encoding, entry.getKey(), null);
encodedUriComponents.put(entry.getKey(), encodedValue); encodedUriComponents.put(entry.getKey(), encodedValue);
} }
} }
@ -329,7 +332,6 @@ public abstract class UriUtils {
String query, String query,
String fragment, String fragment,
String encoding) throws UnsupportedEncodingException { String encoding) throws UnsupportedEncodingException {
Assert.hasLength(encoding, "'encoding' must not be empty"); Assert.hasLength(encoding, "'encoding' must not be empty");
if (scheme != null) { if (scheme != null) {
@ -360,168 +362,88 @@ public abstract class UriUtils {
} }
/** /**
* Encodes the given URI scheme with the given encoding. * Encodes the given source into an encoded String using the rules specified by the given component.
*
* @param scheme the scheme to be encoded
* @param encoding the character encoding to encode to
* @return the encoded scheme
* @throws UnsupportedEncodingException when the given encoding parameter is not supported
*/
public static String encodeScheme(String scheme, String encoding) throws UnsupportedEncodingException {
return encode(scheme, encoding, UriComponent.SCHEME, false);
}
/**
* Encodes the given URI authority with the given encoding.
*
* @param authority the authority to be encoded
* @param encoding the character encoding to encode to
* @return the encoded authority
* @throws UnsupportedEncodingException when the given encoding parameter is not supported
*/
public static String encodeAuthority(String authority, String encoding) throws UnsupportedEncodingException {
return encode(authority, encoding, UriComponent.AUTHORITY, false);
}
/**
* Encodes the given URI user info with the given encoding.
*
* @param userInfo the user info to be encoded
* @param encoding the character encoding to encode to
* @return the encoded user info
* @throws UnsupportedEncodingException when the given encoding parameter is not supported
*/
public static String encodeUserInfo(String userInfo, String encoding) throws UnsupportedEncodingException {
return encode(userInfo, encoding, UriComponent.USER_INFO, false);
}
/**
* Encodes the given URI host with the given encoding.
*
* @param host the host to be encoded
* @param encoding the character encoding to encode to
* @return the encoded host
* @throws UnsupportedEncodingException when the given encoding parameter is not supported
*/
public static String encodeHost(String host, String encoding) throws UnsupportedEncodingException {
return encode(host, encoding, UriComponent.HOST, false);
}
/**
* Encodes the given URI port with the given encoding.
*
* @param port the port to be encoded
* @param encoding the character encoding to encode to
* @return the encoded port
* @throws UnsupportedEncodingException when the given encoding parameter is not supported
*/
public static String encodePort(String port, String encoding) throws UnsupportedEncodingException {
return encode(port, encoding, UriComponent.PORT, false);
}
/**
* Encodes the given URI path with the given encoding.
*
* @param path the path to be encoded
* @param encoding the character encoding to encode to
* @return the encoded path
* @throws UnsupportedEncodingException when the given encoding parameter is not supported
*/
public static String encodePath(String path, String encoding) throws UnsupportedEncodingException {
return encode(path, encoding, UriComponent.PATH, false);
}
/**
* Encodes the given URI path segment with the given encoding.
*
* @param segment the segment to be encoded
* @param encoding the character encoding to encode to
* @return the encoded segment
* @throws UnsupportedEncodingException when the given encoding parameter is not supported
*/
public static String encodePathSegment(String segment, String encoding) throws UnsupportedEncodingException {
return encode(segment, encoding, UriComponent.PATH_SEGMENT, false);
}
/**
* Encodes the given URI query with the given encoding.
*
* @param query the query to be encoded
* @param encoding the character encoding to encode to
* @return the encoded query
* @throws UnsupportedEncodingException when the given encoding parameter is not supported
*/
public static String encodeQuery(String query, String encoding) throws UnsupportedEncodingException {
return encode(query, encoding, UriComponent.QUERY, false);
}
/**
* Encodes the given URI query parameter with the given encoding.
*
* @param queryParam the query parameter to be encoded
* @param encoding the character encoding to encode to
* @return the encoded query parameter
* @throws UnsupportedEncodingException when the given encoding parameter is not supported
*/
public static String encodeQueryParam(String queryParam, String encoding) throws UnsupportedEncodingException {
return encode(queryParam, encoding, UriComponent.QUERY_PARAM, false);
}
/**
* Encodes the given URI fragment with the given encoding.
*
* @param fragment the fragment to be encoded
* @param encoding the character encoding to encode to
* @return the encoded fragment
* @throws UnsupportedEncodingException when the given encoding parameter is not supported
*/
public static String encodeFragment(String fragment, String encoding) throws UnsupportedEncodingException {
return encode(fragment, encoding, UriComponent.FRAGMENT, false);
}
/**
* Encodes the given source into an encoded String using the rules specified by the given component. This method
* encodes with the default encoding (i.e. UTF-8).
* *
* @param source the source string * @param source the source string
* @param uriComponent the URI component for the source * @param uriComponent the URI component for the source
* @param allowTemplateVars whether URI template variables are allowed. If {@code true}, '{' and '}' characters are not
* encoded, even though they might not be valid for the component
* @return the encoded URI * @return the encoded URI
* @throws IllegalArgumentException when the given uri parameter is not a valid URI * @throws IllegalArgumentException when the given uri parameter is not a valid URI
*/ */
public static String encode(String source, UriComponent uriComponent, boolean allowTemplateVars) { public static String encodeUriComponent(String source, UriComponent uriComponent) {
return encodeUriComponent(source, uriComponent, null);
}
/**
* 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 uriComponent the URI component for the source
* @param encodingOptions the options used when encoding. May be {@code null}.
* @return the encoded URI
* @throws IllegalArgumentException when the given uri parameter is not a valid URI
* @see EncodingOption
*/
public static String encodeUriComponent(String source,
UriComponent uriComponent,
Set<EncodingOption> encodingOptions) {
try { try {
return encode(source, DEFAULT_ENCODING, uriComponent, allowTemplateVars); return encodeUriComponent(source, DEFAULT_ENCODING, uriComponent, encodingOptions);
} }
catch (UnsupportedEncodingException e) { catch (UnsupportedEncodingException ex) {
throw new InternalError("'" + DEFAULT_ENCODING + "' encoding not supported"); throw new InternalError("\"" + DEFAULT_ENCODING + "\" not supported");
} }
} }
/** /**
* Encodes the given source into an encoded String using the rules specified by the given component. * Encodes the given source into an encoded String using the rules specified by the given component.
* *
* @param source the source string * @param source the source string
* @param encoding the encoding of the source string * @param encoding the encoding of the source string
* @param uriComponent the URI component for the source * @param uriComponent the URI component for the source
* @param allowTemplateVars whether URI template variables are allowed. If {@code true}, '{' and '}' characters are not
* encoded, even though they might not be valid for the component
* @return the encoded URI * @return the encoded URI
* @throws IllegalArgumentException when the given uri parameter is not a valid URI * @throws IllegalArgumentException when the given uri parameter is not a valid URI
*/ */
public static String encode(String source, String encoding, UriComponent uriComponent, boolean allowTemplateVars) public static String encodeUriComponent(String source,
throws UnsupportedEncodingException { String encoding,
UriComponent uriComponent) throws UnsupportedEncodingException {
return encodeUriComponent(source, encoding, uriComponent, null);
}
/**
* 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 uriComponent the URI component for the source
* @param encodingOptions the options used when encoding. May be {@code null}.
* @return the encoded URI
* @throws IllegalArgumentException when the given uri parameter is not a valid URI
* @see EncodingOption
*/
public static String encodeUriComponent(String source,
String encoding,
UriComponent uriComponent,
Set<EncodingOption> encodingOptions) throws UnsupportedEncodingException {
Assert.hasLength(encoding, "'encoding' must not be empty"); Assert.hasLength(encoding, "'encoding' must not be empty");
byte[] bytes = encodeInternal(source.getBytes(encoding), uriComponent, allowTemplateVars); byte[] bytes = encodeInternal(source.getBytes(encoding), uriComponent, encodingOptions);
return new String(bytes, "US-ASCII"); return new String(bytes, "US-ASCII");
} }
private static byte[] encodeInternal(byte[] source, UriComponent uriComponent, boolean allowTemplateVars) { private static byte[] encodeInternal(byte[] source,
UriComponent uriComponent,
Set<EncodingOption> encodingOptions) {
Assert.notNull(source, "'source' must not be null"); Assert.notNull(source, "'source' must not be null");
Assert.notNull(uriComponent, "'uriComponent' must not be null"); Assert.notNull(uriComponent, "'uriComponent' must not be null");
if (encodingOptions == null) {
encodingOptions = Collections.emptySet();
}
ByteArrayOutputStream bos = new ByteArrayOutputStream(source.length); ByteArrayOutputStream bos = new ByteArrayOutputStream(source.length);
for (int i = 0; i < source.length; i++) { for (int i = 0; i < source.length; i++) {
int b = source[i]; int b = source[i];
@ -531,7 +453,7 @@ public abstract class UriUtils {
if (uriComponent.isAllowed(b)) { if (uriComponent.isAllowed(b)) {
bos.write(b); bos.write(b);
} }
else if (allowTemplateVars && (b == '{' || b == '}')) { else if (encodingOptions.contains(EncodingOption.ALLOW_TEMPLATE_VARS) && (b == '{' || b == '}')) {
bos.write(b); bos.write(b);
} }
else { else {
@ -547,13 +469,139 @@ public abstract class UriUtils {
return bos.toByteArray(); return bos.toByteArray();
} }
// encoding convenience methods
/** /**
* Decodes the given encoded source String into an URI. Based on the following rules: <ul> <li>Alphanumeric characters * Encodes the given URI scheme with the given encoding.
* {@code "a"} through {@code "z"}, {@code "A"} through {@code "Z"}, and {@code "0"} through {@code "9"} stay the same. *
* <li>Special characters {@code "-"}, {@code "_"}, {@code "."}, and {@code "*"} stay the same. <li>All other * @param scheme the scheme to be encoded
* characters are converted into one or more bytes using the given encoding scheme. Each of the resulting bytes is * @param encoding the character encoding to encode to
* written as a hexadecimal string in the {@code %xy} format. <li>A sequence "<code>%<i>xy</i></code>" is interpreted * @return the encoded scheme
* as a hexadecimal representation of the character. </ul> * @throws UnsupportedEncodingException when the given encoding parameter is not supported
*/
public static String encodeScheme(String scheme, String encoding) throws UnsupportedEncodingException {
return encodeUriComponent(scheme, encoding, UriComponent.SCHEME, null);
}
/**
* Encodes the given URI authority with the given encoding.
*
* @param authority the authority to be encoded
* @param encoding the character encoding to encode to
* @return the encoded authority
* @throws UnsupportedEncodingException when the given encoding parameter is not supported
*/
public static String encodeAuthority(String authority, String encoding) throws UnsupportedEncodingException {
return encodeUriComponent(authority, encoding, UriComponent.AUTHORITY, null);
}
/**
* Encodes the given URI user info with the given encoding.
*
* @param userInfo the user info to be encoded
* @param encoding the character encoding to encode to
* @return the encoded user info
* @throws UnsupportedEncodingException when the given encoding parameter is not supported
*/
public static String encodeUserInfo(String userInfo, String encoding) throws UnsupportedEncodingException {
return encodeUriComponent(userInfo, encoding, UriComponent.USER_INFO, null);
}
/**
* Encodes the given URI host with the given encoding.
*
* @param host the host to be encoded
* @param encoding the character encoding to encode to
* @return the encoded host
* @throws UnsupportedEncodingException when the given encoding parameter is not supported
*/
public static String encodeHost(String host, String encoding) throws UnsupportedEncodingException {
return encodeUriComponent(host, encoding, UriComponent.HOST, null);
}
/**
* Encodes the given URI port with the given encoding.
*
* @param port the port to be encoded
* @param encoding the character encoding to encode to
* @return the encoded port
* @throws UnsupportedEncodingException when the given encoding parameter is not supported
*/
public static String encodePort(String port, String encoding) throws UnsupportedEncodingException {
return encodeUriComponent(port, encoding, UriComponent.PORT, null);
}
/**
* Encodes the given URI path with the given encoding.
*
* @param path the path to be encoded
* @param encoding the character encoding to encode to
* @return the encoded path
* @throws UnsupportedEncodingException when the given encoding parameter is not supported
*/
public static String encodePath(String path, String encoding) throws UnsupportedEncodingException {
return encodeUriComponent(path, encoding, UriComponent.PATH, null);
}
/**
* Encodes the given URI path segment with the given encoding.
*
* @param segment the segment to be encoded
* @param encoding the character encoding to encode to
* @return the encoded segment
* @throws UnsupportedEncodingException when the given encoding parameter is not supported
*/
public static String encodePathSegment(String segment, String encoding) throws UnsupportedEncodingException {
return encodeUriComponent(segment, encoding, UriComponent.PATH_SEGMENT, null);
}
/**
* Encodes the given URI query with the given encoding.
*
* @param query the query to be encoded
* @param encoding the character encoding to encode to
* @return the encoded query
* @throws UnsupportedEncodingException when the given encoding parameter is not supported
*/
public static String encodeQuery(String query, String encoding) throws UnsupportedEncodingException {
return encodeUriComponent(query, encoding, UriComponent.QUERY, null);
}
/**
* Encodes the given URI query parameter with the given encoding.
*
* @param queryParam the query parameter to be encoded
* @param encoding the character encoding to encode to
* @return the encoded query parameter
* @throws UnsupportedEncodingException when the given encoding parameter is not supported
*/
public static String encodeQueryParam(String queryParam, String encoding) throws UnsupportedEncodingException {
return encodeUriComponent(queryParam, encoding, UriComponent.QUERY_PARAM, null);
}
/**
* Encodes the given URI fragment with the given encoding.
*
* @param fragment the fragment to be encoded
* @param encoding the character encoding to encode to
* @return the encoded fragment
* @throws UnsupportedEncodingException when the given encoding parameter is not supported
*/
public static String encodeFragment(String fragment, String encoding) throws UnsupportedEncodingException {
return encodeUriComponent(fragment, encoding, UriComponent.FRAGMENT, null);
}
// decoding
/**
* Decodes the given encoded source String into an URI. Based on the following rules:
* <ul>
* <li>Alphanumeric characters {@code "a"} through {@code "z"}, {@code "A"} through {@code "Z"}, and
* {@code "0"} through {@code "9"} stay the same.</li>
* <li>Special characters {@code "-"}, {@code "_"}, {@code "."}, and {@code "*"} stay the same.</li>
* <li>A sequence "<code>%<i>xy</i></code>" is interpreted as a hexadecimal representation of the character.</li>
* </ul>
* *
* @param source the source string * @param source the source string
* @param encoding the encoding * @param encoding the encoding
@ -590,4 +638,16 @@ public abstract class UriUtils {
return changed ? new String(bos.toByteArray(), encoding) : source; return changed ? new String(bos.toByteArray(), encoding) : source;
} }
/**
* Enumeration used to control how URIs are encoded.
*/
public enum EncodingOption {
/**
* Allow for URI template variables to occur in the URI component (i.e. '{foo}')
*/
ALLOW_TEMPLATE_VARS
}
} }

View File

@ -19,9 +19,10 @@ package org.springframework.web.util;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.util.Collections; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import static org.junit.Assert.*; import static org.junit.Assert.*;
@ -55,11 +56,11 @@ public class UriBuilderTests {
} }
@Test @Test
@Ignore("Working on it")
public void templateVarsVarArgs() throws URISyntaxException { public void templateVarsVarArgs() throws URISyntaxException {
UriBuilder builder = UriBuilder.newInstance(); URI result = UriBuilder.fromPath("/{foo}/{bar}").build("baz", "qux");
URI result = builder.scheme("http").host("example.com").path("{foo}").build("bar");
URI expected = new URI("http://example.com/bar"); URI expected = new URI("http://example.com/baz/qux");
assertEquals("Invalid result URI", expected, result); assertEquals("Invalid result URI", expected, result);
} }
@ -82,11 +83,11 @@ public class UriBuilderTests {
@Test @Test
public void templateVarsMap() throws URISyntaxException { public void templateVarsMap() throws URISyntaxException {
Map<String, String> vars = Collections.singletonMap("foo", "bar"); Map<String, String> vars = new HashMap<String, String>(2);
UriBuilder builder = UriBuilder.newInstance(); vars.put("bar", "qux");
URI result = builder.scheme("http").host("example.com").path("{foo}").build(vars); vars.put("foo", "baz");
URI result = UriBuilder.fromPath("/{foo}/{bar}").build(vars);
URI expected = new URI("http://example.com/bar"); URI expected = new URI("/baz/qux");
assertEquals("Invalid result URI", expected, result); assertEquals("Invalid result URI", expected, result);
} }