diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/support/ServletUriBuilder.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/support/ServletUriComponentsBuilder.java similarity index 83% rename from org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/support/ServletUriBuilder.java rename to org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/support/ServletUriComponentsBuilder.java index 536c23efbf7..4c5b0413e60 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/support/ServletUriBuilder.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/support/ServletUriComponentsBuilder.java @@ -22,13 +22,13 @@ import org.springframework.util.Assert; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; -import org.springframework.web.util.UriBuilder; +import org.springframework.web.util.UriComponentsBuilder; /** @author Arjen Poutsma */ -public class ServletUriBuilder extends UriBuilder { +public class ServletUriComponentsBuilder extends UriComponentsBuilder { - public static ServletUriBuilder fromCurrentServletRequest() { + public static ServletUriComponentsBuilder fromCurrentServletRequest() { RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); Assert.state(requestAttributes != null, "Could not find current RequestAttributes in RequestContextHolder"); Assert.isInstanceOf(ServletRequestAttributes.class, requestAttributes); @@ -38,10 +38,10 @@ public class ServletUriBuilder extends UriBuilder { return fromServletRequest(servletRequest); } - public static ServletUriBuilder fromServletRequest(HttpServletRequest request) { + public static ServletUriComponentsBuilder fromServletRequest(HttpServletRequest request) { Assert.notNull(request, "'request' must not be null"); - ServletUriBuilder builder = new ServletUriBuilder(); + ServletUriComponentsBuilder builder = new ServletUriComponentsBuilder(); builder.scheme(request.getScheme()); builder.host(request.getServerName()); builder.port(request.getServerPort()); diff --git a/org.springframework.web/src/main/java/org/springframework/web/util/UriBuilder.java b/org.springframework.web/src/main/java/org/springframework/web/util/UriBuilder.java deleted file mode 100644 index 205d9b7d458..00000000000 --- a/org.springframework.web/src/main/java/org/springframework/web/util/UriBuilder.java +++ /dev/null @@ -1,515 +0,0 @@ -/* - * 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.EnumSet; -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. - * - *

Typical usage involves: - *

    - *
  1. Create a {@code UriBuilder} with one of the static factory methods (such as {@link #fromPath(String)} or - * {@link #fromUri(URI)})
  2. - *
  3. 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)}.
  4. - *
  5. Build the URI with one of the {@link #build} method variants.
  6. - *
- * - *

Most of the URI component methods accept URI template variables (i.e. {@code "{foo}"}), which are expanded by - * calling {@code build}. - * - *

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 pathSegments = new ArrayList(); - - 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() { - String port = portAsString(); - String path = null; - if (!pathSegments.isEmpty()) { - StringBuilder pathBuilder = new StringBuilder(); - for (String pathSegment : pathSegments) { - boolean startsWithSlash = pathSegment.charAt(0) == '/'; - boolean endsWithSlash = pathBuilder.length() > 0 && pathBuilder.charAt(pathBuilder.length() - 1) == '/'; - - if (!endsWithSlash && !startsWithSlash) { - pathBuilder.append('/'); - } - else if (endsWithSlash && startsWithSlash) { - pathSegment = pathSegment.substring(1); - } - pathBuilder.append(pathSegment); - } - path = pathBuilder.toString(); - } - String query = queryAsString(); - - String uri = UriUtils.buildUri(scheme, null, userInfo, host, port, path, query, fragment); - - 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 uriVariables) { - return buildFromMap(uriVariables, true); - } - - /** - * 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 uriVariables) { - return buildFromMap(uriVariables, false); - } - - private URI buildFromMap(Map uriVariables, boolean encodeUriVariableValues) { - if (CollectionUtils.isEmpty(uriVariables)) { - return build(); - } - String scheme = expand(this.scheme, UriComponents.Type.SCHEME, uriVariables, encodeUriVariableValues); - String userInfo = expand(this.userInfo, UriComponents.Type.USER_INFO, uriVariables, encodeUriVariableValues); - String host = expand(this.host, UriComponents.Type.HOST, uriVariables, encodeUriVariableValues); - String port = expand(this.portAsString(), UriComponents.Type.PORT, uriVariables, encodeUriVariableValues); - String path = null; - if (!this.pathSegments.isEmpty()) { - StringBuilder pathBuilder = new StringBuilder(); - for (String pathSegment : this.pathSegments) { - boolean startsWithSlash = pathSegment.charAt(0) == '/'; - boolean endsWithSlash = pathBuilder.length() > 0 && pathBuilder.charAt(pathBuilder.length() - 1) == '/'; - - if (!endsWithSlash && !startsWithSlash) { - pathBuilder.append('/'); - } - else if (endsWithSlash && startsWithSlash) { - pathSegment = pathSegment.substring(1); - } - pathSegment = expand(pathSegment, UriComponents.Type.PATH_SEGMENT, uriVariables, encodeUriVariableValues); - pathBuilder.append(pathSegment); - } - path = pathBuilder.toString(); - } - String query = expand(this.queryAsString(), UriComponents.Type.QUERY, uriVariables, encodeUriVariableValues); - String fragment = expand(this.fragment, UriComponents.Type.FRAGMENT, uriVariables, encodeUriVariableValues); - - String uri = UriUtils.buildUri(scheme, null, userInfo, host, port, path, query, fragment); - return URI.create(uri); - } - - private String expand(String source, - UriComponents.Type uriComponent, - Map uriVariables, - boolean encodeUriVariableValues) { - if (source == null) { - return null; - } - if (source.indexOf('{') == -1) { - return source; - } - UriTemplate template = new UriComponentTemplate(source, uriComponent, encodeUriVariableValues); - return template.expandAsString(uriVariables); - } - - /** - * 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 UriComponentTemplate(scheme, UriComponents.Type.SCHEME, encodeUriVariableValues); - uriBuilder.append(template.expandAsString(uriVariableValues)); - uriBuilder.append(':'); - } - - if (userInfo != null || host != null || port != -1) { - uriBuilder.append("//"); - - if (StringUtils.hasLength(userInfo)) { - template = new UriComponentTemplate(userInfo, UriComponents.Type.USER_INFO, encodeUriVariableValues); - uriBuilder.append(template.expandAsString(uriVariableValues)); - uriBuilder.append('@'); - } - - if (host != null) { - template = new UriComponentTemplate(host, UriComponents.Type.HOST, encodeUriVariableValues); - uriBuilder.append(template.expandAsString(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 UriComponentTemplate(pathSegment, UriComponents.Type.PATH_SEGMENT, encodeUriVariableValues); - uriBuilder.append(template.expandAsString(uriVariableValues)); - } - } - - if (queryBuilder.length() > 0) { - uriBuilder.append('?'); - template = new UriComponentTemplate(queryBuilder.toString(), UriComponents.Type.QUERY, encodeUriVariableValues); - uriBuilder.append(template.expandAsString(uriVariableValues)); - } - - if (StringUtils.hasLength(fragment)) { - uriBuilder.append('#'); - template = new UriComponentTemplate(fragment, UriComponents.Type.FRAGMENT, encodeUriVariableValues); - uriBuilder.append(template.expandAsString(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 = encodeUriComponent(scheme, UriComponents.Type.SCHEME); - } - 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 = encodeUriComponent(userInfo, UriComponents.Type.USER_INFO); - } - 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 = encodeUriComponent(host, UriComponents.Type.HOST); - } - 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; - } - - 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. - * - * @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(encodeUriComponent(segment, UriComponents.Type.PATH_SEGMENT)); - } - - 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 = encodeUriComponent(name, UriComponents.Type.QUERY_PARAM); - - 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(encodeUriComponent(valueAsString, UriComponents.Type.QUERY_PARAM)); - } - - } - } - 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 - * 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 = encodeUriComponent(fragment, UriComponents.Type.FRAGMENT); - } - else { - this.fragment = null; - } - return this; - } - - - private String encodeUriComponent(String source, UriComponents.Type uriComponent) { - return UriUtils.encodeUriComponent(source, uriComponent, EnumSet.of(UriUtils.EncodingOption.ALLOW_TEMPLATE_VARS)); - } - - -} diff --git a/org.springframework.web/src/main/java/org/springframework/web/util/UriComponentTemplate.java b/org.springframework.web/src/main/java/org/springframework/web/util/UriComponentTemplate.java deleted file mode 100644 index c2d73bbce7b..00000000000 --- a/org.springframework.web/src/main/java/org/springframework/web/util/UriComponentTemplate.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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 org.springframework.util.Assert; - -/** - * Subclass of {@link UriTemplate} that operates on URI components, rather than full URIs. - * - * @author Arjen Poutsma - * @since 3.1 - */ -class UriComponentTemplate extends UriTemplate { - - private final UriComponents.Type uriComponent; - - private boolean encodeUriVariableValues; - - UriComponentTemplate(String uriTemplate, UriComponents.Type uriComponent, boolean encodeUriVariableValues) { - super(uriTemplate); - Assert.notNull(uriComponent, "'uriComponent' must not be null"); - this.uriComponent = uriComponent; - this.encodeUriVariableValues = encodeUriVariableValues; - } - - @Override - protected String getVariableValueAsString(Object variableValue) { - String variableValueString = super.getVariableValueAsString(variableValue); - return encodeUriVariableValues ? UriUtils.encodeUriComponent(variableValueString, uriComponent) : - variableValueString; - } -} 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 92ae9094598..1552d44c797 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,11 +16,14 @@ package org.springframework.web.util; +import java.io.ByteArrayOutputStream; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.EnumMap; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -28,526 +31,748 @@ 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 the components that make up a URI, mapping component type to string values. Contains convenience getters - * and setters for all components, as well as the regular {@link Map} implementation. - * - *

This mapping does not contain mappings for {@link org.springframework.web.util.UriComponents.Type#PATH_SEGMENT} or nor {@link - * org.springframework.web.util.UriComponents.Type#QUERY_PARAM}, since those components can occur multiple times in the URI. Instead, one can use {@link - * #getPathSegments()} or {@link #getQueryParams()} respectively. + * Represents an immutable collection of URI components, mapping component type to string values. Contains convenience + * getters for all components, as well as the regular {@link Map} implementation. Effectively similar to {@link URI}, + * but with more powerful encoding options. + *

+ * Note that this {@code Map} does not contain entries for {@link Type#PATH_SEGMENT} + * nor {@link Type#QUERY_PARAM}, since those components can occur multiple + * times in the URI. Instead, one can use {@link #getPathSegments()} or {@link #getQueryParams()} respectively. * * @author Arjen Poutsma * @since 3.1 + * @see UriComponentsBuilder */ -public class UriComponents implements Map { +public final class UriComponents implements Map { - private static final String PATH_DELIMITER = "/"; + /** + * The default encoding used for various encode methods. + */ + public static final String DEFAULT_ENCODING = "UTF-8"; - private static final Pattern QUERY_PARAM_PATTERN = Pattern.compile("([^&=]+)=?([^&=]+)?"); + private static final String SCHEME_PATTERN = "([^:/?#]+):"; - private final Map uriComponents; + private static final String HTTP_PATTERN = "(http|https):"; - /** Constructs a new, empty instance of the {@code UriComponents} object. */ - public UriComponents() { - this.uriComponents = new EnumMap(Type.class); - } + private static final String USERINFO_PATTERN = "([^@/]*)"; - /** - * Creates an instance of the {@code UriComponents} object that contains the given component. - * - * @param uriComponents the component to initialize with - */ - public UriComponents(Map uriComponents) { - Assert.notNull(uriComponents, "'uriComponents' must not be null"); - this.uriComponents = new EnumMap(uriComponents); - } + private static final String HOST_PATTERN = "([^/?#:]*)"; - // convenience properties + private static final String PORT_PATTERN = "(\\d*)"; - /** - * Returns the scheme. - * - * @return the scheme. Can be {@code null}. - */ - public String getScheme() { - return get(Type.SCHEME); - } + private static final String PATH_PATTERN = "([^?#]*)"; - /** - * Sets the scheme. - * - * @param scheme the scheme. Can be {@code null}. - */ - public void setScheme(String scheme) { - put(Type.SCHEME, scheme); - } + private static final String QUERY_PATTERN = "([^#]*)"; - /** - * Returns the authority. - * - * @return the authority. Can be {@code null}. - */ - public String getAuthority() { - return get(Type.AUTHORITY); - } + private static final String LAST_PATTERN = "(.*)"; - /** - * Sets the authority. - * - * @param authority the authority. Can be {@code null}. - */ - public void setAuthority(String authority) { - put(Type.AUTHORITY, authority); - } + // Regex patterns that matches URIs. See RFC 3986, appendix B + private static final Pattern URI_PATTERN = Pattern.compile( + "^(" + SCHEME_PATTERN + ")?" + "(//(" + USERINFO_PATTERN + "@)?" + HOST_PATTERN + "(:" + PORT_PATTERN + + ")?" + ")?" + PATH_PATTERN + "(\\?" + QUERY_PATTERN + ")?" + "(#" + LAST_PATTERN + ")?"); - /** - * Returns the user info. - * - * @return the user info. Can be {@code null}. - */ - public String getUserInfo() { - return get(Type.USER_INFO); - } + private static final Pattern HTTP_URL_PATTERN = Pattern.compile( + "^" + HTTP_PATTERN + "(//(" + USERINFO_PATTERN + "@)?" + HOST_PATTERN + "(:" + PORT_PATTERN + ")?" + ")?" + + PATH_PATTERN + "(\\?" + LAST_PATTERN + ")?"); - /** - * Sets the user info - * - * @param userInfo the user info. Can be {@code null} - */ - public void setUserInfo(String userInfo) { - put(Type.USER_INFO, userInfo); - } - /** - * Returns the host. - * - * @return the host. Can be {@code null}. - */ - public String getHost() { - return get(Type.HOST); - } + private static final String PATH_DELIMITER = "/"; - /** - * Sets the host. - * - * @param host the host. Can be {@code null}. - */ - public void setHost(String host) { - put(Type.HOST, host); - } + private static final Pattern QUERY_PARAM_PATTERN = Pattern.compile("([^&=]+)=?([^&=]+)?"); - /** - * Returns the port as string. - * - * @return the port as string. Can be {@code null}. - */ - public String getPort() { - return get(Type.PORT); - } + private final Map uriComponents; - /** - * Sets the port as string. - * - * @param port the port as string. Can be {@code null}. - */ - public void setPort(String port) { - put(Type.PORT, port); - } + private final boolean encoded; - /** - * Returns the port as integer. Returns {@code -1} if no port has been set. - * - * @return the port the port as integer - */ - public int getPortAsInteger() { - String port = getPort(); - return port != null ? Integer.parseInt(port) : -1; - } + private UriComponents(Map uriComponents, boolean encoded) { + Assert.notEmpty(uriComponents, "'uriComponents' must not be empty"); + this.uriComponents = Collections.unmodifiableMap(uriComponents); + this.encoded = encoded; + } - /** - * Sets the port as integer. A value < 0 resets the port to an empty value. - * - * @param port the port as integer - */ - public void setPortAsInteger(int port) { - String portString = port > -1 ? Integer.toString(port) : null; - put(Type.PORT, portString); - } + /** + * Creates a new {@code UriComponents} object from the given string URI. + * + * @param uri the source URI + * @return the URI components of the URI + */ + public static UriComponents fromUriString(String uri) { + Assert.notNull(uri, "'uri' must not be null"); + Matcher m = URI_PATTERN.matcher(uri); + if (m.matches()) { + Map result = new EnumMap(UriComponents.Type.class); - /** - * Returns the path. - * - * @return the path. Can be {@code null}. - */ - public String getPath() { - return get(Type.PATH); - } + result.put(UriComponents.Type.SCHEME, m.group(2)); + result.put(UriComponents.Type.AUTHORITY, m.group(3)); + result.put(UriComponents.Type.USER_INFO, m.group(5)); + result.put(UriComponents.Type.HOST, m.group(6)); + result.put(UriComponents.Type.PORT, m.group(8)); + result.put(UriComponents.Type.PATH, m.group(9)); + result.put(UriComponents.Type.QUERY, m.group(11)); + result.put(UriComponents.Type.FRAGMENT, m.group(13)); - /** - * Sets the path. - * - * @param path the path. Can be {@code null}. - */ - public void setPath(String path) { - put(Type.PATH, path); - } + return new UriComponents(result, false); + } + else { + throw new IllegalArgumentException("[" + uri + "] is not a valid URI"); + } + } - /** - * Returns the list of path segments. - * - * @return the path segments. Empty if no path has been set. - */ - public List getPathSegments() { - String path = getPath(); - if (path != null) { - return Arrays.asList(StringUtils.tokenizeToStringArray(path, PATH_DELIMITER)); - } - else { - return Collections.emptyList(); - } - } + /** + * Creates a new {@code UriComponents} object from the string HTTP URL. + * + * @param httpUrl the source URI + * @return the URI components of the URI + */ + public static UriComponents fromHttpUrl(String httpUrl) { + Assert.notNull(httpUrl, "'httpUrl' must not be null"); + Matcher m = HTTP_URL_PATTERN.matcher(httpUrl); + if (m.matches()) { + Map result = new EnumMap(UriComponents.Type.class); - /** - * Sets the path segments. An empty or {@code null} value resets the path to an empty value. - * - * @param pathSegments the path segments - */ - public void setPathSegments(List pathSegments) { - if (!CollectionUtils.isEmpty(pathSegments)) { - StringBuilder pathBuilder = new StringBuilder("/"); - for (Iterator iterator = pathSegments.iterator(); iterator.hasNext(); ) { - String pathSegment = iterator.next(); - pathBuilder.append(pathSegment); - if (iterator.hasNext()) { - pathBuilder.append('/'); - } - } - setPath(pathBuilder.toString()); - } - else { - setPath(null); - } - } + result.put(UriComponents.Type.SCHEME, m.group(1)); + result.put(UriComponents.Type.AUTHORITY, m.group(2)); + result.put(UriComponents.Type.USER_INFO, m.group(4)); + result.put(UriComponents.Type.HOST, m.group(5)); + result.put(UriComponents.Type.PORT, m.group(7)); + result.put(UriComponents.Type.PATH, m.group(8)); + result.put(UriComponents.Type.QUERY, m.group(10)); - /** - * Returns the query. - * - * @return the query. Can be {@code null}. - */ - public String getQuery() { - return get(Type.QUERY); - } + return new UriComponents(result, false); + } + else { + throw new IllegalArgumentException("[" + httpUrl + "] is not a valid HTTP URL"); + } + } - /** - * Sets the query. - * - * @param query the query. Can be {@code null}. - */ - public void setQuery(String query) { - put(Type.QUERY, query); - } + /** + * Creates a new {@code UriComponents} object from the given {@code URI}. + * + * @param uri the URI + * @return the URI components of the URI + */ + public static UriComponents fromUri(URI uri) { + Assert.notNull(uri, "'uri' must not be null"); - /** - * Returns the map of query parameters. - * - * @return the query parameters. Empty if no query has been set. - */ - public MultiValueMap getQueryParams() { - MultiValueMap result = new LinkedMultiValueMap(); - String query = getQuery(); - if (query != null) { - Matcher m = QUERY_PARAM_PATTERN.matcher(query); - while (m.find()) { - String name = m.group(1); - String value = m.group(2); - result.add(name, value); - } - } - return result; - } + Map uriComponents = new EnumMap(Type.class); + if (uri.getScheme() != null) { + uriComponents.put(Type.SCHEME, uri.getScheme()); + } + if (uri.getRawAuthority() != null) { + uriComponents.put(Type.AUTHORITY, uri.getRawAuthority()); + } + if (uri.getRawUserInfo() != null) { + uriComponents.put(Type.USER_INFO, uri.getRawUserInfo()); + } + if (uri.getHost() != null) { + uriComponents.put(Type.HOST, uri.getHost()); + } + if (uri.getPort() != -1) { + uriComponents.put(Type.PORT, Integer.toString(uri.getPort())); + } + if (uri.getRawPath() != null) { + uriComponents.put(Type.PATH, uri.getRawPath()); + } + if (uri.getRawQuery() != null) { + uriComponents.put(Type.QUERY, uri.getRawQuery()); + } + if (uri.getRawFragment() != null) { + uriComponents.put(Type.FRAGMENT, uri.getRawFragment()); + } + return new UriComponents(uriComponents, true); + } - /** - * Sets the query parameters. An empty or {@code null} value resets the query to an empty value. - * - * @param queryParams the query parameters - */ - public void setQueryParams(MultiValueMap queryParams) { - if (!CollectionUtils.isEmpty(queryParams)) { - StringBuilder queryBuilder = new StringBuilder(); - for (Iterator>> entryIterator = queryParams.entrySet().iterator(); - entryIterator.hasNext(); ) { - Entry> entry = entryIterator.next(); - String name = entry.getKey(); - List values = entry.getValue(); - if (CollectionUtils.isEmpty(values) || (values.size() == 1 && values.get(0) == null)) { - queryBuilder.append(name); - } - else { - for (Iterator valueIterator = values.iterator(); valueIterator.hasNext(); ) { - String value = valueIterator.next(); - queryBuilder.append(name); - queryBuilder.append('='); - queryBuilder.append(value); - if (valueIterator.hasNext()) { - queryBuilder.append('&'); - } - } - } - if (entryIterator.hasNext()) { - queryBuilder.append('&'); - } - } - setQuery(queryBuilder.toString()); - } - else { - setQuery(null); - } - } - /** - * Returns the fragment. - * - * @return the fragment. Can be {@code null}. - */ - public String getFragment() { - return get(Type.FRAGMENT); - } + /** + * Creates an instance of the {@code UriComponents} object from the given components. All the given arguments + * can be {@code null} and are considered to be unencoded. + */ + public static UriComponents fromUriComponents(String scheme, + String authority, + String userInfo, + String host, + String port, + String path, + String query, + String fragment) { + return fromUriComponents(scheme, authority, userInfo, host, port, path, query, fragment, false); + } - /** - * Sets the fragment. - * - * @param fragment the fragment. Can be {@code null}. - */ - public void setFragment(String fragment) { - put(Type.FRAGMENT, fragment); - } + /** + * Creates an instance of the {@code UriComponents} object from the given components. All the given arguments + * can be {@code null}. + * + * @param encoded {@code true} if the arguments are encoded; {@code false} otherwise + */ + public static UriComponents fromUriComponents(String scheme, + String authority, + String userInfo, + String host, + String port, + String path, + String query, + String fragment, + boolean encoded) { + Map uriComponents = new EnumMap(Type.class); + if (scheme != null) { + uriComponents.put(Type.SCHEME, scheme); + } + if (authority != null) { + uriComponents.put(Type.AUTHORITY, authority); + } + if (userInfo != null) { + uriComponents.put(Type.USER_INFO, userInfo); + } + if (host != null) { + uriComponents.put(Type.HOST, host); + } + if (port != null) { + uriComponents.put(Type.PORT, port); + } + if (path != null) { + uriComponents.put(Type.PATH, path); + } + if (query != null) { + uriComponents.put(Type.QUERY, query); + } + if (fragment != null) { + uriComponents.put(Type.FRAGMENT, fragment); + } + return new UriComponents(uriComponents, encoded); + } - // Map implementation + /** + * Creates an instance of the {@code UriComponents} object that contains the given components map. + * + * @param uriComponents the component to initialize with + */ + public static UriComponents fromUriComponentMap(Map uriComponents) { + boolean encoded; + if (uriComponents instanceof UriComponents) { + encoded = ((UriComponents) uriComponents).encoded; + } + else { + encoded = false; + } + return new UriComponents(uriComponents, encoded); + } - public int size() { - return this.uriComponents.size(); - } + /** + * Creates an instance of the {@code UriComponents} object that contains the given components map. + * + * @param uriComponents the component to initialize with + * @param encoded whether the components are encpded + */ + public static UriComponents fromUriComponentMap(Map uriComponents, boolean encoded) { + return new UriComponents(uriComponents, encoded); + } - public boolean isEmpty() { - return this.uriComponents.isEmpty(); - } + // component getters - public boolean containsKey(Object key) { - return this.uriComponents.containsKey(key); - } + /** + * Returns the scheme. + * + * @return the scheme. Can be {@code null}. + */ + public String getScheme() { + return get(Type.SCHEME); + } - public boolean containsValue(Object value) { - return this.uriComponents.containsValue(value); - } + /** + * Returns the authority. + * + * @return the authority. Can be {@code null}. + */ + public String getAuthority() { + return get(Type.AUTHORITY); + } - public String get(Object key) { - return this.uriComponents.get(key); - } + /** + * Returns the user info. + * + * @return the user info. Can be {@code null}. + */ + public String getUserInfo() { + return get(Type.USER_INFO); + } - public String put(Type key, String value) { - return this.uriComponents.put(key, value); - } + /** + * Returns the host. + * + * @return the host. Can be {@code null}. + */ + public String getHost() { + return get(Type.HOST); + } - public String remove(Object key) { - return this.uriComponents.remove(key); - } + /** + * Returns the port as string. + * + * @return the port as string. Can be {@code null}. + */ + public String getPort() { + return get(Type.PORT); + } - public void putAll(Map m) { - this.uriComponents.putAll(m); - } + /** + * Returns the port as integer. Returns {@code -1} if no port has been set. + * + * @return the port the port as integer + */ + public int getPortAsInteger() { + String port = getPort(); + return port != null ? Integer.parseInt(port) : -1; + } - public void clear() { - this.uriComponents.clear(); - } + /** + * Returns the path. + * + * @return the path. Can be {@code null}. + */ + public String getPath() { + return get(Type.PATH); + } - public Set keySet() { - return this.uriComponents.keySet(); - } + /** + * Returns the list of path segments. + * + * @return the path segments. Empty if no path has been set. + */ + public List getPathSegments() { + String path = getPath(); + if (path != null) { + return Arrays.asList(StringUtils.tokenizeToStringArray(path, PATH_DELIMITER)); + } + else { + return Collections.emptyList(); + } + } - public Collection values() { - return this.uriComponents.values(); - } + /** + * Returns the query. + * + * @return the query. Can be {@code null}. + */ + public String getQuery() { + return get(Type.QUERY); + } - public Set> entrySet() { - return this.uriComponents.entrySet(); - } + /** + * Returns the map of query parameters. + * + * @return the query parameters. Empty if no query has been set. + */ + public MultiValueMap getQueryParams() { + MultiValueMap result = new LinkedMultiValueMap(); + String query = getQuery(); + if (query != null) { + Matcher m = QUERY_PARAM_PATTERN.matcher(query); + while (m.find()) { + String name = m.group(1); + String value = m.group(2); + result.add(name, value); + } + } + return result; + } - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o instanceof UriComponents) { - UriComponents other = (UriComponents) o; - return this.uriComponents.equals(other.uriComponents); - } - return false; - } + /** + * Returns the fragment. + * + * @return the fragment. Can be {@code null}. + */ + public String getFragment() { + return get(Type.FRAGMENT); + } - @Override - public int hashCode() { - return this.uriComponents.hashCode(); - } + // other functionality - @Override - public String toString() { - return this.uriComponents.toString(); - } + public UriComponents encode() { + try { + return encode(DEFAULT_ENCODING); + } + catch (UnsupportedEncodingException e) { + throw new InternalError("\"" + DEFAULT_ENCODING + "\" not supported"); + } + } - // inner types + /** + * 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"); - /** - * 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 - */ - public static enum Type { + if (encoded) { + return this; + } - 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; - } - }; + final Map encoded = new EnumMap(Type.class); + for (Entry entry : uriComponents.entrySet()) { + Type key = entry.getKey(); + String value = entry.getValue(); + if (value != null) { + value = encodeUriComponent(value, encoding, key); + } + encoded.put(key, value); + } + return new UriComponents(encoded, true); + } - /** - * 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); + /** + * 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 + */ + static String encodeUriComponent(String source, + String encoding, + UriComponents.Type uriComponent) throws UnsupportedEncodingException { + Assert.hasLength(encoding, "'encoding' must not be empty"); - /** - * 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'; - } + byte[] bytes = encodeInternal(source.getBytes(encoding), uriComponent); + return new String(bytes, "US-ASCII"); + } - /** - * 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'; - } + private static byte[] encodeInternal(byte[] source, + UriComponents.Type uriComponent) { + Assert.notNull(source, "'source' must not be null"); + Assert.notNull(uriComponent, "'uriComponent' must not be null"); - /** - * 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; - } + ByteArrayOutputStream bos = new ByteArrayOutputStream(source.length); + for (int i = 0; i < source.length; i++) { + int b = source[i]; + if (b < 0) { + b += 256; + } + if (uriComponent.isAllowed(b)) { + bos.write(b); + } + else { + bos.write('%'); - /** - * 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; - } + char hex1 = Character.toUpperCase(Character.forDigit((b >> 4) & 0xF, 16)); + char hex2 = Character.toUpperCase(Character.forDigit(b & 0xF, 16)); - /** - * 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); - } + bos.write(hex1); + bos.write(hex2); + } + } + return bos.toByteArray(); + } - /** - * 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; - } + /** + * Returns a URI string from this {@code UriComponents} instance. + * + * @return the URI created from the given components + */ + public String toUriString() { + StringBuilder uriBuilder = new StringBuilder(); + + if (getScheme() != null) { + uriBuilder.append(getScheme()); + uriBuilder.append(':'); + } + + if (getUserInfo() != null || getHost() != null || getPort() != null) { + uriBuilder.append("//"); + if (getUserInfo() != null) { + uriBuilder.append(getUserInfo()); + uriBuilder.append('@'); + } + if (getHost() != null) { + uriBuilder.append(getHost()); + } + if (getPort() != null) { + uriBuilder.append(':'); + uriBuilder.append(getPort()); + } + } + else if (getAuthority() != null) { + uriBuilder.append("//"); + uriBuilder.append(getAuthority()); + } + + if (getPath() != null) { + uriBuilder.append(getPath()); + } + + if (getQuery() != null) { + uriBuilder.append('?'); + uriBuilder.append(getQuery()); + } + + if (getFragment() != null) { + uriBuilder.append('#'); + uriBuilder.append(getFragment()); + } + + return uriBuilder.toString(); + } + + /** + * Returns a {@code URI} from this {@code UriComponents} instance. + * + * @return the URI created from the given components + */ + public URI toUri() { + try { + if (encoded) { + return new URI(toUriString()); + } + else { + if (getUserInfo() != null || getHost() != null || getPort() != null) { + return new URI(getScheme(), getUserInfo(), getHost(), getPortAsInteger(), getPath(), getQuery(), + getFragment()); + } + else { + return new URI(getScheme(), getAuthority(), getPath(), getQuery(), getFragment()); + } + } + } + catch (URISyntaxException ex) { + throw new IllegalStateException("Could not create URI object: " + ex.getMessage(), ex); + } + } + + // Map implementation + + public int size() { + return this.uriComponents.size(); + } + + public boolean isEmpty() { + return this.uriComponents.isEmpty(); + } + + public boolean containsKey(Object key) { + return this.uriComponents.containsKey(key); + } + + public boolean containsValue(Object value) { + return this.uriComponents.containsValue(value); + } + + public String get(Object key) { + return this.uriComponents.get(key); + } + + public String put(Type key, String value) { + return this.uriComponents.put(key, value); + } + + public String remove(Object key) { + return this.uriComponents.remove(key); + } + + public void putAll(Map m) { + this.uriComponents.putAll(m); + } + + public void clear() { + this.uriComponents.clear(); + } + + public Set keySet() { + return this.uriComponents.keySet(); + } + + public Collection values() { + return this.uriComponents.values(); + } + + public Set> entrySet() { + return this.uriComponents.entrySet(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o instanceof UriComponents) { + UriComponents other = (UriComponents) o; + return this.uriComponents.equals(other.uriComponents); + } + return false; + } + + @Override + public int hashCode() { + return this.uriComponents.hashCode(); + } + + @Override + public String toString() { + return this.uriComponents.toString(); + } + + // 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 + */ + public 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; + } + + } - } } 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 new file mode 100644 index 00000000000..b94de723cb9 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java @@ -0,0 +1,323 @@ +/* + * 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 org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * Builder for {@link UriComponents}. + *

+ * 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. + *
  3. 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)}.
  4. + *
  5. Build the {@link UriComponents} instance with the {@link #build()} method.
  6. + *
+ * + * @author Arjen Poutsma + * @see #newInstance() + * @see #fromPath(String) + * @see #fromUri(URI) + * @since 3.1 + */ +public class UriComponentsBuilder { + + private static final char PATH_DELIMITER = '/'; + + private String scheme; + + private String userInfo; + + private String host; + + private int port = -1; + + private final List pathSegments = new ArrayList(); + + 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 UriComponentsBuilder() { + } + + // Factory methods + + /** + * Returns a new, empty URI builder. + * + * @return the new {@code UriComponentsBuilder} + */ + public static UriComponentsBuilder newInstance() { + return new UriComponentsBuilder(); + } + + /** + * Returns a URI builder that is initialized with the given path. + * + * @param path the path to initialize with + * @return the new {@code UriComponentsBuilder} + */ + public static UriComponentsBuilder fromPath(String path) { + UriComponentsBuilder builder = new UriComponentsBuilder(); + 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 UriComponentsBuilder} + */ + public static UriComponentsBuilder fromUri(URI uri) { + UriComponentsBuilder builder = new UriComponentsBuilder(); + builder.uri(uri); + return builder; + } + + // build methods + + /** + * Builds a {@code UriComponents} instance from the various components contained in this builder. + * + * @return the URI components + */ + public UriComponents build() { + String port = portAsString(); + String path = pathAsString(); + String query = queryAsString(); + return UriComponents.fromUriComponents(scheme, null, userInfo, host, port, path, query, fragment, false); + } + + // URI components methods + + /** + * Initializes all components of this URI builder with the components of the given URI. + * + * @param uri the URI + * @return this 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.getUserInfo() != null) { + this.userInfo = uri.getUserInfo(); + } + if (uri.getHost() != null) { + this.host = uri.getHost(); + } + if (uri.getPort() != -1) { + this.port = uri.getPort(); + } + if (StringUtils.hasLength(uri.getPath())) { + String[] pathSegments = StringUtils.tokenizeToStringArray(uri.getPath(), "/"); + + this.pathSegments.clear(); + Collections.addAll(this.pathSegments, pathSegments); + } + if (StringUtils.hasLength(uri.getQuery())) { + this.queryBuilder.setLength(0); + this.queryBuilder.append(uri.getQuery()); + } + if (uri.getFragment() != null) { + this.fragment = uri.getFragment(); + } + 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 UriComponentsBuilder + */ + public UriComponentsBuilder scheme(String scheme) { + this.scheme = scheme; + 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; + 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 UriComponentsBuilder + */ + public UriComponentsBuilder host(String host) { + this.host = host; + return this; + } + + /** + * Sets the URI port. Passing {@code -1} will clear the port of this builder. + * + * @param port the URI port + * @return this UriComponentsBuilder + */ + public UriComponentsBuilder port(int port) { + Assert.isTrue(port >= -1, "'port' must not be < -1"); + this.port = port; + 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. + * + * @param path the URI path + * @return this UriComponentsBuilder + */ + public UriComponentsBuilder path(String path) { + Assert.notNull(path, "path must not be null"); + + String[] pathSegments = StringUtils.tokenizeToStringArray(path, "/"); + return pathSegment(pathSegments); + } + + private String pathAsString() { + if (!pathSegments.isEmpty()) { + StringBuilder pathBuilder = new StringBuilder(); + for (String pathSegment : pathSegments) { + boolean startsWithSlash = pathSegment.charAt(0) == PATH_DELIMITER; + boolean endsWithSlash = + pathBuilder.length() > 0 && pathBuilder.charAt(pathBuilder.length() - 1) == PATH_DELIMITER; + + if (!endsWithSlash && !startsWithSlash) { + pathBuilder.append('/'); + } + else if (endsWithSlash && startsWithSlash) { + pathSegment = pathSegment.substring(1); + } + pathBuilder.append(pathSegment); + } + return pathBuilder.toString(); + } + else { + return null; + } + } + + + /** + * 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 UriComponentsBuilder + */ + public UriComponentsBuilder pathSegment(String... segments) throws IllegalArgumentException { + Assert.notNull(segments, "'segments' must not be null"); + Collections.addAll(this.pathSegments, segments); + + 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 UriComponentsBuilder + */ + public UriComponentsBuilder queryParam(String name, Object... values) { + Assert.notNull(name, "'name' must not be null"); + + if (ObjectUtils.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 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 + * the fragment of this builder. + * + * @param fragment the URI fragment + * @return this UriComponentsBuilder + */ + public UriComponentsBuilder fragment(String fragment) { + if (fragment != null) { + Assert.hasLength(fragment, "'fragment' must not be empty"); + this.fragment = fragment; + } + else { + this.fragment = null; + } + return this; + } + +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/util/UriTemplate.java b/org.springframework.web/src/main/java/org/springframework/web/util/UriTemplate.java index 6026020b976..832c45dfd78 100644 --- a/org.springframework.web/src/main/java/org/springframework/web/util/UriTemplate.java +++ b/org.springframework.web/src/main/java/org/springframework/web/util/UriTemplate.java @@ -21,10 +21,13 @@ import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.util.Collections; +import java.util.EnumMap; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -49,16 +52,17 @@ public class UriTemplate implements Serializable { /** Replaces template variables in the URI template. */ private static final String DEFAULT_VARIABLE_PATTERN = "(.*)"; - private final List variableNames; private final Pattern matchPattern; private final String uriTemplate; + private final UriComponents uriComponents; + /** - * Construct a new {@link UriTemplate} with the given URI String. + * Construct a new {@code UriTemplate} with the given URI String. * @param uriTemplate the URI template string */ public UriTemplate(String uriTemplate) { @@ -66,8 +70,18 @@ public class UriTemplate implements Serializable { this.uriTemplate = uriTemplate; this.variableNames = parser.getVariableNames(); this.matchPattern = parser.getMatchPattern(); + this.uriComponents = UriComponents.fromUriString(uriTemplate); } + public UriTemplate(Map uriComponents) { + this.uriComponents = UriComponents.fromUriComponentMap(uriComponents); + String uriTemplate = this.uriComponents.toUriString(); + Parser parser = new Parser(uriTemplate); + this.uriTemplate = uriTemplate; + this.variableNames = parser.getVariableNames(); + this.matchPattern = parser.getMatchPattern(); + } + /** * Return the names of the variables in the template, in order. * @return the template variable names @@ -76,6 +90,7 @@ public class UriTemplate implements Serializable { return this.variableNames; } + // expanding /** * Given the Map of variables, expands this template into a URI. The Map keys represent variable names, @@ -95,7 +110,8 @@ public class UriTemplate implements Serializable { * or if it does not contain values for all the variable names */ public URI expand(Map uriVariables) { - return encodeUri(expandAsString(uriVariables)); + UriComponents expandedComponents = expandAsUriComponents(uriVariables, true); + return expandedComponents.toUri(); } /** @@ -116,20 +132,72 @@ public class UriTemplate implements Serializable { * @throws IllegalArgumentException if uriVariables is null; * or if it does not contain values for all the variable names */ - public String expandAsString(Map 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++) { - String name = this.variableNames.get(i); - if (!uriVariables.containsKey(name)) { - throw new IllegalArgumentException("'uriVariables' Map has no value for '" + name + "'"); - } - values[i] = uriVariables.get(name); - } - return expandAsString(values); + public String expandAsString(final Map uriVariables, boolean encode) { + UriComponents expandedComponents = expandAsUriComponents(uriVariables, encode); + return expandedComponents.toUriString(); } - /** + public UriComponents expandAsUriComponents(final Map uriVariables, boolean encode) { + Assert.notNull(uriVariables, "'uriVariables' must not be null"); + Set variablesSet = new HashSet(this.variableNames); + variablesSet.removeAll(uriVariables.keySet()); + Assert.isTrue(variablesSet.isEmpty(), + "'uriVariables' does not contain keys for all variables: " + variablesSet); + + Map expandedComponents = new EnumMap(UriComponents.Type.class); + + for (Map.Entry entry : this.uriComponents.entrySet()) { + UriComponents.Type key = entry.getKey(); + String value = entry.getValue(); + String expandedValue = expandUriComponent(key, value, uriVariables); + expandedComponents.put(key, expandedValue); + } + UriComponents result = UriComponents.fromUriComponentMap(expandedComponents); + if (encode) { + result = result.encode(); + } + return result; + } + + private String expandUriComponent(UriComponents.Type componentType, String value, Map uriVariables) { + if (value == null) { + return null; + } + if (value.indexOf('{') == -1) { + return value; + } + Matcher matcher = NAMES_PATTERN.matcher(value); + StringBuffer sb = new StringBuffer(); + while (matcher.find()) { + String match = matcher.group(1); + String variableName = getVariableName(match); + Object variableValue = uriVariables.get(variableName); + String uriVariableValueString = getVariableValueAsString(variableValue); + String replacement = Matcher.quoteReplacement(uriVariableValueString); + matcher.appendReplacement(sb, replacement); + } + matcher.appendTail(sb); + return sb.toString(); + } + + private String getVariableName(String match) { + int colonIdx = match.indexOf(':'); + return colonIdx == -1 ? match : match.substring(0, colonIdx); + } + + /** + * Template method that returns the string representation of the given URI template value. + * + *

Defaults implementation simply calls {@link Object#toString()}, or returns an empty string for {@code null}. + * + * @param variableValue the URI template variable value + * @return the variable value as string + */ + protected String getVariableValueAsString(Object variableValue) { + return variableValue != null ? variableValue.toString() : ""; + } + + /** * Given an array of variables, expand this template into a full URI. The array represent variable values. * The order of variables is significant. *

Example: @@ -144,7 +212,8 @@ public class UriTemplate implements Serializable { * or if it does not contain sufficient variables */ public URI expand(Object... uriVariableValues) { - return encodeUri(expandAsString(uriVariableValues)); + UriComponents expandedComponents = expandAsUriComponents(uriVariableValues, true); + return expandedComponents.toUri(); } /** @@ -157,42 +226,37 @@ public class UriTemplate implements Serializable { * * will print:

http://example.com/hotels/1/bookings/42
* - * @param uriVariableValues the array of URI variables - * @return the expanded URI + * + * @param uriVariableValues the array of URI variables + * @return the expanded URI * @throws IllegalArgumentException if uriVariables is null * or if it does not contain sufficient variables */ - public String expandAsString(Object... uriVariableValues) { - Assert.notNull(uriVariableValues, "'uriVariableValues' must not be null"); - if (uriVariableValues.length < this.variableNames.size()) { - throw new IllegalArgumentException( - "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 uriBuffer = new StringBuffer(); - int i = 0; - while (matcher.find()) { - Object uriVariableValue = uriVariableValues[i++]; - String uriVariableValueString = getVariableValueAsString(uriVariableValue); - String replacement = Matcher.quoteReplacement(uriVariableValueString); - matcher.appendReplacement(uriBuffer, replacement); - } - matcher.appendTail(uriBuffer); - return uriBuffer.toString(); + public String expandAsString(boolean encode, Object[] uriVariableValues) { + UriComponents expandedComponents = expandAsUriComponents(uriVariableValues, encode); + return expandedComponents.toUriString(); } - /** - * Template method that returns the string representation of the given URI template value. - * - *

Defaults implementation simply calls {@link Object#toString()}, or returns an empty string for {@code null}. - * - * @param variableValue the URI template variable value - * @return the variable value as string - */ - protected String getVariableValueAsString(Object variableValue) { - return variableValue != null ? variableValue.toString() : ""; - } + public UriComponents expandAsUriComponents(Object[] uriVariableValues, boolean encode) { + Assert.notNull(uriVariableValues, "'uriVariableValues' must not be null"); + if (uriVariableValues.length < this.variableNames.size()) { + throw new IllegalArgumentException( + "Not enough of variables values in [" + this.uriTemplate + "]: expected at least " + + this.variableNames.size() + "; got " + uriVariableValues.length); + } + Map uriVariables = new LinkedHashMap(this.variableNames.size()); + + for (int i = 0, size = variableNames.size(); i < size; i++) { + String variableName = variableNames.get(i); + Object variableValue = uriVariableValues[i]; + uriVariables.put(variableName, variableValue); + } + + return expandAsUriComponents(uriVariables, encode); + } + + + // matching /** * Indicate whether the given URI matches this template. @@ -316,4 +380,5 @@ public class UriTemplate implements Serializable { } } + } 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 5f4bb72d6dd..05e45e9b13a 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 @@ -18,12 +18,6 @@ package org.springframework.web.util; import java.io.ByteArrayOutputStream; import java.io.UnsupportedEncodingException; -import java.util.Collections; -import java.util.EnumMap; -import java.util.Map; -import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import org.springframework.util.Assert; @@ -44,181 +38,6 @@ import org.springframework.util.Assert; */ public abstract class UriUtils { - private static final String DEFAULT_ENCODING = "UTF-8"; - - private static final String SCHEME_PATTERN = "([^:/?#]+):"; - - private static final String HTTP_PATTERN = "(http|https):"; - - private static final String USERINFO_PATTERN = "([^@/]*)"; - - private static final String HOST_PATTERN = "([^/?#:]*)"; - - private static final String PORT_PATTERN = "(\\d*)"; - - private static final String PATH_PATTERN = "([^?#]*)"; - - private static final String QUERY_PATTERN = "([^#]*)"; - - private static final String LAST_PATTERN = "(.*)"; - - // Regex patterns that matches URIs. See RFC 3986, appendix B - private static final Pattern URI_PATTERN = Pattern.compile( - "^(" + SCHEME_PATTERN + ")?" + "(//(" + USERINFO_PATTERN + "@)?" + HOST_PATTERN + "(:" + PORT_PATTERN + - ")?" + ")?" + PATH_PATTERN + "(\\?" + QUERY_PATTERN + ")?" + "(#" + LAST_PATTERN + ")?"); - - private static final Pattern HTTP_URL_PATTERN = Pattern.compile( - "^" + HTTP_PATTERN + "(//(" + USERINFO_PATTERN + "@)?" + HOST_PATTERN + "(:" + PORT_PATTERN + ")?" + ")?" + - PATH_PATTERN + "(\\?" + LAST_PATTERN + ")?"); - - - // Parsing - - /** - * Parses the given source URI into a mapping of URI components to string values. - * - * @param uri the source URI - * @return the URI components of the URI - */ - public static UriComponents parseUriComponents(String uri) { - Assert.notNull(uri, "'uri' must not be null"); - Matcher m = URI_PATTERN.matcher(uri); - if (m.matches()) { - Map result = new EnumMap(UriComponents.Type.class); - - result.put(UriComponents.Type.SCHEME, m.group(2)); - result.put(UriComponents.Type.AUTHORITY, m.group(3)); - result.put(UriComponents.Type.USER_INFO, m.group(5)); - result.put(UriComponents.Type.HOST, m.group(6)); - result.put(UriComponents.Type.PORT, m.group(8)); - result.put(UriComponents.Type.PATH, m.group(9)); - result.put(UriComponents.Type.QUERY, m.group(11)); - result.put(UriComponents.Type.FRAGMENT, m.group(13)); - - return new UriComponents(result); - } - else { - throw new IllegalArgumentException("[" + uri + "] is not a valid URI"); - } - } - - /** - * Parses the given source HTTP URL into a mapping of URI components to string values. - - *

Note that the returned map will contain a mapping for - * {@link org.springframework.web.util.UriComponents.Type#FRAGMENT}, as fragments are not supposed to be sent to the - * server, but retained by the client. - * - * @param httpUrl the source URI - * @return the URI components of the URI - */ - public static UriComponents parseHttpUrlComponents(String httpUrl) { - Assert.notNull(httpUrl, "'httpUrl' must not be null"); - Matcher m = HTTP_URL_PATTERN.matcher(httpUrl); - if (m.matches()) { - Map result = new EnumMap(UriComponents.Type.class); - - result.put(UriComponents.Type.SCHEME, m.group(1)); - result.put(UriComponents.Type.AUTHORITY, m.group(2)); - result.put(UriComponents.Type.USER_INFO, m.group(4)); - result.put(UriComponents.Type.HOST, m.group(5)); - result.put(UriComponents.Type.PORT, m.group(7)); - result.put(UriComponents.Type.PATH, m.group(8)); - result.put(UriComponents.Type.QUERY, m.group(10)); - - return new UriComponents(result); - } - else { - throw new IllegalArgumentException("[" + httpUrl + "] is not a valid HTTP URL"); - } - } - - // building - - /** - * Builds a URI from the given URI components. The given map should contain at least one entry. - * - *

Note that {@link org.springframework.web.util.UriComponents.Type#PATH_SEGMENT} and {@link org.springframework.web.util.UriComponents.Type#QUERY_PARAM} keys (if any) - * will not be used to build the URI, in favor of {@link org.springframework.web.util.UriComponents.Type#PATH} and {@link org.springframework.web.util.UriComponents.Type#QUERY} - * respectively. - * - * @param uriComponents the components to build the URI out of - * @return the URI created from the given components - */ - public static String buildUri(Map uriComponents) { - Assert.notEmpty(uriComponents, "'uriComponents' must not be empty"); - - return buildUri(uriComponents.get(UriComponents.Type.SCHEME), uriComponents.get( - UriComponents.Type.AUTHORITY), - uriComponents.get(UriComponents.Type.USER_INFO), uriComponents.get(UriComponents.Type.HOST), - uriComponents.get(UriComponents.Type.PORT), uriComponents.get(UriComponents.Type.PATH), - uriComponents.get(UriComponents.Type.QUERY), uriComponents.get(UriComponents.Type.FRAGMENT)); - } - - /** - * Builds a URI from the given URI component parameters. All parameters can be {@code null}. - * - * @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 URI created from the given components - */ - public static String buildUri(String scheme, - String authority, - String userinfo, - String host, - String port, - String path, - String query, - String fragment) { - StringBuilder uriBuilder = new StringBuilder(); - - if (scheme != null) { - uriBuilder.append(scheme); - uriBuilder.append(':'); - } - - if (userinfo != null || host != null || port != null) { - uriBuilder.append("//"); - if (userinfo != null) { - uriBuilder.append(userinfo); - uriBuilder.append('@'); - } - if (host != null) { - uriBuilder.append(host); - } - if (port != null) { - uriBuilder.append(':'); - uriBuilder.append(port); - } - } - else if (authority != null) { - uriBuilder.append("//"); - uriBuilder.append(authority); - } - - if (path != null) { - uriBuilder.append(path); - } - - if (query != null) { - uriBuilder.append('?'); - uriBuilder.append(query); - } - - if (fragment != null) { - uriBuilder.append('#'); - uriBuilder.append(fragment); - } - - return uriBuilder.toString(); - } - // encoding /** @@ -232,9 +51,10 @@ public abstract class UriUtils { * @throws UnsupportedEncodingException when the given encoding parameter is not supported */ public static String encodeUri(String uri, String encoding) throws UnsupportedEncodingException { - Map uriComponents = parseUriComponents(uri); - return encodeUriComponents(uriComponents, encoding); - } + UriComponents uriComponents = UriComponents.fromUriString(uri); + UriComponents encoded = uriComponents.encode(encoding); + return encoded.toUriString(); + } /** * Encodes the given HTTP URI into an encoded String. All various URI components are encoded according to their @@ -248,33 +68,9 @@ public abstract class UriUtils { * @throws UnsupportedEncodingException when the given encoding parameter is not supported */ public static String encodeHttpUrl(String httpUrl, String encoding) throws UnsupportedEncodingException { - Map uriComponents = parseHttpUrlComponents(httpUrl); - return encodeUriComponents(uriComponents, encoding); - } - - /** - * 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 uriComponents the URI components - * @param encoding the character encoding to encode to - * @return the encoded URI - * @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(Map uriComponents, - String encoding) throws UnsupportedEncodingException { - Assert.notEmpty(uriComponents, "'uriComponents' must not be empty"); - Assert.hasLength(encoding, "'encoding' must not be empty"); - - Map encodedUriComponents = new EnumMap(UriComponents.Type.class); - for (Map.Entry entry : uriComponents.entrySet()) { - if (entry.getValue() != null) { - String encodedValue = encodeUriComponent(entry.getValue(), encoding, entry.getKey(), null); - encodedUriComponents.put(entry.getKey(), encodedValue); - } - } - return buildUri(encodedUriComponents); + UriComponents uriComponents = UriComponents.fromHttpUrl(httpUrl); + UriComponents encoded = uriComponents.encode(encoding); + return encoded.toUriString(); } /** @@ -303,141 +99,10 @@ public abstract class UriUtils { String query, String fragment, String encoding) throws UnsupportedEncodingException { - Assert.hasLength(encoding, "'encoding' must not be empty"); + UriComponents uriComponents = UriComponents.fromUriComponents(scheme, authority, userInfo, host, port, path, query, fragment); + UriComponents encoded = uriComponents.encode(encoding); - if (scheme != null) { - scheme = encodeScheme(scheme, encoding); - } - if (authority != null) { - authority = encodeAuthority(authority, encoding); - } - if (userInfo != null) { - userInfo = encodeUserInfo(userInfo, encoding); - } - if (host != null) { - host = encodeHost(host, encoding); - } - if (port != null) { - port = encodePort(port, encoding); - } - if (path != null) { - path = encodePath(path, encoding); - } - if (query != null) { - query = encodeQuery(query, encoding); - } - if (fragment != null) { - fragment = encodeFragment(fragment, encoding); - } - return buildUri(scheme, authority, userInfo, host, port, path, query, fragment); - } - - /** - * Encodes the given source into an encoded String using the rules specified by the given component. - * - * @param source the source string - * @param uriComponent the URI component for the source - * @return the encoded URI - * @throws IllegalArgumentException when the given uri parameter is not a valid URI - */ - public static String encodeUriComponent(String source, UriComponents.Type 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, - UriComponents.Type uriComponent, - Set encodingOptions) { - try { - return encodeUriComponent(source, DEFAULT_ENCODING, uriComponent, encodingOptions); - } - catch (UnsupportedEncodingException ex) { - throw new InternalError("\"" + DEFAULT_ENCODING + "\" not supported"); - } - } - - - /** - * 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 - * @return the encoded URI - * @throws IllegalArgumentException when the given uri parameter is not a valid URI - */ - public static String encodeUriComponent(String source, - String encoding, - UriComponents.Type 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, - UriComponents.Type uriComponent, - Set encodingOptions) throws UnsupportedEncodingException { - Assert.hasLength(encoding, "'encoding' must not be empty"); - - byte[] bytes = encodeInternal(source.getBytes(encoding), uriComponent, encodingOptions); - return new String(bytes, "US-ASCII"); - } - - private static byte[] encodeInternal(byte[] source, - UriComponents.Type uriComponent, - Set encodingOptions) { - Assert.notNull(source, "'source' must not be null"); - Assert.notNull(uriComponent, "'uriComponent' must not be null"); - - if (encodingOptions == null) { - encodingOptions = Collections.emptySet(); - } - - ByteArrayOutputStream bos = new ByteArrayOutputStream(source.length); - for (int i = 0; i < source.length; i++) { - int b = source[i]; - if (b < 0) { - b += 256; - } - if (uriComponent.isAllowed(b)) { - bos.write(b); - } - else if (encodingOptions.contains(EncodingOption.ALLOW_TEMPLATE_VARS) && (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); - } - } - return bos.toByteArray(); + return encoded.toUriString(); } // encoding convenience methods @@ -451,7 +116,7 @@ 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 encodeUriComponent(scheme, encoding, UriComponents.Type.SCHEME, null); + return UriComponents.encodeUriComponent(scheme, encoding, UriComponents.Type.SCHEME); } /** @@ -463,7 +128,7 @@ 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 encodeUriComponent(authority, encoding, UriComponents.Type.AUTHORITY, null); + return UriComponents.encodeUriComponent(authority, encoding, UriComponents.Type.AUTHORITY); } /** @@ -475,7 +140,7 @@ 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 encodeUriComponent(userInfo, encoding, UriComponents.Type.USER_INFO, null); + return UriComponents.encodeUriComponent(userInfo, encoding, UriComponents.Type.USER_INFO); } /** @@ -487,7 +152,7 @@ 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 encodeUriComponent(host, encoding, UriComponents.Type.HOST, null); + return UriComponents.encodeUriComponent(host, encoding, UriComponents.Type.HOST); } /** @@ -499,7 +164,7 @@ 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 encodeUriComponent(port, encoding, UriComponents.Type.PORT, null); + return UriComponents.encodeUriComponent(port, encoding, UriComponents.Type.PORT); } /** @@ -511,7 +176,7 @@ 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 encodeUriComponent(path, encoding, UriComponents.Type.PATH, null); + return UriComponents.encodeUriComponent(path, encoding, UriComponents.Type.PATH); } /** @@ -523,7 +188,7 @@ 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 encodeUriComponent(segment, encoding, UriComponents.Type.PATH_SEGMENT, null); + return UriComponents.encodeUriComponent(segment, encoding, UriComponents.Type.PATH_SEGMENT); } /** @@ -535,7 +200,7 @@ 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 encodeUriComponent(query, encoding, UriComponents.Type.QUERY, null); + return UriComponents.encodeUriComponent(query, encoding, UriComponents.Type.QUERY); } /** @@ -547,7 +212,7 @@ 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 encodeUriComponent(queryParam, encoding, UriComponents.Type.QUERY_PARAM, null); + return UriComponents.encodeUriComponent(queryParam, encoding, UriComponents.Type.QUERY_PARAM); } /** @@ -559,7 +224,7 @@ 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 encodeUriComponent(fragment, encoding, UriComponents.Type.FRAGMENT, null); + return UriComponents.encodeUriComponent(fragment, encoding, UriComponents.Type.FRAGMENT); } @@ -609,16 +274,4 @@ public abstract class UriUtils { 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 - - } - } diff --git a/org.springframework.web/src/test/java/org/springframework/web/util/UriBuilderTests.java b/org.springframework.web/src/test/java/org/springframework/web/util/UriBuilderTests.java deleted file mode 100644 index 15e31d7fa0b..00000000000 --- a/org.springframework.web/src/test/java/org/springframework/web/util/UriBuilderTests.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * 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.HashMap; -import java.util.Map; - -import org.junit.Ignore; -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 fromPath() throws URISyntaxException { - URI result = UriBuilder.fromPath("foo").queryParam("bar").fragment("baz").build(); - - URI expected = new URI("/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 - @Ignore("Working on it") - public void templateVarsVarArgs() throws URISyntaxException { - URI result = UriBuilder.fromPath("/{foo}/{bar}").build("baz", "qux"); - - URI expected = new URI("http://example.com/baz/qux"); - 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 vars = new HashMap(2); - vars.put("bar", "qux"); - vars.put("foo", "baz"); - URI result = UriBuilder.fromPath("/{foo}/{bar}").build(vars); - URI expected = new URI("/baz/qux"); - 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); - } - - -} 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 new file mode 100644 index 00000000000..75efa2bb417 --- /dev/null +++ b/org.springframework.web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java @@ -0,0 +1,110 @@ +/* + * 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.net.URISyntaxException; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** @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()); + + URI expected = new URI("http://example.com/foo?bar#baz"); + assertEquals("Invalid result URI", expected, result.toUri()); + } + + @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()); + + URI expected = new URI("/foo?bar#baz"); + assertEquals("Invalid result URI", expected, result.toUri()); + + result = UriComponentsBuilder.fromPath("/foo").build(); + assertEquals("/foo", result.getPath()); + + expected = new URI("/foo"); + assertEquals("Invalid result URI", expected, result.toUri()); + } + + @Test + public void fromUri() 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()); + + assertEquals("Invalid result URI", uri, result.toUri()); + } + + @Test + public void pathSegments() throws URISyntaxException { + UriComponentsBuilder builder = UriComponentsBuilder.newInstance(); + URI result = builder.pathSegment("foo").pathSegment("bar").build().toUri(); + + URI expected = new URI("/foo/bar"); + assertEquals("Invalid result URI", expected, result); + } + + @Test + public void queryParam() throws URISyntaxException { + UriComponentsBuilder builder = UriComponentsBuilder.newInstance(); + URI result = builder.queryParam("baz", "qux", 42).build().toUri(); + + URI expected = new URI("?baz=qux&baz=42"); + assertEquals("Invalid result URI", expected, result); + } + + @Test + public void emptyQueryParam() throws URISyntaxException { + UriComponentsBuilder builder = UriComponentsBuilder.newInstance(); + URI result = builder.queryParam("baz").build().toUri(); + + URI expected = new URI("?baz"); + assertEquals("Invalid result URI", expected, result); + } + + @Test + public void combineWithUriTemplate() throws URISyntaxException { + UriComponentsBuilder builder = UriComponentsBuilder.fromPath("/{foo}"); + UriComponents components = builder.build(); + UriTemplate template = new UriTemplate(components); + URI uri = template.expand("bar baz"); + assertEquals(new URI("/bar%20baz"), uri); + } + + +} diff --git a/org.springframework.web/src/test/java/org/springframework/web/util/UriComponentsTests.java b/org.springframework.web/src/test/java/org/springframework/web/util/UriComponentsTests.java index df8f51b8072..93fe3fa35ae 100644 --- a/org.springframework.web/src/test/java/org/springframework/web/util/UriComponentsTests.java +++ b/org.springframework.web/src/test/java/org/springframework/web/util/UriComponentsTests.java @@ -16,58 +16,105 @@ package org.springframework.web.util; +import java.net.URI; +import java.net.URISyntaxException; import java.util.Arrays; +import java.util.Collections; import java.util.List; - -import org.junit.Before; -import org.junit.Test; +import java.util.Map; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; -import static org.junit.Assert.*; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; /** @author Arjen Poutsma */ public class UriComponentsTests { - private UriComponents components; + @Test + public void fromUri() { + Map result = UriComponents.fromUriString("http://www.ietf.org/rfc/rfc3986.txt"); + assertEquals("http", result.get(UriComponents.Type.SCHEME)); + assertNull(result.get(UriComponents.Type.USER_INFO)); + assertEquals("www.ietf.org", result.get(UriComponents.Type.HOST)); + assertNull(result.get(UriComponents.Type.PORT)); + assertEquals("/rfc/rfc3986.txt", result.get(UriComponents.Type.PATH)); + assertNull(result.get(UriComponents.Type.QUERY)); + assertNull(result.get(UriComponents.Type.FRAGMENT)); + + result = UriComponents.fromUriString( + "http://arjen:foobar@java.sun.com:80/javase/6/docs/api/java/util/BitSet.html?foo=bar#and(java.util.BitSet)"); + assertEquals("http", result.get(UriComponents.Type.SCHEME)); + assertEquals("arjen:foobar", result.get(UriComponents.Type.USER_INFO)); + assertEquals("java.sun.com", result.get(UriComponents.Type.HOST)); + assertEquals("80", result.get(UriComponents.Type.PORT)); + assertEquals("/javase/6/docs/api/java/util/BitSet.html", result.get(UriComponents.Type.PATH)); + assertEquals("foo=bar", result.get(UriComponents.Type.QUERY)); + assertEquals("and(java.util.BitSet)", result.get(UriComponents.Type.FRAGMENT)); + + result = UriComponents.fromUriString("mailto:java-net@java.sun.com"); + assertEquals("mailto", result.get(UriComponents.Type.SCHEME)); + assertNull(result.get(UriComponents.Type.USER_INFO)); + assertNull(result.get(UriComponents.Type.HOST)); + assertNull(result.get(UriComponents.Type.PORT)); + assertEquals("java-net@java.sun.com", result.get(UriComponents.Type.PATH)); + assertNull(result.get(UriComponents.Type.QUERY)); + assertNull(result.get(UriComponents.Type.FRAGMENT)); + + result = UriComponents.fromUriString("docs/guide/collections/designfaq.html#28"); + assertNull(result.get(UriComponents.Type.SCHEME)); + assertNull(result.get(UriComponents.Type.USER_INFO)); + assertNull(result.get(UriComponents.Type.HOST)); + assertNull(result.get(UriComponents.Type.PORT)); + assertEquals("docs/guide/collections/designfaq.html", result.get(UriComponents.Type.PATH)); + assertNull(result.get(UriComponents.Type.QUERY)); + assertEquals("28", result.get(UriComponents.Type.FRAGMENT)); + } - @Before - public void createComponents() { - components = new UriComponents(); - } - @Test public void pathSegments() { - String path = "/foo/bar"; - components.setPath(path); + String path = "/foo/bar"; + UriComponents components = UriComponents.fromUriComponentMap(Collections.singletonMap(UriComponents.Type.PATH, path)); List expected = Arrays.asList("foo", "bar"); List pathSegments = components.getPathSegments(); assertEquals(expected, pathSegments); - - components.setPath(null); - - components.setPathSegments(expected); - assertEquals(path, components.getPath()); } @Test public void queryParams() { String query = "foo=bar&foo=baz&qux"; - components.setQuery(query); + UriComponents components = UriComponents.fromUriComponentMap( + Collections.singletonMap(UriComponents.Type.QUERY, query)); MultiValueMap expected = new LinkedMultiValueMap(1); expected.put("foo", Arrays.asList("bar", "baz")); expected.set("qux", null); MultiValueMap result = components.getQueryParams(); assertEquals(expected, result); - - components.setQuery(null); - - components.setQueryParams(expected); - assertEquals(query, components.getQuery()); - } + + @Test + public void encode() { + UriComponents uriComponents = UriComponents.fromUriString("http://example.com/hotel list"); + UriComponents encoded = uriComponents.encode(); + assertEquals("/hotel%20list", encoded.getPath()); + } + + @Test + public void toUriEncoded() throws URISyntaxException { + UriComponents uriComponents = UriComponents.fromUriString("http://example.com/hotel list/Z\u00fcrich"); + UriComponents encoded = uriComponents.encode(); + assertEquals(new URI("http://example.com/hotel%20list/Z%C3%BCrich"), encoded.toUri()); + } + + @Test + public void toUriNotEncoded() throws URISyntaxException { + UriComponents uriComponents = UriComponents.fromUriString("http://example.com/hotel list/Z\u00fcrich"); + assertEquals(new URI("http://example.com/hotel%20list/Z\u00fcrich"), uriComponents.toUri()); + } } diff --git a/org.springframework.web/src/test/java/org/springframework/web/util/UriTemplateTests.java b/org.springframework.web/src/test/java/org/springframework/web/util/UriTemplateTests.java index 7323cdcf39b..d16ba69bed4 100644 --- a/org.springframework.web/src/test/java/org/springframework/web/util/UriTemplateTests.java +++ b/org.springframework.web/src/test/java/org/springframework/web/util/UriTemplateTests.java @@ -53,6 +53,16 @@ public class UriTemplateTests { template.expand("1"); } + @Test + public void expandMap() throws Exception { + Map uriVariables = new HashMap(2); + uriVariables.put("booking", "42"); + uriVariables.put("hotel", "1"); + UriTemplate template = new UriTemplate("http://example.com/hotels/{hotel}/bookings/{booking}"); + URI result = template.expand(uriVariables); + assertEquals("Invalid expanded template", new URI("http://example.com/hotels/1/bookings/42"), result); + } + @Test public void expandMapDuplicateVariables() throws Exception { UriTemplate template = new UriTemplate("/order/{c}/{c}/{c}"); @@ -61,16 +71,6 @@ public class UriTemplateTests { assertEquals("Invalid expanded template", new URI("/order/cheeseburger/cheeseburger/cheeseburger"), result); } - @Test - public void expandMap() throws Exception { - Map uriVariables = new HashMap(2); - uriVariables.put("booking", "42"); - uriVariables.put("hotel", "1"); - UriTemplate template = new UriTemplate("http://example.com/hotels/{hotel}/bookings/{booking}"); - URI result = template.expand(uriVariables); - assertEquals("Invalid expanded template", new URI("http://example.com/hotels/1/bookings/42"), result); - } - @Test public void expandMapNonString() throws Exception { Map uriVariables = new HashMap(2); @@ -80,6 +80,15 @@ public class UriTemplateTests { URI result = template.expand(uriVariables); assertEquals("Invalid expanded template", new URI("http://example.com/hotels/1/bookings/42"), result); } + + @Test + public void expandMapEncoded() throws Exception { + Map uriVariables = Collections.singletonMap("hotel", "Z\u00fcrich"); + UriTemplate template = new UriTemplate("http://example.com/hotel list/{hotel}"); + URI result = template.expand(uriVariables); + assertEquals("Invalid expanded template", new URI("http://example.com/hotel%20list/Z%C3%BCrich"), result); + } + @Test(expected = IllegalArgumentException.class) public void expandMapInvalidAmountVariables() throws Exception { diff --git a/org.springframework.web/src/test/java/org/springframework/web/util/UriUtilsTests.java b/org.springframework.web/src/test/java/org/springframework/web/util/UriUtilsTests.java index 27f5e15a83d..53df99702ac 100644 --- a/org.springframework.web/src/test/java/org/springframework/web/util/UriUtilsTests.java +++ b/org.springframework.web/src/test/java/org/springframework/web/util/UriUtilsTests.java @@ -17,11 +17,10 @@ package org.springframework.web.util; import java.io.UnsupportedEncodingException; -import java.util.Map; import org.junit.Test; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; /** * @author Arjen Poutsma @@ -31,45 +30,6 @@ public class UriUtilsTests { private static final String ENC = "UTF-8"; - @Test - public void parseUriComponents() { - Map result = UriUtils.parseUriComponents("http://www.ietf.org/rfc/rfc3986.txt"); - assertEquals("http", result.get(UriComponents.Type.SCHEME)); - assertNull(result.get(UriComponents.Type.USER_INFO)); - assertEquals("www.ietf.org", result.get(UriComponents.Type.HOST)); - assertNull(result.get(UriComponents.Type.PORT)); - assertEquals("/rfc/rfc3986.txt", result.get(UriComponents.Type.PATH)); - assertNull(result.get(UriComponents.Type.QUERY)); - assertNull(result.get(UriComponents.Type.FRAGMENT)); - - result = UriUtils.parseUriComponents( - "http://arjen:foobar@java.sun.com:80/javase/6/docs/api/java/util/BitSet.html?foo=bar#and(java.util.BitSet)"); - assertEquals("http", result.get(UriComponents.Type.SCHEME)); - assertEquals("arjen:foobar", result.get(UriComponents.Type.USER_INFO)); - assertEquals("java.sun.com", result.get(UriComponents.Type.HOST)); - assertEquals("80", result.get(UriComponents.Type.PORT)); - assertEquals("/javase/6/docs/api/java/util/BitSet.html", result.get(UriComponents.Type.PATH)); - assertEquals("foo=bar", result.get(UriComponents.Type.QUERY)); - assertEquals("and(java.util.BitSet)", result.get(UriComponents.Type.FRAGMENT)); - - result = UriUtils.parseUriComponents("mailto:java-net@java.sun.com"); - assertEquals("mailto", result.get(UriComponents.Type.SCHEME)); - assertNull(result.get(UriComponents.Type.USER_INFO)); - assertNull(result.get(UriComponents.Type.HOST)); - assertNull(result.get(UriComponents.Type.PORT)); - assertEquals("java-net@java.sun.com", result.get(UriComponents.Type.PATH)); - assertNull(result.get(UriComponents.Type.QUERY)); - assertNull(result.get(UriComponents.Type.FRAGMENT)); - - result = UriUtils.parseUriComponents("docs/guide/collections/designfaq.html#28"); - assertNull(result.get(UriComponents.Type.SCHEME)); - assertNull(result.get(UriComponents.Type.USER_INFO)); - assertNull(result.get(UriComponents.Type.HOST)); - assertNull(result.get(UriComponents.Type.PORT)); - assertEquals("docs/guide/collections/designfaq.html", result.get(UriComponents.Type.PATH)); - assertNull(result.get(UriComponents.Type.QUERY)); - assertEquals("28", result.get(UriComponents.Type.FRAGMENT)); - } @Test public void encodeScheme() throws UnsupportedEncodingException {