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 index ec620c8ed6..f2e0b7364d 100644 --- 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 @@ -52,6 +52,7 @@ import org.springframework.util.StringUtils; * values are encoded when a URI is built. * Template parameter regular expressions are ignored when building a URI, i.e. * no validation is performed. + * *

Inspired by {@link javax.ws.rs.core.UriBuilder}. * * @author Arjen Poutsma @@ -132,7 +133,8 @@ public class UriBuilder { StringBuilder uriBuilder = new StringBuilder(); if (scheme != null) { - uriBuilder.append(scheme).append(':'); + uriBuilder.append(scheme); + uriBuilder.append(':'); } if (userInfo != null || host != null || port != -1) { @@ -218,7 +220,7 @@ public class UriBuilder { UriTemplate template; if (scheme != null) { - template = new UriTemplate(scheme, UriUtils.SCHEME_COMPONENT); + template = new UriTemplate(scheme, UriComponent.SCHEME); uriBuilder.append(template.expandAsString(encodeUriVariableValues, uriVariables)); uriBuilder.append(':'); } @@ -227,13 +229,13 @@ public class UriBuilder { uriBuilder.append("//"); if (StringUtils.hasLength(userInfo)) { - template = new UriTemplate(userInfo, UriUtils.USER_INFO_COMPONENT); + template = new UriTemplate(userInfo, UriComponent.USER_INFO); uriBuilder.append(template.expandAsString(encodeUriVariableValues, uriVariables)); uriBuilder.append('@'); } if (host != null) { - template = new UriTemplate(host, UriUtils.HOST_COMPONENT); + template = new UriTemplate(host, UriComponent.HOST); uriBuilder.append(template.expandAsString(encodeUriVariableValues, uriVariables)); } @@ -254,19 +256,19 @@ public class UriBuilder { else if (endsWithSlash && startsWithSlash) { pathSegment = pathSegment.substring(1); } - template = new UriTemplate(pathSegment, UriUtils.PATH_SEGMENT_COMPONENT); + template = new UriTemplate(pathSegment, UriComponent.PATH_SEGMENT); uriBuilder.append(template.expandAsString(encodeUriVariableValues, uriVariables)); } } if (queryBuilder.length() > 0) { uriBuilder.append('?'); - template = new UriTemplate(queryBuilder.toString(), UriUtils.QUERY_COMPONENT); + template = new UriTemplate(queryBuilder.toString(), UriComponent.QUERY); uriBuilder.append(template.expandAsString(encodeUriVariableValues, uriVariables)); } if (StringUtils.hasLength(fragment)) { uriBuilder.append('#'); - template = new UriTemplate(fragment, UriUtils.FRAGMENT_COMPONENT); + template = new UriTemplate(fragment, UriComponent.FRAGMENT); uriBuilder.append(template.expandAsString(encodeUriVariableValues, uriVariables)); } @@ -306,7 +308,7 @@ public class UriBuilder { UriTemplate template; if (scheme != null) { - template = new UriTemplate(scheme, UriUtils.SCHEME_COMPONENT); + template = new UriTemplate(scheme, UriComponent.SCHEME); uriBuilder.append(template.expandAsString(encodeUriVariableValues, uriVariableValues)); uriBuilder.append(':'); } @@ -315,13 +317,13 @@ public class UriBuilder { uriBuilder.append("//"); if (StringUtils.hasLength(userInfo)) { - template = new UriTemplate(userInfo, UriUtils.USER_INFO_COMPONENT); + template = new UriTemplate(userInfo, UriComponent.USER_INFO); uriBuilder.append(template.expandAsString(encodeUriVariableValues, uriVariableValues)); uriBuilder.append('@'); } if (host != null) { - template = new UriTemplate(host, UriUtils.HOST_COMPONENT); + template = new UriTemplate(host, UriComponent.HOST); uriBuilder.append(template.expandAsString(encodeUriVariableValues, uriVariableValues)); } @@ -342,20 +344,20 @@ public class UriBuilder { else if (endsWithSlash && startsWithSlash) { pathSegment = pathSegment.substring(1); } - template = new UriTemplate(pathSegment, UriUtils.PATH_SEGMENT_COMPONENT); + template = new UriTemplate(pathSegment, UriComponent.PATH_SEGMENT); uriBuilder.append(template.expandAsString(encodeUriVariableValues, uriVariableValues)); } } if (queryBuilder.length() > 0) { uriBuilder.append('?'); - template = new UriTemplate(queryBuilder.toString(), UriUtils.QUERY_COMPONENT); + template = new UriTemplate(queryBuilder.toString(), UriComponent.QUERY); uriBuilder.append(template.expandAsString(encodeUriVariableValues, uriVariableValues)); } if (StringUtils.hasLength(fragment)) { uriBuilder.append('#'); - template = new UriTemplate(fragment, UriUtils.FRAGMENT_COMPONENT); + template = new UriTemplate(fragment, UriComponent.FRAGMENT); uriBuilder.append(template.expandAsString(encodeUriVariableValues, uriVariableValues)); } @@ -411,7 +413,7 @@ public class UriBuilder { public UriBuilder scheme(String scheme) { if (scheme != null) { Assert.hasLength(scheme, "'scheme' must not be empty"); - this.scheme = UriUtils.encode(scheme, UriUtils.SCHEME_COMPONENT, true); + this.scheme = UriUtils.encode(scheme, UriComponent.SCHEME, true); } else { this.scheme = null; @@ -429,7 +431,7 @@ public class UriBuilder { public UriBuilder userInfo(String userInfo) { if (userInfo != null) { Assert.hasLength(userInfo, "'userInfo' must not be empty"); - this.userInfo = UriUtils.encode(userInfo, UriUtils.USER_INFO_COMPONENT, true); + this.userInfo = UriUtils.encode(userInfo, UriComponent.USER_INFO, true); } else { this.userInfo = null; @@ -447,7 +449,7 @@ public class UriBuilder { public UriBuilder host(String host) { if (host != null) { Assert.hasLength(host, "'host' must not be empty"); - this.host = UriUtils.encode(host, UriUtils.HOST_COMPONENT, true); + this.host = UriUtils.encode(host, UriComponent.HOST, true); } else { this.host = null; @@ -490,7 +492,7 @@ public class UriBuilder { public UriBuilder pathSegment(String... segments) throws IllegalArgumentException { Assert.notNull(segments, "'segments' must not be null"); for (String segment : segments) { - this.pathSegments.add(UriUtils.encode(segment, UriUtils.PATH_SEGMENT_COMPONENT, true)); + this.pathSegments.add(UriUtils.encode(segment, UriComponent.PATH_SEGMENT, true)); } return this; @@ -508,7 +510,7 @@ public class UriBuilder { public UriBuilder queryParam(String name, Object... values) { Assert.notNull(name, "'name' must not be null"); - String encodedName = UriUtils.encode(name, UriUtils.QUERY_PARAM_COMPONENT, true); + String encodedName = UriUtils.encode(name, UriComponent.QUERY_PARAM, true); if (ObjectUtils.isEmpty(values)) { if (queryBuilder.length() != 0) { @@ -526,7 +528,7 @@ public class UriBuilder { String valueAsString = value != null ? value.toString() : ""; if (valueAsString.length() != 0) { queryBuilder.append('='); - queryBuilder.append(UriUtils.encode(valueAsString, UriUtils.QUERY_PARAM_COMPONENT, true)); + queryBuilder.append(UriUtils.encode(valueAsString, UriComponent.QUERY_PARAM, true)); } } @@ -544,7 +546,7 @@ public class UriBuilder { public UriBuilder fragment(String fragment) { if (fragment != null) { Assert.hasLength(fragment, "'fragment' must not be empty"); - this.fragment = UriUtils.encode(fragment, UriUtils.FRAGMENT_COMPONENT, true); + this.fragment = UriUtils.encode(fragment, UriComponent.FRAGMENT, true); } else { this.fragment = null; diff --git a/org.springframework.web/src/main/java/org/springframework/web/util/UriComponent.java b/org.springframework.web/src/main/java/org/springframework/web/util/UriComponent.java new file mode 100644 index 0000000000..c54a12ce5e --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/util/UriComponent.java @@ -0,0 +1,168 @@ +/* + * 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; + +/** + * 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 + * @since 3.1 + */ +public enum UriComponent { + + 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/UriTemplate.java b/org.springframework.web/src/main/java/org/springframework/web/util/UriTemplate.java index 513d2e31db..51ebb22460 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 @@ -56,7 +56,7 @@ public class UriTemplate implements Serializable { private final String uriTemplate; - private final UriUtils.UriComponent uriComponent; + private final UriComponent uriComponent; /** @@ -75,7 +75,7 @@ public class UriTemplate implements Serializable { * Construct a new {@link UriTemplate} with the given URI String. * @param uriTemplate the URI template string */ - public UriTemplate(String uriTemplate, UriUtils.UriComponent uriComponent) { + public UriTemplate(String uriTemplate, UriComponent uriComponent) { Parser parser = new Parser(uriTemplate); this.uriTemplate = uriTemplate; this.variableNames = parser.getVariableNames(); @@ -110,7 +110,7 @@ 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(true, uriVariables)); + return encodeUri(expandAsString(false, uriVariables)); } /** @@ -159,7 +159,7 @@ public class UriTemplate implements Serializable { * or if it does not contain sufficient variables */ public URI expand(Object... uriVariableValues) { - return encodeUri(expandAsString(true, uriVariableValues)); + return encodeUri(expandAsString(false, uriVariableValues)); } /** 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 b48f9c516d..cb9e4fe4c1 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 @@ -239,7 +239,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 encode(scheme, encoding, SCHEME_COMPONENT, false); + return encode(scheme, encoding, UriComponent.SCHEME, false); } /** @@ -250,7 +250,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 encode(authority, encoding, AUTHORITY_COMPONENT, false); + return encode(authority, encoding, UriComponent.AUTHORITY, false); } /** @@ -261,7 +261,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 encode(userInfo, encoding, USER_INFO_COMPONENT, false); + return encode(userInfo, encoding, UriComponent.USER_INFO, false); } /** @@ -272,7 +272,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 encode(host, encoding, HOST_COMPONENT, false); + return encode(host, encoding, UriComponent.HOST, false); } /** @@ -283,7 +283,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 encode(port, encoding, PORT_COMPONENT, false); + return encode(port, encoding, UriComponent.PORT, false); } /** @@ -294,7 +294,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 encode(path, encoding, PATH_COMPONENT, false); + return encode(path, encoding, UriComponent.PATH, false); } /** @@ -305,7 +305,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 encode(segment, encoding, PATH_SEGMENT_COMPONENT, false); + return encode(segment, encoding, UriComponent.PATH_SEGMENT, false); } /** @@ -316,7 +316,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 encode(query, encoding, QUERY_COMPONENT, false); + return encode(query, encoding, UriComponent.QUERY, false); } /** @@ -327,7 +327,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 encode(queryParam, encoding, QUERY_PARAM_COMPONENT, false); + return encode(queryParam, encoding, UriComponent.QUERY_PARAM, false); } /** @@ -338,7 +338,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 encode(fragment, encoding, FRAGMENT_COMPONENT, false); + return encode(fragment, encoding, UriComponent.FRAGMENT, false); } /** @@ -478,126 +478,4 @@ public abstract class UriUtils { return changed ? new String(bos.toByteArray(), encoding) : source; } - /** - * Defines the contract for an URI component, i.e. scheme, host, path, etc. - */ - public interface UriComponent { - - /** - * Specifies whether the given character is allowed in this URI component. - * @param c the character - * @return {@code true} if the character is allowed; {@code false} otherwise - */ - boolean isAllowed(int c); - - } - - private static abstract class AbstractUriComponent implements UriComponent { - - protected boolean isAlpha(int c) { - return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z'; - } - - protected boolean isDigit(int c) { - return c >= '0' && c <= '9'; - } - - protected boolean isGenericDelimiter(int c) { - return ':' == c || '/' == c || '?' == c || '#' == c || '[' == c || ']' == c || '@' == c; - } - - protected boolean isSubDelimiter(int c) { - return '!' == c || '$' == c || '&' == c || '\'' == c || '(' == c || ')' == c || '*' == c || '+' == c || - ',' == c || ';' == c || '=' == c; - } - - protected boolean isReserved(char c) { - return isGenericDelimiter(c) || isReserved(c); - } - - protected boolean isUnreserved(int c) { - return isAlpha(c) || isDigit(c) || '-' == c || '.' == c || '_' == c || '~' == c; - } - - protected boolean isPchar(int c) { - return isUnreserved(c) || isSubDelimiter(c) || ':' == c || '@' == c; - } - - } - - /** The scheme URI component. */ - public static final UriComponent SCHEME_COMPONENT = new AbstractUriComponent() { - public boolean isAllowed(int c) { - return isAlpha(c) || isDigit(c) || '+' == c || '-' == c || '.' == c; - } - }; - - /** The authority URI component. */ - public static final UriComponent AUTHORITY_COMPONENT = new AbstractUriComponent() { - public boolean isAllowed(int c) { - return isUnreserved(c) || isSubDelimiter(c) || ':' == c || '@' == c; - } - }; - - /** The user info URI component. */ - public static final UriComponent USER_INFO_COMPONENT = new AbstractUriComponent() { - public boolean isAllowed(int c) { - return isUnreserved(c) || isSubDelimiter(c) || ':' == c; - } - }; - - /** The host URI component. */ - public static final UriComponent HOST_COMPONENT = new AbstractUriComponent() { - public boolean isAllowed(int c) { - return isUnreserved(c) || isSubDelimiter(c); - } - }; - - /** The port URI component. */ - public static final UriComponent PORT_COMPONENT = new AbstractUriComponent() { - public boolean isAllowed(int c) { - return isDigit(c); - } - }; - - /** The path URI component. */ - public static final UriComponent PATH_COMPONENT = new AbstractUriComponent() { - public boolean isAllowed(int c) { - return isPchar(c) || '/' == c; - } - }; - - /** The path segment URI component. */ - public static final UriComponent PATH_SEGMENT_COMPONENT = new AbstractUriComponent() { - public boolean isAllowed(int c) { - return isPchar(c); - } - }; - - /** The query URI component. */ - public static final UriComponent QUERY_COMPONENT = new AbstractUriComponent() { - public boolean isAllowed(int c) { - return isPchar(c) || '/' == c || '?' == c; - } - }; - - /** The query parameter URI component. */ - public static final UriComponent QUERY_PARAM_COMPONENT = new AbstractUriComponent() { - public boolean isAllowed(int c) { - if ('=' == c || '+' == c || '&' == c) { - return false; - } - else { - return isPchar(c) || '/' == c || '?' == c; - } - } - }; - - /** The fragment URI component. */ - public static final UriComponent FRAGMENT_COMPONENT = new AbstractUriComponent() { - public boolean isAllowed(int c) { - return isPchar(c) || '/' == c || '?' == c; - } - }; - } 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 index 65c83db1bd..ee30c36063 100644 --- 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 @@ -38,6 +38,14 @@ public class UriBuilderTests { 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");