Browse Source

SPR-5973: UriBuilder

pull/7/head
Arjen Poutsma 15 years ago
parent
commit
9a25efbbda
  1. 556
      org.springframework.web/src/main/java/org/springframework/web/util/UriBuilder.java
  2. 99
      org.springframework.web/src/main/java/org/springframework/web/util/UriTemplate.java
  3. 416
      org.springframework.web/src/main/java/org/springframework/web/util/UriUtils.java
  4. 122
      org.springframework.web/src/test/java/org/springframework/web/util/UriBuilderTests.java
  5. 27
      org.springframework.web/src/test/java/org/springframework/web/util/UriTemplateTests.java

556
org.springframework.web/src/main/java/org/springframework/web/util/UriBuilder.java

@ -0,0 +1,556 @@ @@ -0,0 +1,556 @@
/*
* Copyright 2002-2011 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.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* Builder for {@link URI} objects.
*
* <p>Typical usage involves:
* <ol>
* <li>Create a {@code UriBuilder} with one of the static factory methods (such as {@link #fromPath(String)} or
* {@link #fromUri(URI)})</li>
* <li>Set the various URI components through the respective methods ({@link #scheme(String)},
* {@link #userInfo(String)}, {@link #host(String)}, {@link #port(int)}, {@link #path(String)},
* {@link #pathSegment(String...)}, {@link #queryParam(String, Object...)}, and {@link #fragment(String)}.</li>
* <li>Build the URI with one of the {@link #build} method variants.</li>
* </ol>
*
* <p>Most of the URI component methods accept URI template variables (i.e. {@code "{foo}"}), which are expanded by
* calling {@code build}.
* one , are allowed in most components of a URI but their value is
* restricted to a particular component. E.g.
* <blockquote><code>UriBuilder.fromPath("{arg1}").build("foo#bar");</code></blockquote>
* would result in encoding of the '#' such that the resulting URI is
* "foo%23bar". To create a URI "foo#bar" use
* <blockquote><code>UriBuilder.fromPath("{arg1}").fragment("{arg2}").build("foo", "bar")</code></blockquote>
* instead. URI template names and delimiters are never encoded but their
* values are encoded when a URI is built.
* Template parameter regular expressions are ignored when building a URI, i.e.
* no validation is performed.
* <p>Inspired by {@link javax.ws.rs.core.UriBuilder}.
*
* @author Arjen Poutsma
* @since 3.1
* @see #newInstance()
* @see #fromPath(String)
* @see #fromUri(URI)
*/
public class UriBuilder {
private String scheme;
private String userInfo;
private String host;
private int port = -1;
private final List<String> pathSegments = new ArrayList<String>();
private final StringBuilder queryBuilder = new StringBuilder();
private String fragment;
/**
* Default constructor. Protected to prevent direct instantiation.
*
* @see #newInstance()
* @see #fromPath(String)
* @see #fromUri(URI)
*/
protected UriBuilder() {
}
// Factory methods
/**
* Returns a new, empty URI builder.
*
* @return the new {@code UriBuilder}
*/
public static UriBuilder newInstance() {
return new UriBuilder();
}
/**
* Returns a URI builder that is initialized with the given path.
*
* @param path the path to initialize with
* @return the new {@code UriBuilder}
*/
public static UriBuilder fromPath(String path) {
UriBuilder builder = new UriBuilder();
builder.path(path);
return builder;
}
/**
* Returns a URI builder that is initialized with the given {@code URI}.
*
* @param uri the URI to initialize with
* @return the new {@code UriBuilder}
*/
public static UriBuilder fromUri(URI uri) {
UriBuilder builder = new UriBuilder();
builder.uri(uri);
return builder;
}
// build methods
/**
* Builds a URI with no URI template variables. Any template variable definitions found will be encoded (i.e.
* {@code "/{foo}"} will result in {@code "/%7Bfoo%7D"}.
* @return the resulting URI
*/
public URI build() {
StringBuilder uriBuilder = new StringBuilder();
if (scheme != null) {
uriBuilder.append(scheme).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()) {
for (String pathSegment : pathSegments) {
boolean startsWithSlash = pathSegment.charAt(0) == '/';
boolean endsWithSlash = uriBuilder.length() > 0 && uriBuilder.charAt(uriBuilder.length() - 1) == '/';
if (!endsWithSlash && !startsWithSlash) {
uriBuilder.append('/');
}
else if (endsWithSlash && startsWithSlash) {
pathSegment = pathSegment.substring(1);
}
uriBuilder.append(pathSegment);
}
}
if (queryBuilder.length() > 0) {
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, "}", "%7D");
return URI.create(uri);
}
/**
* Builds a URI with the given URI template variables. Any template variable definitions found will be expanded with
* the given variables map. All variable values will be encoded in accordance with the encoding rules for the URI
* component they occur in.
*
* @param uriVariables the map of URI variables
* @return the resulting URI
*/
public URI build(Map<String, ?> uriVariables) {
return buildFromMap(true, uriVariables);
}
/**
* Builds a URI with the given URI template variables. Any template variable definitions found will be expanded with the
* given variables map. All variable values will not be encoded.
*
* @param uriVariables the map of URI variables
* @return the resulting URI
*/
public URI buildFromEncoded(Map<String, ?> uriVariables) {
return buildFromMap(false, uriVariables);
}
private URI buildFromMap(boolean encodeUriVariableValues, Map<String, ?> uriVariables) {
if (CollectionUtils.isEmpty(uriVariables)) {
return build();
}
StringBuilder uriBuilder = new StringBuilder();
UriTemplate template;
if (scheme != null) {
template = new UriTemplate(scheme, UriUtils.SCHEME_COMPONENT);
uriBuilder.append(template.expandAsString(encodeUriVariableValues, uriVariables));
uriBuilder.append(':');
}
if (userInfo != null || host != null || port != -1) {
uriBuilder.append("//");
if (StringUtils.hasLength(userInfo)) {
template = new UriTemplate(userInfo, UriUtils.USER_INFO_COMPONENT);
uriBuilder.append(template.expandAsString(encodeUriVariableValues, uriVariables));
uriBuilder.append('@');
}
if (host != null) {
template = new UriTemplate(host, UriUtils.HOST_COMPONENT);
uriBuilder.append(template.expandAsString(encodeUriVariableValues, uriVariables));
}
if (port != -1) {
uriBuilder.append(':');
uriBuilder.append(port);
}
}
if (!pathSegments.isEmpty()) {
for (String pathSegment : pathSegments) {
boolean startsWithSlash = pathSegment.charAt(0) == '/';
boolean endsWithSlash = uriBuilder.length() > 0 && uriBuilder.charAt(uriBuilder.length() - 1) == '/';
if (!endsWithSlash && !startsWithSlash) {
uriBuilder.append('/');
}
else if (endsWithSlash && startsWithSlash) {
pathSegment = pathSegment.substring(1);
}
template = new UriTemplate(pathSegment, UriUtils.PATH_SEGMENT_COMPONENT);
uriBuilder.append(template.expandAsString(encodeUriVariableValues, uriVariables));
}
}
if (queryBuilder.length() > 0) {
uriBuilder.append('?');
template = new UriTemplate(queryBuilder.toString(), UriUtils.QUERY_COMPONENT);
uriBuilder.append(template.expandAsString(encodeUriVariableValues, uriVariables));
}
if (StringUtils.hasLength(fragment)) {
uriBuilder.append('#');
template = new UriTemplate(fragment, UriUtils.FRAGMENT_COMPONENT);
uriBuilder.append(template.expandAsString(encodeUriVariableValues, uriVariables));
}
return URI.create(uriBuilder.toString());
}
/**
* Builds a URI with the given URI template variable values. Any template variable definitions found will be expanded
* with the given variables. All variable values will be encoded in accordance with the encoding rules for the URI
* component they occur in.
*
* @param uriVariableValues the array of URI variables
* @return the resulting URI
*/
public URI build(Object... uriVariableValues) {
return buildFromVarArg(true, uriVariableValues);
}
/**
* Builds a URI with the given URI template variable values. Any template variable definitions found will be expanded
* with the given variables. All variable values will not be encoded.
*
* @param uriVariableValues the array of URI variables
* @return the resulting URI
*/
public URI buildFromEncoded(Object... uriVariableValues) {
return buildFromVarArg(false, uriVariableValues);
}
private URI buildFromVarArg(boolean encodeUriVariableValues, Object... uriVariableValues) {
if (ObjectUtils.isEmpty(uriVariableValues)) {
return build();
}
StringBuilder uriBuilder = new StringBuilder();
UriTemplate template;
if (scheme != null) {
template = new UriTemplate(scheme, UriUtils.SCHEME_COMPONENT);
uriBuilder.append(template.expandAsString(encodeUriVariableValues, uriVariableValues));
uriBuilder.append(':');
}
if (userInfo != null || host != null || port != -1) {
uriBuilder.append("//");
if (StringUtils.hasLength(userInfo)) {
template = new UriTemplate(userInfo, UriUtils.USER_INFO_COMPONENT);
uriBuilder.append(template.expandAsString(encodeUriVariableValues, uriVariableValues));
uriBuilder.append('@');
}
if (host != null) {
template = new UriTemplate(host, UriUtils.HOST_COMPONENT);
uriBuilder.append(template.expandAsString(encodeUriVariableValues, uriVariableValues));
}
if (port != -1) {
uriBuilder.append(':');
uriBuilder.append(port);
}
}
if (!pathSegments.isEmpty()) {
for (String pathSegment : pathSegments) {
boolean startsWithSlash = pathSegment.charAt(0) == '/';
boolean endsWithSlash = uriBuilder.length() > 0 && uriBuilder.charAt(uriBuilder.length() - 1) == '/';
if (!endsWithSlash && !startsWithSlash) {
uriBuilder.append('/');
}
else if (endsWithSlash && startsWithSlash) {
pathSegment = pathSegment.substring(1);
}
template = new UriTemplate(pathSegment, UriUtils.PATH_SEGMENT_COMPONENT);
uriBuilder.append(template.expandAsString(encodeUriVariableValues, uriVariableValues));
}
}
if (queryBuilder.length() > 0) {
uriBuilder.append('?');
template = new UriTemplate(queryBuilder.toString(), UriUtils.QUERY_COMPONENT);
uriBuilder.append(template.expandAsString(encodeUriVariableValues, uriVariableValues));
}
if (StringUtils.hasLength(fragment)) {
uriBuilder.append('#');
template = new UriTemplate(fragment, UriUtils.FRAGMENT_COMPONENT);
uriBuilder.append(template.expandAsString(encodeUriVariableValues, uriVariableValues));
}
return URI.create(uriBuilder.toString());
}
// URI components methods
/**
* Initializes all components of this URI builder with the components of the given URI.
*
* @param uri the URI
* @return this UriBuilder
*/
public UriBuilder uri(URI uri) {
Assert.notNull(uri, "'uri' must not be null");
Assert.isTrue(!uri.isOpaque(), "Opaque URI [" + uri + "] not supported");
this.scheme = uri.getScheme();
if (uri.getRawUserInfo() != null) {
this.userInfo = uri.getRawUserInfo();
}
if (uri.getHost() != null) {
this.host = uri.getHost();
}
if (uri.getPort() != -1) {
this.port = uri.getPort();
}
if (StringUtils.hasLength(uri.getRawPath())) {
String[] pathSegments = StringUtils.tokenizeToStringArray(uri.getRawPath(), "/");
this.pathSegments.clear();
Collections.addAll(this.pathSegments, pathSegments);
}
if (StringUtils.hasLength(uri.getRawQuery())) {
this.queryBuilder.setLength(0);
this.queryBuilder.append(uri.getRawQuery());
}
if (uri.getRawFragment() != null) {
this.fragment = uri.getRawFragment();
}
return this;
}
/**
* 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
* @return this UriBuilder
*/
public UriBuilder scheme(String scheme) {
if (scheme != null) {
Assert.hasLength(scheme, "'scheme' must not be empty");
this.scheme = UriUtils.encode(scheme, UriUtils.SCHEME_COMPONENT, true);
}
else {
this.scheme = null;
}
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 UriBuilder
*/
public UriBuilder userInfo(String userInfo) {
if (userInfo != null) {
Assert.hasLength(userInfo, "'userInfo' must not be empty");
this.userInfo = UriUtils.encode(userInfo, UriUtils.USER_INFO_COMPONENT, true);
}
else {
this.userInfo = null;
}
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.
*
* @param host the URI host
* @return this UriBuilder
*/
public UriBuilder host(String host) {
if (host != null) {
Assert.hasLength(host, "'host' must not be empty");
this.host = UriUtils.encode(host, UriUtils.HOST_COMPONENT, true);
}
else {
this.host = null;
}
return this;
}
/**
* Sets the URI port. Passing {@code -1} will clear the port of this builder.
*
* @param port the URI port
* @return this UriBuilder
*/
public UriBuilder port(int port) {
Assert.isTrue(port >= -1, "'port' must not be < -1");
this.port = port;
return this;
}
/**
* 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 UriBuilder
*/
public UriBuilder path(String path) {
Assert.notNull(path, "path must not be null");
String[] pathSegments = StringUtils.tokenizeToStringArray(path, "/");
return pathSegment(pathSegments);
}
/**
* Appends the given path segments to the existing path of this builder. Each given path segments may contain URI
* template variables.
*
* @param segments the URI path segments
* @return this UriBuilder
*/
public UriBuilder pathSegment(String... segments) throws IllegalArgumentException {
Assert.notNull(segments, "'segments' must not be null");
for (String segment : segments) {
this.pathSegments.add(UriUtils.encode(segment, UriUtils.PATH_SEGMENT_COMPONENT, true));
}
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}.
*
* @param name the query parameter name
* @param values the query parameter values
* @return this UriBuilder
*/
public UriBuilder queryParam(String name, Object... values) {
Assert.notNull(name, "'name' must not be null");
String encodedName = UriUtils.encode(name, UriUtils.QUERY_PARAM_COMPONENT, true);
if (ObjectUtils.isEmpty(values)) {
if (queryBuilder.length() != 0) {
queryBuilder.append('&');
}
queryBuilder.append(encodedName);
}
else {
for (Object value : values) {
if (queryBuilder.length() != 0) {
queryBuilder.append('&');
}
queryBuilder.append(encodedName);
String valueAsString = value != null ? value.toString() : "";
if (valueAsString.length() != 0) {
queryBuilder.append('=');
queryBuilder.append(UriUtils.encode(valueAsString, UriUtils.QUERY_PARAM_COMPONENT, true));
}
}
}
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.
*
* @param fragment the URI fragment
* @return this UriBuilder
*/
public UriBuilder fragment(String fragment) {
if (fragment != null) {
Assert.hasLength(fragment, "'fragment' must not be empty");
this.fragment = UriUtils.encode(fragment, UriUtils.FRAGMENT_COMPONENT, true);
}
else {
this.fragment = null;
}
return this;
}
}

99
org.springframework.web/src/main/java/org/springframework/web/util/UriTemplate.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2010 the original author or authors.
* Copyright 2002-2011 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.
@ -47,7 +47,7 @@ public class UriTemplate implements Serializable { @@ -47,7 +47,7 @@ public class UriTemplate implements Serializable {
private static final Pattern NAMES_PATTERN = Pattern.compile("\\{([^/]+?)\\}");
/** Replaces template variables in the URI template. */
private static final String VALUE_REGEX = "(.*)";
private static final String DEFAULT_VARIABLE_PATTERN = "(.*)";
private final List<String> variableNames;
@ -56,6 +56,8 @@ public class UriTemplate implements Serializable { @@ -56,6 +56,8 @@ public class UriTemplate implements Serializable {
private final String uriTemplate;
private final UriUtils.UriComponent uriComponent;
/**
* Construct a new {@link UriTemplate} with the given URI String.
@ -66,6 +68,19 @@ public class UriTemplate implements Serializable { @@ -66,6 +68,19 @@ public class UriTemplate implements Serializable {
this.uriTemplate = uriTemplate;
this.variableNames = parser.getVariableNames();
this.matchPattern = parser.getMatchPattern();
this.uriComponent = null;
}
/**
* Construct a new {@link UriTemplate} with the given URI String.
* @param uriTemplate the URI template string
*/
public UriTemplate(String uriTemplate, UriUtils.UriComponent uriComponent) {
Parser parser = new Parser(uriTemplate);
this.uriTemplate = uriTemplate;
this.variableNames = parser.getVariableNames();
this.matchPattern = parser.getMatchPattern();
this.uriComponent = uriComponent;
}
/**
@ -95,6 +110,28 @@ public class UriTemplate implements Serializable { @@ -95,6 +110,28 @@ public class UriTemplate implements Serializable {
* or if it does not contain values for all the variable names
*/
public URI expand(Map<String, ?> uriVariables) {
return encodeUri(expandAsString(true, uriVariables));
}
/**
* Given the Map of variables, expands this template into a URI. The Map keys represent variable names,
* the Map values variable values. The order of variables is not significant.
* <p>Example:
* <pre class="code">
* UriTemplate template = new UriTemplate("http://example.com/hotels/{hotel}/bookings/{booking}");
* Map&lt;String, String&gt; uriVariables = new HashMap&lt;String, String&gt;();
* uriVariables.put("booking", "42");
* uriVariables.put("hotel", "1");
* System.out.println(template.expand(uriVariables));
* </pre>
* will print: <blockquote><code>http://example.com/hotels/1/bookings/42</code></blockquote>
* @param encodeUriVariableValues indicates whether uri template variables should be encoded or not
* @param uriVariables the map of URI variables
* @return the expanded URI
* @throws IllegalArgumentException if <code>uriVariables</code> is <code>null</code>;
* or if it does not contain values for all the variable names
*/
public String expandAsString(boolean encodeUriVariableValues, Map<String, ?> uriVariables) {
Assert.notNull(uriVariables, "'uriVariables' must not be null");
Object[] values = new Object[this.variableNames.size()];
for (int i = 0; i < this.variableNames.size(); i++) {
@ -104,7 +141,7 @@ public class UriTemplate implements Serializable { @@ -104,7 +141,7 @@ public class UriTemplate implements Serializable {
}
values[i] = uriVariables.get(name);
}
return expand(values);
return expandAsString(encodeUriVariableValues, values);
}
/**
@ -122,22 +159,45 @@ public class UriTemplate implements Serializable { @@ -122,22 +159,45 @@ public class UriTemplate implements Serializable {
* or if it does not contain sufficient variables
*/
public URI expand(Object... uriVariableValues) {
return encodeUri(expandAsString(true, uriVariableValues));
}
/**
* Given an array of variables, expand this template into a full URI String. The array represent variable values.
* The order of variables is significant.
* <p>Example:
* <pre class="code">
* UriTemplate template = new UriTemplate("http://example.com/hotels/{hotel}/bookings/{booking}");
* System.out.println(template.expand("1", "42));
* </pre>
* will print: <blockquote><code>http://example.com/hotels/1/bookings/42</code></blockquote>
* @param encodeVariableValues indicates whether uri template variables should be encoded or not
* @param uriVariableValues the array of URI variables
* @return the expanded URI
* @throws IllegalArgumentException if <code>uriVariables</code> is <code>null</code>
* or if it does not contain sufficient variables
*/
public String expandAsString(boolean encodeVariableValues, Object... uriVariableValues) {
Assert.notNull(uriVariableValues, "'uriVariableValues' must not be null");
if (uriVariableValues.length != this.variableNames.size()) {
if (uriVariableValues.length < this.variableNames.size()) {
throw new IllegalArgumentException(
"Invalid amount of variables values in [" + this.uriTemplate + "]: expected " +
"Not enough of variables values in [" + this.uriTemplate + "]: expected at least " +
this.variableNames.size() + "; got " + uriVariableValues.length);
}
Matcher matcher = NAMES_PATTERN.matcher(this.uriTemplate);
StringBuffer buffer = new StringBuffer();
StringBuffer uriBuffer = new StringBuffer();
int i = 0;
while (matcher.find()) {
Object uriVariable = uriVariableValues[i++];
String replacement = Matcher.quoteReplacement(uriVariable != null ? uriVariable.toString() : "");
matcher.appendReplacement(buffer, replacement);
String uriVariableString = uriVariable != null ? uriVariable.toString() : "";
if (encodeVariableValues && uriComponent != null) {
uriVariableString = UriUtils.encode(uriVariableString, uriComponent, false);
}
String replacement = Matcher.quoteReplacement(uriVariableString);
matcher.appendReplacement(uriBuffer, replacement);
}
matcher.appendTail(buffer);
return encodeUri(buffer.toString());
matcher.appendTail(uriBuffer);
return uriBuffer.toString();
}
/**
@ -220,8 +280,23 @@ public class UriTemplate implements Serializable { @@ -220,8 +280,23 @@ public class UriTemplate implements Serializable {
int end = 0;
while (m.find()) {
this.patternBuilder.append(quote(uriTemplate, end, m.start()));
this.patternBuilder.append(VALUE_REGEX);
this.variableNames.add(m.group(1));
String match = m.group(1);
int colonIdx = match.indexOf(':');
if (colonIdx == -1) {
this.patternBuilder.append(DEFAULT_VARIABLE_PATTERN);
this.variableNames.add(match);
}
else {
if (colonIdx + 1 == match.length()) {
throw new IllegalArgumentException("No custom regular expression specified after ':' in \"" + match + "\"");
}
String variablePattern = match.substring(colonIdx + 1, match.length());
this.patternBuilder.append('(');
this.patternBuilder.append(variablePattern);
this.patternBuilder.append(')');
String variableName = match.substring(0, colonIdx);
this.variableNames.add(variableName);
}
end = m.end();
}
this.patternBuilder.append(quote(uriTemplate, end, uriTemplate.length()));

416
org.springframework.web/src/main/java/org/springframework/web/util/UriUtils.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2010 the original author or authors.
* Copyright 2002-2011 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.
@ -18,7 +18,6 @@ package org.springframework.web.util; @@ -18,7 +18,6 @@ package org.springframework.web.util;
import java.io.ByteArrayOutputStream;
import java.io.UnsupportedEncodingException;
import java.util.BitSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -41,23 +40,7 @@ import org.springframework.util.Assert; @@ -41,23 +40,7 @@ import org.springframework.util.Assert;
*/
public abstract class UriUtils {
private static final BitSet SCHEME;
private static final BitSet USER_INFO;
private static final BitSet HOST;
private static final BitSet PORT;
private static final BitSet PATH;
private static final BitSet SEGMENT;
private static final BitSet QUERY;
private static final BitSet QUERY_PARAM;
private static final BitSet FRAGMENT;
private static final String DEFAULT_ENCODING = "UTF-8";
private static final String SCHEME_PATTERN = "([^:/?#]+):";
@ -84,107 +67,7 @@ public abstract class UriUtils { @@ -84,107 +67,7 @@ public abstract class UriUtils {
"^" + HTTP_PATTERN + "(//(" + USERINFO_PATTERN + "@)?" + HOST_PATTERN + "(:" + PORT_PATTERN + ")?" +
")?" + PATH_PATTERN + "(\\?" + LAST_PATTERN + ")?");
static {
// variable names refer to RFC 3986, appendix A
BitSet alpha = new BitSet(256);
for (int i = 'a'; i <= 'z'; i++) {
alpha.set(i);
}
for (int i = 'A'; i <= 'Z'; i++) {
alpha.set(i);
}
BitSet digit = new BitSet(256);
for (int i = '0'; i <= '9'; i++) {
digit.set(i);
}
BitSet gendelims = new BitSet(256);
gendelims.set(':');
gendelims.set('/');
gendelims.set('?');
gendelims.set('#');
gendelims.set('[');
gendelims.set(']');
gendelims.set('@');
BitSet subdelims = new BitSet(256);
subdelims.set('!');
subdelims.set('$');
subdelims.set('&');
subdelims.set('\'');
subdelims.set('(');
subdelims.set(')');
subdelims.set('*');
subdelims.set('+');
subdelims.set(',');
subdelims.set(';');
subdelims.set('=');
BitSet reserved = new BitSet(256);
reserved.or(gendelims);
reserved.or(subdelims);
BitSet unreserved = new BitSet(256);
unreserved.or(alpha);
unreserved.or(digit);
unreserved.set('-');
unreserved.set('.');
unreserved.set('_');
unreserved.set('~');
SCHEME = new BitSet(256);
SCHEME.or(alpha);
SCHEME.or(digit);
SCHEME.set('+');
SCHEME.set('-');
SCHEME.set('.');
USER_INFO = new BitSet(256);
USER_INFO.or(unreserved);
USER_INFO.or(subdelims);
USER_INFO.set(':');
HOST = new BitSet(256);
HOST.or(unreserved);
HOST.or(subdelims);
PORT = new BitSet(256);
PORT.or(digit);
BitSet pchar = new BitSet(256);
pchar.or(unreserved);
pchar.or(subdelims);
pchar.set(':');
pchar.set('@');
SEGMENT = new BitSet(256);
SEGMENT.or(pchar);
PATH = new BitSet(256);
PATH.or(SEGMENT);
PATH.set('/');
QUERY = new BitSet(256);
QUERY.or(pchar);
QUERY.set('/');
QUERY.set('?');
QUERY_PARAM = new BitSet(256);
QUERY_PARAM.or(pchar);
QUERY_PARAM.set('/');
QUERY_PARAM.set('?');
QUERY_PARAM.clear('=');
QUERY_PARAM.clear('+');
QUERY_PARAM.clear('&');
FRAGMENT = new BitSet(256);
FRAGMENT.or(pchar);
FRAGMENT.set('/');
FRAGMENT.set('?');
}
/**
* Encodes the given source URI into an encoded String. All various URI components
* are encoded according to their respective valid character sets.
@ -246,6 +129,38 @@ public abstract class UriUtils { @@ -246,6 +129,38 @@ public abstract class UriUtils {
}
}
/**
* Encodes the given source URI components into an encoded String.
* All various URI components are optional, but encoded according
* to their respective valid character sets.
* @param scheme the scheme
* @param authority the authority
* @param userinfo the user info
* @param host the host
* @param port the port
* @param path the path
* @param query the query
* @param fragment the fragment
* @return the encoded URI
* @throws IllegalArgumentException when the given uri parameter is not a valid URI
*/
public static String encodeUriComponents(String scheme,
String authority,
String userinfo,
String host,
String port,
String path,
String query,
String fragment) {
try {
return encodeUriComponents(scheme, authority, userinfo, host, port, path, query, fragment,
DEFAULT_ENCODING);
}
catch (UnsupportedEncodingException e) {
throw new InternalError("'UTF-8' encoding not supported");
}
}
/**
* Encodes the given source URI components into an encoded String.
* All various URI components are optional, but encoded according
@ -263,10 +178,16 @@ public abstract class UriUtils { @@ -263,10 +178,16 @@ public abstract class UriUtils {
* @throws IllegalArgumentException when the given uri parameter is not a valid URI
* @throws UnsupportedEncodingException when the given encoding parameter is not supported
*/
public static String encodeUriComponents(String scheme, String authority, String userinfo,
String host, String port, String path, String query, String fragment, String encoding)
throws UnsupportedEncodingException {
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();
@ -275,7 +196,7 @@ public abstract class UriUtils { @@ -275,7 +196,7 @@ public abstract class UriUtils {
sb.append(':');
}
if (authority != null) {
if (userinfo != null || host != null || port != null) {
sb.append("//");
if (userinfo != null) {
sb.append(encodeUserInfo(userinfo, encoding));
@ -288,9 +209,14 @@ public abstract class UriUtils { @@ -288,9 +209,14 @@ public abstract class UriUtils {
sb.append(':');
sb.append(encodePort(port, encoding));
}
} else if (authority != null) {
sb.append("//");
sb.append(encodeAuthority(authority, encoding));
}
sb.append(encodePath(path, encoding));
if (path != null) {
sb.append(encodePath(path, encoding));
}
if (query != null) {
sb.append('?');
@ -306,129 +232,194 @@ public abstract class UriUtils { @@ -306,129 +232,194 @@ public abstract class UriUtils {
}
/**
* Encodes the given URI scheme.
* Encodes the given URI scheme with the given encoding.
* @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, SCHEME);
return encode(scheme, encoding, SCHEME_COMPONENT, false);
}
/**
* Encodes the given URI user info.
* 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, AUTHORITY_COMPONENT, 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, USER_INFO);
return encode(userInfo, encoding, USER_INFO_COMPONENT, false);
}
/**
* Encodes the given URI host.
* 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, HOST);
return encode(host, encoding, HOST_COMPONENT, false);
}
/**
* Encodes the given URI port.
* 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, PORT);
return encode(port, encoding, PORT_COMPONENT, false);
}
/**
* Encodes the given URI path.
* 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, PATH);
return encode(path, encoding, PATH_COMPONENT, false);
}
/**
* Encodes the given URI path segment.
* 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, SEGMENT);
return encode(segment, encoding, PATH_SEGMENT_COMPONENT, false);
}
/**
* Encodes the given URI query.
* 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, QUERY);
return encode(query, encoding, QUERY_COMPONENT, false);
}
/**
* Encodes the given URI query parameter.
* 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, QUERY_PARAM);
return encode(queryParam, encoding, QUERY_PARAM_COMPONENT, false);
}
/**
* Encodes the given URI fragment.
* 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, FRAGMENT);
return encode(fragment, encoding, FRAGMENT_COMPONENT, false);
}
private static String encode(String source, String encoding, BitSet notEncoded)
throws UnsupportedEncodingException {
/**
* 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 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
* @throws IllegalArgumentException when the given uri parameter is not a valid URI
* @see #SCHEME_COMPONENT
* @see #AUTHORITY_COMPONENT
* @see #USER_INFO_COMPONENT
* @see #HOST_COMPONENT
* @see #PORT_COMPONENT
* @see #PATH_COMPONENT
* @see #PATH_SEGMENT_COMPONENT
* @see #QUERY_COMPONENT
* @see #QUERY_PARAM_COMPONENT
* @see #FRAGMENT_COMPONENT
*/
public static String encode(String source, UriComponent uriComponent, boolean allowTemplateVars) {
try {
return encode(source, DEFAULT_ENCODING, uriComponent, allowTemplateVars);
}
catch (UnsupportedEncodingException e) {
throw new InternalError("'" + DEFAULT_ENCODING + "' encoding not supported");
}
}
Assert.notNull(source, "'source' must not be null");
/**
* Encodes the given source into an encoded String using the rules specified by the given component.
* @param source the source string
* @param encoding the encoding of the source string
* @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
* @throws IllegalArgumentException when the given uri parameter is not a valid URI
* @see #SCHEME_COMPONENT
* @see #AUTHORITY_COMPONENT
* @see #USER_INFO_COMPONENT
* @see #HOST_COMPONENT
* @see #PORT_COMPONENT
* @see #PATH_COMPONENT
* @see #PATH_SEGMENT_COMPONENT
* @see #QUERY_COMPONENT
* @see #QUERY_PARAM_COMPONENT
* @see #FRAGMENT_COMPONENT
*/
public static String encode(String source, String encoding, UriComponent uriComponent, boolean allowTemplateVars)
throws UnsupportedEncodingException {
Assert.hasLength(encoding, "'encoding' must not be empty");
byte[] bytes = encode(source.getBytes(encoding), notEncoded);
byte[] bytes = encodeInternal(source.getBytes(encoding), uriComponent, allowTemplateVars);
return new String(bytes, "US-ASCII");
}
private static byte[] encode(byte[] source, BitSet notEncoded) {
private static byte[] encodeInternal(byte[] source, UriComponent uriComponent, boolean allowTemplateVars) {
Assert.notNull(source, "'source' must not be null");
ByteArrayOutputStream bos = new ByteArrayOutputStream(source.length * 2);
Assert.notNull(uriComponent, "'uriComponent' 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 (notEncoded.get(b)) {
if (uriComponent.isAllowed(b)) {
bos.write(b);
}
else if (allowTemplateVars && (b == '{' || 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);
}
@ -436,6 +427,7 @@ public abstract class UriUtils { @@ -436,6 +427,7 @@ public abstract class UriUtils {
return bos.toByteArray();
}
/**
* Decodes the given encoded source String into an URI. Based on the following
* rules:
@ -486,4 +478,126 @@ public abstract class UriUtils { @@ -486,4 +478,126 @@ public abstract class UriUtils {
return changed ? new String(bos.toByteArray(), encoding) : source;
}
/**
* Defines the contract for an URI component, i.e. scheme, host, path, etc.
*/
public interface UriComponent {
/**
* Specifies 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
*/
boolean isAllowed(int c);
}
private static abstract class AbstractUriComponent implements UriComponent {
protected boolean isAlpha(int c) {
return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z';
}
protected boolean isDigit(int c) {
return c >= '0' && c <= '9';
}
protected boolean isGenericDelimiter(int c) {
return ':' == c || '/' == c || '?' == c || '#' == c || '[' == c || ']' == c || '@' == c;
}
protected boolean isSubDelimiter(int c) {
return '!' == c || '$' == c || '&' == c || '\'' == c || '(' == c || ')' == c || '*' == c || '+' == c ||
',' == c || ';' == c || '=' == c;
}
protected boolean isReserved(char c) {
return isGenericDelimiter(c) || isReserved(c);
}
protected boolean isUnreserved(int c) {
return isAlpha(c) || isDigit(c) || '-' == c || '.' == c || '_' == c || '~' == c;
}
protected boolean isPchar(int c) {
return isUnreserved(c) || isSubDelimiter(c) || ':' == c || '@' == c;
}
}
/** The scheme URI component. */
public static final UriComponent SCHEME_COMPONENT = new AbstractUriComponent() {
public boolean isAllowed(int c) {
return isAlpha(c) || isDigit(c) || '+' == c || '-' == c || '.' == c;
}
};
/** The authority URI component. */
public static final UriComponent AUTHORITY_COMPONENT = new AbstractUriComponent() {
public boolean isAllowed(int c) {
return isUnreserved(c) || isSubDelimiter(c) || ':' == c || '@' == c;
}
};
/** The user info URI component. */
public static final UriComponent USER_INFO_COMPONENT = new AbstractUriComponent() {
public boolean isAllowed(int c) {
return isUnreserved(c) || isSubDelimiter(c) || ':' == c;
}
};
/** The host URI component. */
public static final UriComponent HOST_COMPONENT = new AbstractUriComponent() {
public boolean isAllowed(int c) {
return isUnreserved(c) || isSubDelimiter(c);
}
};
/** The port URI component. */
public static final UriComponent PORT_COMPONENT = new AbstractUriComponent() {
public boolean isAllowed(int c) {
return isDigit(c);
}
};
/** The path URI component. */
public static final UriComponent PATH_COMPONENT = new AbstractUriComponent() {
public boolean isAllowed(int c) {
return isPchar(c) || '/' == c;
}
};
/** The path segment URI component. */
public static final UriComponent PATH_SEGMENT_COMPONENT = new AbstractUriComponent() {
public boolean isAllowed(int c) {
return isPchar(c);
}
};
/** The query URI component. */
public static final UriComponent QUERY_COMPONENT = new AbstractUriComponent() {
public boolean isAllowed(int c) {
return isPchar(c) || '/' == c || '?' == c;
}
};
/** The query parameter URI component. */
public static final UriComponent QUERY_PARAM_COMPONENT = new AbstractUriComponent() {
public boolean isAllowed(int c) {
if ('=' == c || '+' == c || '&' == c) {
return false;
}
else {
return isPchar(c) || '/' == c || '?' == c;
}
}
};
/** The fragment URI component. */
public static final UriComponent FRAGMENT_COMPONENT = new AbstractUriComponent() {
public boolean isAllowed(int c) {
return isPchar(c) || '/' == c || '?' == c;
}
};
}

122
org.springframework.web/src/test/java/org/springframework/web/util/UriBuilderTests.java

@ -0,0 +1,122 @@ @@ -0,0 +1,122 @@
/*
* Copyright 2002-2011 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.Map;
import org.junit.Test;
import static org.junit.Assert.*;
/** @author Arjen Poutsma */
public class UriBuilderTests {
@Test
public void plain() throws URISyntaxException {
UriBuilder builder = UriBuilder.newInstance();
URI result = builder.scheme("http").host("example.com").path("foo").queryParam("bar").fragment("baz").build();
URI expected = new URI("http://example.com/foo?bar#baz");
assertEquals("Invalid result URI", expected, result);
}
@Test
public void fromUri() throws URISyntaxException {
URI uri = new URI("http://example.com/foo?bar#baz");
URI result = UriBuilder.fromUri(uri).build();
assertEquals("Invalid result URI", uri, result);
}
@Test
public void templateVarsVarArgs() throws URISyntaxException {
UriBuilder builder = UriBuilder.newInstance();
URI result = builder.scheme("http").host("example.com").path("{foo}").build("bar");
URI expected = new URI("http://example.com/bar");
assertEquals("Invalid result URI", expected, result);
}
@Test
public void templateVarsEncoded() throws URISyntaxException, UnsupportedEncodingException {
URI result = UriBuilder.fromPath("{foo}").build("bar baz");
URI expected = new URI("/bar%20baz");
assertEquals("Invalid result URI", expected, result);
}
@Test
public void templateVarsNotEncoded() throws URISyntaxException {
UriBuilder builder = UriBuilder.newInstance();
URI result = builder.scheme("http").host("example.com").path("{foo}").buildFromEncoded("bar%20baz");
URI expected = new URI("http://example.com/bar%20baz");
assertEquals("Invalid result URI", expected, result);
}
@Test
public void templateVarsMap() throws URISyntaxException {
Map<String, String> vars = Collections.singletonMap("foo", "bar");
UriBuilder builder = UriBuilder.newInstance();
URI result = builder.scheme("http").host("example.com").path("{foo}").build(vars);
URI expected = new URI("http://example.com/bar");
assertEquals("Invalid result URI", expected, result);
}
@Test
public void unusedTemplateVars() throws URISyntaxException {
UriBuilder builder = UriBuilder.newInstance();
URI result = builder.scheme("http").host("example.com").path("{foo}").build();
URI expected = new URI("http://example.com/%7Bfoo%7D");
assertEquals("Invalid result URI", expected, result);
}
@Test
public void pathSegments() throws URISyntaxException {
UriBuilder builder = UriBuilder.newInstance();
URI result = builder.pathSegment("foo").pathSegment("bar").build();
URI expected = new URI("/foo/bar");
assertEquals("Invalid result URI", expected, result);
}
@Test
public void queryParam() throws URISyntaxException {
UriBuilder builder = UriBuilder.newInstance();
URI result = builder.queryParam("baz", "qux", 42).build();
URI expected = new URI("?baz=qux&baz=42");
assertEquals("Invalid result URI", expected, result);
}
@Test
public void emptyQueryParam() throws URISyntaxException {
UriBuilder builder = UriBuilder.newInstance();
URI result = builder.queryParam("baz").build();
URI expected = new URI("?baz");
assertEquals("Invalid result URI", expected, result);
}
}

27
org.springframework.web/src/test/java/org/springframework/web/util/UriTemplateTests.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2010 the original author or authors.
* Copyright 2002-2011 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.
@ -23,9 +23,10 @@ import java.util.HashMap; @@ -23,9 +23,10 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.*;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* @author Arjen Poutsma
* @author Juergen Hoeller
@ -47,9 +48,9 @@ public class UriTemplateTests { @@ -47,9 +48,9 @@ public class UriTemplateTests {
}
@Test(expected = IllegalArgumentException.class)
public void expandVarArgsInvalidAmountVariables() throws Exception {
public void expandVarArgsNotEnoughVariables() throws Exception {
UriTemplate template = new UriTemplate("http://example.com/hotels/{hotel}/bookings/{booking}");
template.expand("1", "42", "100");
template.expand("1");
}
@Test
@ -110,6 +111,13 @@ public class UriTemplateTests { @@ -110,6 +111,13 @@ public class UriTemplateTests {
assertFalse("UriTemplate matches", template.matches(""));
assertFalse("UriTemplate matches", template.matches(null));
}
@Test
public void matchesCustomRegex() throws Exception {
UriTemplate template = new UriTemplate("http://example.com/hotels/{hotel:\\d+}");
assertTrue("UriTemplate does not match", template.matches("http://example.com/hotels/42"));
assertFalse("UriTemplate matches", template.matches("http://example.com/hotels/foo"));
}
@Test
public void match() throws Exception {
@ -122,6 +130,17 @@ public class UriTemplateTests { @@ -122,6 +130,17 @@ public class UriTemplateTests {
assertEquals("Invalid match", expected, result);
}
@Test
public void matchCustomRegex() throws Exception {
Map<String, String> expected = new HashMap<String, String>(2);
expected.put("booking", "42");
expected.put("hotel", "1");
UriTemplate template = new UriTemplate("http://example.com/hotels/{hotel:\\d}/bookings/{booking:\\d+}");
Map<String, String> result = template.match("http://example.com/hotels/1/bookings/42");
assertEquals("Invalid match", expected, result);
}
@Test
public void matchDuplicate() throws Exception {
UriTemplate template = new UriTemplate("/order/{c}/{c}/{c}");

Loading…
Cancel
Save