SPR-6946 - RestTemplate should not encode fragments (#'s)

This commit is contained in:
Arjen Poutsma 2010-03-05 11:40:52 +00:00
parent 54d0346084
commit c91ff130d5
4 changed files with 199 additions and 67 deletions

View File

@ -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;
/**
* <strong>The central class for client-side HTTP access.</strong> It simplifies communication with HTTP servers, and
@ -302,7 +304,7 @@ public class RestTemplate extends HttpAccessor implements RestOperations {
public <T> T execute(String url, HttpMethod method, RequestCallback requestCallback,
ResponseExtractor<T> 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> T execute(String url, HttpMethod method, RequestCallback requestCallback,
ResponseExtractor<T> responseExtractor, Map<String, ?> 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);
}
}
}
}

View File

@ -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<String> getVariableNames() {
public List<String> 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.
*
* <p>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.
*/

View File

@ -36,13 +36,13 @@ import org.springframework.util.Assert;
* </ul>
*
* @author Arjen Poutsma
* @since 3.0
* @see <a href="http://www.ietf.org/rfc/rfc3986.txt">RFC 3986</a>
* @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.
*
* <p><strong>Note</strong> 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 {
* <li>A sequence "<code>%<i>xy</i></code>" is interpreted as a hexadecimal
* representation of the character.
* </ul>
* @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)

View File

@ -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);
}
}