diff --git a/org.springframework.web/src/main/java/org/springframework/web/client/RestTemplate.java b/org.springframework.web/src/main/java/org/springframework/web/client/RestTemplate.java index 626772f6e82..12673043d03 100644 --- a/org.springframework.web/src/main/java/org/springframework/web/client/RestTemplate.java +++ b/org.springframework.web/src/main/java/org/springframework/web/client/RestTemplate.java @@ -17,9 +17,10 @@ package org.springframework.web.client; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.net.URI; +import java.net.URISyntaxException; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; @@ -36,11 +37,12 @@ import org.springframework.http.converter.FormHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter; -import org.springframework.http.converter.xml.SourceHttpMessageConverter; import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter; +import org.springframework.http.converter.xml.SourceHttpMessageConverter; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.web.util.UriTemplate; +import org.springframework.web.util.UriUtils; /** * The central class for client-side HTTP access. It simplifies communication with HTTP servers, and @@ -302,7 +304,7 @@ public class RestTemplate extends HttpAccessor implements RestOperations { public T execute(String url, HttpMethod method, RequestCallback requestCallback, ResponseExtractor responseExtractor, Object... urlVariables) throws RestClientException { - UriTemplate uriTemplate = new UriTemplate(url); + UriTemplate uriTemplate = new HttpUrlTemplate(url); URI expanded = uriTemplate.expand(urlVariables); return doExecute(expanded, method, requestCallback, responseExtractor); } @@ -310,7 +312,7 @@ public class RestTemplate extends HttpAccessor implements RestOperations { public T execute(String url, HttpMethod method, RequestCallback requestCallback, ResponseExtractor responseExtractor, Map urlVariables) throws RestClientException { - UriTemplate uriTemplate = new UriTemplate(url); + UriTemplate uriTemplate = new HttpUrlTemplate(url); URI expanded = uriTemplate.expand(urlVariables); return doExecute(expanded, method, requestCallback, responseExtractor); } @@ -489,4 +491,29 @@ public class RestTemplate extends HttpAccessor implements RestOperations { } } + /** + * HTTP-specific subclass of UriTemplate, overriding the encode method. + */ + private static class HttpUrlTemplate extends UriTemplate { + + public HttpUrlTemplate(String uriTemplate) { + super(uriTemplate); + } + + @Override + protected URI encodeUri(String uri) { + try { + String encoded = UriUtils.encodeHttpUrl(uri, "UTF-8"); + return new URI(encoded); + } + catch (UnsupportedEncodingException ex) { + // should not happen, UTF-8 is always supported + throw new IllegalStateException(ex); + } + catch (URISyntaxException ex) { + throw new IllegalArgumentException("Could not create HTTP URL from [" + uri + "]: " + ex, ex); + } + } + } + } 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 baabb6ba7e6..b997a657c5d 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 @@ -70,7 +70,7 @@ public class UriTemplate { * Return the names of the variables in the template, in order. * @return the template variable names */ - public final List getVariableNames() { + public List getVariableNames() { return this.variableNames; } @@ -172,7 +172,15 @@ public class UriTemplate { return this.uriTemplate; } - private static URI encodeUri(String uri) { + /** + * Encodes the given String as URL. + * + *

Defaults to {@link UriUtils#encodeUri(String, String)}. + * + * @param uri the URI to encode + * @return the encoded URI + */ + protected URI encodeUri(String uri) { try { String encoded = UriUtils.encodeUri(uri, "UTF-8"); return new URI(encoded); @@ -186,7 +194,6 @@ public class UriTemplate { } } - /** * Static inner class to parse uri template strings into a matching regular expression. */ 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 10a22270ff8..4b023395e3d 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 @@ -36,13 +36,13 @@ import org.springframework.util.Assert; * * * @author Arjen Poutsma - * @since 3.0 * @see RFC 3986 + * @since 3.0 */ public abstract class UriUtils { private static final BitSet SCHEME; - + private static final BitSet USER_INFO; private static final BitSet HOST; @@ -61,6 +61,8 @@ public abstract class UriUtils { 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 = "([^/?#:]*)"; @@ -71,15 +73,16 @@ public abstract class UriUtils { private static final String QUERY_PATTERN = "([^#]*)"; - private static final String FRAGMENT_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 + ")?" + - "(#" + FRAGMENT_PATTERN + ")?"); + 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 + ")?"); static { // variable names refer to RFC 3986, appendix A @@ -181,10 +184,10 @@ public abstract class UriUtils { } /** - * Encodes the given source URI into an encoded String. All various URI components are encoded according to - * their respective valid character sets. + * Encodes the given source URI into an encoded String. All various URI components are encoded according to their + * respective valid character sets. * - * @param uri the URI to be encoded + * @param uri the URI to be encoded * @param encoding the character encoding to encode to * @return the encoded URI * @throws IllegalArgumentException when the given uri parameter is not a valid URI @@ -204,50 +207,115 @@ public abstract class UriUtils { String query = m.group(11); String fragment = m.group(13); - StringBuilder sb = new StringBuilder(); - - if (scheme != null) { - sb.append(encodeScheme(scheme, encoding)); - sb.append(':'); - } - - if (authority != null) { - sb.append("//"); - if (userinfo != null) { - sb.append(encodeUserInfo(userinfo, encoding)); - sb.append('@'); - } - if (host != null) { - sb.append(encodeHost(host, encoding)); - } - if (port != null) { - sb.append(':'); - sb.append(encodePort(port, encoding)); - } - } - - sb.append(encodePath(path, encoding)); - - if (query != null) { - sb.append('?'); - sb.append(encodeQuery(query, encoding)); - } - - if (fragment != null) { - sb.append('#'); - sb.append(encodeFragment(fragment, encoding)); - } - - return sb.toString(); - } else { + return encodeUriComponents(scheme, authority, userinfo, host, port, path, query, fragment, encoding); + } + else { throw new IllegalArgumentException("[" + uri + "] is not a valid URI"); } } + /** + * Encodes the given HTTP URI into an encoded String. All various URI components are encoded according to their + * respective valid character sets. + * + *

Note that this method does not support fragments ({@code #}), as these are not supposed to be + * sent to the server, but retained by the client. + * + * @param httpUrl the HTTP URL to be encoded + * @param encoding the character encoding to encode to + * @return the encoded URL + * @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 encodeHttpUrl(String httpUrl, String encoding) throws UnsupportedEncodingException { + Assert.notNull(httpUrl, "'httpUrl' must not be null"); + Assert.hasLength(encoding, "'encoding' must not be empty"); + Matcher m = HTTP_URL_PATTERN.matcher(httpUrl); + if (m.matches()) { + String scheme = m.group(1); + String authority = m.group(2); + String userinfo = m.group(4); + String host = m.group(5); + String portString = m.group(7); + String path = m.group(8); + String query = m.group(10); + + return encodeUriComponents(scheme, authority, userinfo, host, portString, path, query, null, encoding); + } + else { + throw new IllegalArgumentException("[" + httpUrl + "] is not a valid HTTP URL"); + } + } + + /** + * Encodes the given source URI components into an encoded String. All various URI components are optional, but encoded according + * to their respective valid character sets. + * + * @param scheme the scheme + * @param authority the authority + * @param userinfo the user info + * @param host the host + * @param port the port + * @param path the path + * @param query the query + * @param fragment the fragment + * @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(String scheme, + String authority, + String userinfo, + String host, + String port, + String path, + String query, + String fragment, + String encoding) throws UnsupportedEncodingException { + + Assert.hasLength(encoding, "'encoding' must not be empty"); + StringBuilder sb = new StringBuilder(); + + if (scheme != null) { + sb.append(encodeScheme(scheme, encoding)); + sb.append(':'); + } + + if (authority != null) { + sb.append("//"); + if (userinfo != null) { + sb.append(encodeUserInfo(userinfo, encoding)); + sb.append('@'); + } + if (host != null) { + sb.append(encodeHost(host, encoding)); + } + if (port != null) { + sb.append(':'); + sb.append(encodePort(port, encoding)); + } + } + + sb.append(encodePath(path, encoding)); + + if (query != null) { + sb.append('?'); + sb.append(encodeQuery(query, encoding)); + } + + if (fragment != null) { + sb.append('#'); + sb.append(encodeFragment(fragment, encoding)); + } + + return sb.toString(); + } + /** * Encodes the given URI scheme. * - * @param scheme the scheme to be encoded + * @param scheme the scheme to be encoded * @param encoding the character encoding to encode to * @return the encoded scheme * @throws UnsupportedEncodingException when the given encoding parameter is not supported @@ -271,7 +339,7 @@ public abstract class UriUtils { /** * Encodes the given URI host. * - * @param host the host to be encoded + * @param host the host to be encoded * @param encoding the character encoding to encode to * @return the encoded host * @throws UnsupportedEncodingException when the given encoding parameter is not supported @@ -283,7 +351,7 @@ public abstract class UriUtils { /** * Encodes the given URI port. * - * @param port the port to be encoded + * @param port the port to be encoded * @param encoding the character encoding to encode to * @return the encoded port * @throws UnsupportedEncodingException when the given encoding parameter is not supported @@ -295,7 +363,7 @@ public abstract class UriUtils { /** * Encodes the given URI path. * - * @param path the path to be encoded + * @param path the path to be encoded * @param encoding the character encoding to encode to * @return the encoded path * @throws UnsupportedEncodingException when the given encoding parameter is not supported @@ -307,7 +375,7 @@ public abstract class UriUtils { /** * Encodes the given URI path segment. * - * @param segment the segment to be encoded + * @param segment the segment to be encoded * @param encoding the character encoding to encode to * @return the encoded segment * @throws UnsupportedEncodingException when the given encoding parameter is not supported @@ -319,7 +387,7 @@ public abstract class UriUtils { /** * Encodes the given URI query. * - * @param query the query to be encoded + * @param query the query to be encoded * @param encoding the character encoding to encode to * @return the encoded query * @throws UnsupportedEncodingException when the given encoding parameter is not supported @@ -332,7 +400,7 @@ public abstract class UriUtils { * Encodes the given URI query parameter. * * @param queryParam the query parameter to be encoded - * @param encoding the character encoding to encode to + * @param encoding the character encoding to encode to * @return the encoded query parameter * @throws UnsupportedEncodingException when the given encoding parameter is not supported */ @@ -352,7 +420,8 @@ public abstract class UriUtils { return encode(fragment, encoding, FRAGMENT); } - private static String encode(String source, String encoding, BitSet notEncoded) throws UnsupportedEncodingException { + private static String encode(String source, String encoding, BitSet notEncoded) + throws UnsupportedEncodingException { Assert.notNull(source, "'source' must not be null"); Assert.hasLength(encoding, "'encoding' must not be empty"); ByteArrayOutputStream bos = new ByteArrayOutputStream(source.length() * 2); @@ -388,8 +457,9 @@ public abstract class UriUtils { *

  • A sequence "%xy" is interpreted as a hexadecimal * representation of the character. * - * @param source - * @param encoding + * + * @param source the source string + * @param encoding the encoding * @return the decoded URI * @throws UnsupportedEncodingException when the given encoding parameter is not supported * @see java.net.URLDecoder#decode(String, String) diff --git a/org.springframework.web/src/test/java/org/springframework/web/util/UriUtilsTest.java b/org.springframework.web/src/test/java/org/springframework/web/util/UriUtilsTest.java index c383102185d..a3713d1efa2 100644 --- a/org.springframework.web/src/test/java/org/springframework/web/util/UriUtilsTest.java +++ b/org.springframework.web/src/test/java/org/springframework/web/util/UriUtilsTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 the original author or authors. + * Copyright 2002-2010 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. @@ -99,6 +99,8 @@ public class UriUtilsTest { public void encodeUri() throws UnsupportedEncodingException { assertEquals("Invalid encoded URI", "http://www.ietf.org/rfc/rfc3986.txt", UriUtils.encodeUri("http://www.ietf.org/rfc/rfc3986.txt", ENC)); + assertEquals("Invalid encoded URI", "https://www.ietf.org/rfc/rfc3986.txt", + UriUtils.encodeUri("https://www.ietf.org/rfc/rfc3986.txt", ENC)); assertEquals("Invalid encoded URI", "http://www.google.com/?q=z%FCrich", UriUtils.encodeUri("http://www.google.com/?q=z\u00fcrich", ENC)); assertEquals("Invalid encoded URI", @@ -117,8 +119,34 @@ public class UriUtilsTest { assertEquals("Invalid encoded URI", "../../../demo/jfc/SwingSet2/src/SwingSet2.java", UriUtils.encodeUri("../../../demo/jfc/SwingSet2/src/SwingSet2.java", ENC)); assertEquals("Invalid encoded URI", "file:///~/calendar", UriUtils.encodeUri("file:///~/calendar", ENC)); - assertEquals("Invalid encoded URI", "http://example.com/query=foo@bar", UriUtils.encodeUri("http://example.com/query=foo@bar", ENC)); + assertEquals("Invalid encoded URI", "http://example.com/query=foo@bar", + UriUtils.encodeUri("http://example.com/query=foo@bar", ENC)); } + @Test + public void encodeHttpUrl() throws UnsupportedEncodingException { + assertEquals("Invalid encoded HTTP URL", "http://www.ietf.org/rfc/rfc3986.txt", + UriUtils.encodeHttpUrl("http://www.ietf.org/rfc/rfc3986.txt", ENC)); + assertEquals("Invalid encoded URI", "https://www.ietf.org/rfc/rfc3986.txt", + UriUtils.encodeHttpUrl("https://www.ietf.org/rfc/rfc3986.txt", ENC)); + assertEquals("Invalid encoded HTTP URL", "http://www.google.com/?q=z%FCrich", + UriUtils.encodeHttpUrl("http://www.google.com/?q=z\u00fcrich", ENC)); + assertEquals("Invalid encoded HTTP URL", + "http://arjen:foobar@java.sun.com:80/javase/6/docs/api/java/util/BitSet.html?foo=bar", + UriUtils.encodeHttpUrl( + "http://arjen:foobar@java.sun.com:80/javase/6/docs/api/java/util/BitSet.html?foo=bar", ENC)); + assertEquals("Invalid encoded HTTP URL", "http://search.twitter.com/search.atom?q=%23avatar", + UriUtils.encodeHttpUrl("http://search.twitter.com/search.atom?q=#avatar", ENC)); + assertEquals("Invalid encoded HTTP URL", "http://java.sun.com/j2se/1.3/", + UriUtils.encodeHttpUrl("http://java.sun.com/j2se/1.3/", ENC)); + assertEquals("Invalid encoded HTTP URL", "http://example.com/query=foo@bar", + UriUtils.encodeHttpUrl("http://example.com/query=foo@bar", ENC)); + } + + @Test(expected = IllegalArgumentException.class) + public void encodeHttpUrlMail() throws UnsupportedEncodingException { + UriUtils.encodeHttpUrl("mailto:java-net@java.sun.com", ENC); + } + }