SPR-8803 Add UriComponentsBuilder methods to replace path/query.

This commit is contained in:
Rossen Stoyanchev 2011-11-04 16:43:03 +00:00
parent 8889284517
commit d3f4c69f00
6 changed files with 333 additions and 174 deletions

View File

@ -21,6 +21,7 @@ Changes in version 3.1 RC2 (2011-11-15)
* The Form input tag allows type values other than "text" such as HTML5-specific types. * The Form input tag allows type values other than "text" such as HTML5-specific types.
* The Form hidden tag supports "disabled" attribute * The Form hidden tag supports "disabled" attribute
* Add ignoreDefaultModelOnRedirect attribute to <mvc:annotation-driven/> * Add ignoreDefaultModelOnRedirect attribute to <mvc:annotation-driven/>
* Add methods to UriComponentsBuilder for replacing the path or the query.
Changes in version 3.1 RC1 (2011-10-11) Changes in version 3.1 RC1 (2011-10-11)
--------------------------------------- ---------------------------------------

View File

@ -44,6 +44,7 @@ import org.springframework.util.StringUtils;
* </ol> * </ol>
* *
* @author Arjen Poutsma * @author Arjen Poutsma
* @author Rossen Stoyanchev
* @see #newInstance() * @see #newInstance()
* @see #fromPath(String) * @see #fromPath(String)
* @see #fromUri(URI) * @see #fromUri(URI)
@ -78,20 +79,20 @@ public class UriComponentsBuilder {
"^" + HTTP_PATTERN + "(//(" + USERINFO_PATTERN + "@)?" + HOST_PATTERN + "(:" + PORT_PATTERN + ")?" + ")?" + "^" + HTTP_PATTERN + "(//(" + USERINFO_PATTERN + "@)?" + HOST_PATTERN + "(:" + PORT_PATTERN + ")?" + ")?" +
PATH_PATTERN + "(\\?" + LAST_PATTERN + ")?"); PATH_PATTERN + "(\\?" + LAST_PATTERN + ")?");
private String scheme;
private String scheme; private String userInfo;
private String userInfo; private String host;
private String host; private int port = -1;
private int port = -1;
private PathComponentBuilder pathBuilder = NULL_PATH_COMPONENT_BUILDER; private PathComponentBuilder pathBuilder = NULL_PATH_COMPONENT_BUILDER;
private final MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<String, String>(); private final MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<String, String>();
private String fragment; private String fragment;
/** /**
* Default constructor. Protected to prevent direct instantiation. * Default constructor. Protected to prevent direct instantiation.
@ -101,42 +102,42 @@ public class UriComponentsBuilder {
* @see #fromUri(URI) * @see #fromUri(URI)
*/ */
protected UriComponentsBuilder() { protected UriComponentsBuilder() {
} }
// Factory methods // Factory methods
/** /**
* Returns a new, empty builder. * Returns a new, empty builder.
* *
* @return the new {@code UriComponentsBuilder} * @return the new {@code UriComponentsBuilder}
*/ */
public static UriComponentsBuilder newInstance() { public static UriComponentsBuilder newInstance() {
return new UriComponentsBuilder(); return new UriComponentsBuilder();
} }
/** /**
* Returns a builder that is initialized with the given path. * Returns a builder that is initialized with the given path.
* *
* @param path the path to initialize with * @param path the path to initialize with
* @return the new {@code UriComponentsBuilder} * @return the new {@code UriComponentsBuilder}
*/ */
public static UriComponentsBuilder fromPath(String path) { public static UriComponentsBuilder fromPath(String path) {
UriComponentsBuilder builder = new UriComponentsBuilder(); UriComponentsBuilder builder = new UriComponentsBuilder();
builder.path(path); builder.path(path);
return builder; return builder;
} }
/** /**
* Returns a builder that is initialized with the given {@code URI}. * Returns a builder that is initialized with the given {@code URI}.
* *
* @param uri the URI to initialize with * @param uri the URI to initialize with
* @return the new {@code UriComponentsBuilder} * @return the new {@code UriComponentsBuilder}
*/ */
public static UriComponentsBuilder fromUri(URI uri) { public static UriComponentsBuilder fromUri(URI uri) {
UriComponentsBuilder builder = new UriComponentsBuilder(); UriComponentsBuilder builder = new UriComponentsBuilder();
builder.uri(uri); builder.uri(uri);
return builder; return builder;
} }
/** /**
* Returns a builder that is initialized with the given URI string. * Returns a builder that is initialized with the given URI string.
@ -201,132 +202,145 @@ public class UriComponentsBuilder {
// build methods // build methods
/** /**
* Builds a {@code UriComponents} instance from the various components contained in this builder. * Builds a {@code UriComponents} instance from the various components contained in this builder.
* *
* @return the URI components * @return the URI components
*/ */
public UriComponents build() { public UriComponents build() {
return build(false); return build(false);
} }
/** /**
* Builds a {@code UriComponents} instance from the various components contained in this builder. * Builds a {@code UriComponents} instance from the various components contained in this builder.
* *
* @param encoded whether all the components set in this builder are encoded ({@code true}) or not ({@code false}). * @param encoded whether all the components set in this builder are encoded ({@code true}) or not ({@code false}).
* @return the URI components * @return the URI components
*/ */
public UriComponents build(boolean encoded) { public UriComponents build(boolean encoded) {
return new UriComponents(scheme, userInfo, host, port, pathBuilder.build(), queryParams, fragment, encoded, true); return new UriComponents(scheme, userInfo, host, port, pathBuilder.build(), queryParams, fragment, encoded, true);
} }
// URI components methods // URI components methods
/** /**
* Initializes all components of this URI builder with the components of the given URI. * Initializes all components of this URI builder with the components of the given URI.
* *
* @param uri the URI * @param uri the URI
* @return this UriComponentsBuilder * @return this UriComponentsBuilder
*/ */
public UriComponentsBuilder uri(URI uri) { public UriComponentsBuilder uri(URI uri) {
Assert.notNull(uri, "'uri' must not be null"); Assert.notNull(uri, "'uri' must not be null");
Assert.isTrue(!uri.isOpaque(), "Opaque URI [" + uri + "] not supported"); Assert.isTrue(!uri.isOpaque(), "Opaque URI [" + uri + "] not supported");
this.scheme = uri.getScheme(); this.scheme = uri.getScheme();
if (uri.getUserInfo() != null) { if (uri.getUserInfo() != null) {
this.userInfo = uri.getUserInfo(); this.userInfo = uri.getUserInfo();
} }
if (uri.getHost() != null) { if (uri.getHost() != null) {
this.host = uri.getHost(); this.host = uri.getHost();
} }
if (uri.getPort() != -1) { if (uri.getPort() != -1) {
this.port = uri.getPort(); this.port = uri.getPort();
} }
if (StringUtils.hasLength(uri.getPath())) { if (StringUtils.hasLength(uri.getPath())) {
this.pathBuilder = new FullPathComponentBuilder(uri.getPath()); this.pathBuilder = new FullPathComponentBuilder(uri.getPath());
} }
if (StringUtils.hasLength(uri.getQuery())) { if (StringUtils.hasLength(uri.getQuery())) {
this.queryParams.clear(); this.queryParams.clear();
query(uri.getQuery()); query(uri.getQuery());
} }
if (uri.getFragment() != null) { if (uri.getFragment() != null) {
this.fragment = uri.getFragment(); this.fragment = uri.getFragment();
} }
return this; 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;
}
/** /**
* Appends the given path to the existing path of this builder. The given path may contain URI template variables. * 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 path the URI path *
* @return this UriComponentsBuilder * @param scheme the URI scheme
*/ * @return this UriComponentsBuilder
public UriComponentsBuilder path(String path) { */
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;
}
/**
* 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) {
if (path != null) { if (path != null) {
this.pathBuilder = this.pathBuilder.appendPath(path); this.pathBuilder = this.pathBuilder.appendPath(path);
} else { }
else {
this.pathBuilder = NULL_PATH_COMPONENT_BUILDER; this.pathBuilder = NULL_PATH_COMPONENT_BUILDER;
} }
return this; return this;
} }
/** /**
* Appends the given path segments to the existing path of this builder. Each given path segments may contain URI * Sets the path of this builder overriding all existing path and path segment values.
* template variables. *
* * @param path the URI path; a {@code null} value results in an empty path.
* @param pathSegments the URI path segments * @return this UriComponentsBuilder
* @return this UriComponentsBuilder */
*/ public UriComponentsBuilder replacePath(String path) {
public UriComponentsBuilder pathSegment(String... pathSegments) throws IllegalArgumentException { this.pathBuilder = NULL_PATH_COMPONENT_BUILDER;
path(path);
return this;
}
/**
* Appends the given path segments to the existing path of this builder. Each given path segments may contain URI
* template variables.
*
* @param pathSegments the URI path segments
* @return this UriComponentsBuilder
*/
public UriComponentsBuilder pathSegment(String... pathSegments) throws IllegalArgumentException {
Assert.notNull(pathSegments, "'segments' must not be null"); Assert.notNull(pathSegments, "'segments' must not be null");
this.pathBuilder = this.pathBuilder.appendPathSegments(pathSegments); this.pathBuilder = this.pathBuilder.appendPathSegments(pathSegments);
return this; return this;
@ -335,7 +349,7 @@ public class UriComponentsBuilder {
/** /**
* Appends the given query to the existing query of this builder. The given query may contain URI template variables. * Appends the given query to the existing query of this builder. The given query may contain URI template variables.
* *
* @param query the URI path * @param query the query string
* @return this UriComponentsBuilder * @return this UriComponentsBuilder
*/ */
public UriComponentsBuilder query(String query) { public UriComponentsBuilder query(String query) {
@ -348,52 +362,78 @@ public class UriComponentsBuilder {
} }
} }
else { else {
queryParams.clear(); this.queryParams.clear();
} }
return this; return this;
} }
/**
* Sets the query of this builder overriding all existing query parameters.
*
* @param query the query string; a {@code null} value removes all query parameters.
* @return this UriComponentsBuilder
*/
public UriComponentsBuilder replaceQuery(String query) {
this.queryParams.clear();
query(query);
return this;
}
/** /**
* Appends the given query parameter to the existing query parameters. The given name or any of the values may contain * 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. * 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}. * {@code ?foo} instead of {@code ?foo=bar}.
* *
* @param name the query parameter name * @param name the query parameter name
* @param values the query parameter values * @param values the query parameter values
* @return this UriComponentsBuilder * @return this UriComponentsBuilder
*/ */
public UriComponentsBuilder queryParam(String name, Object... values) { public UriComponentsBuilder queryParam(String name, Object... values) {
Assert.notNull(name, "'name' must not be null"); Assert.notNull(name, "'name' must not be null");
if (!ObjectUtils.isEmpty(values)) { if (!ObjectUtils.isEmpty(values)) {
for (Object value : values) { for (Object value : values) {
String valueAsString = value != null ? value.toString() : null; String valueAsString = value != null ? value.toString() : null;
queryParams.add(name, valueAsString); this.queryParams.add(name, valueAsString);
} }
} }
else { else {
queryParams.add(name, null); this.queryParams.add(name, null);
} }
return this; return this;
} }
/** /**
* Sets the URI fragment. The given fragment may contain URI template variables, and may also be {@code null} to clear * Sets the query parameter values overriding all existing query values for the same parameter.
* the fragment of this builder. * If no values are given, the resulting URI will contain the query parameter name only.
* *
* @param fragment the URI fragment * @param name the query parameter name
* @return this UriComponentsBuilder * @param values the query parameter values
*/ * @return this UriComponentsBuilder
public UriComponentsBuilder fragment(String fragment) { */
if (fragment != null) { public UriComponentsBuilder replaceQueryParam(String name, Object... values) {
Assert.hasLength(fragment, "'fragment' must not be empty"); Assert.notNull(name, "'name' must not be null");
this.fragment = fragment; this.queryParams.remove(name);
} queryParam(name, values);
else { return this;
this.fragment = null; }
}
return this; /**
} * Sets the URI fragment. The given fragment may contain URI template variables, and may also be {@code null} to clear
* the fragment of this builder.
*
* @param fragment the URI fragment
* @return this 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;
}
/** /**
* Represents a builder for {@link org.springframework.web.util.UriComponents.PathComponent} * Represents a builder for {@link org.springframework.web.util.UriComponents.PathComponent}
@ -403,7 +443,7 @@ public class UriComponentsBuilder {
UriComponents.PathComponent build(); UriComponents.PathComponent build();
PathComponentBuilder appendPath(String path); PathComponentBuilder appendPath(String path);
PathComponentBuilder appendPathSegments(String... pathSegments); PathComponentBuilder appendPathSegments(String... pathSegments);
} }

View File

@ -174,6 +174,36 @@ public class UriComponentsBuilderTests {
assertEquals(Arrays.asList("foo"), result.getPathSegments()); assertEquals(Arrays.asList("foo"), result.getPathSegments());
} }
@Test
public void replacePath() {
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString("http://www.ietf.org/rfc/rfc2396.txt");
builder.replacePath("/rfc/rfc3986.txt");
UriComponents result = builder.build();
assertEquals("http://www.ietf.org/rfc/rfc3986.txt", result.toUriString());
builder = UriComponentsBuilder.fromUriString("http://www.ietf.org/rfc/rfc2396.txt");
builder.replacePath(null);
result = builder.build();
assertEquals("http://www.ietf.org", result.toUriString());
}
@Test
public void replaceQuery() {
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString("http://example.com/foo?foo=bar&baz=qux");
builder.replaceQuery("baz=42");
UriComponents result = builder.build();
assertEquals("http://example.com/foo?baz=42", result.toUriString());
builder = UriComponentsBuilder.fromUriString("http://example.com/foo?foo=bar&baz=qux");
builder.replaceQuery(null);
result = builder.build();
assertEquals("http://example.com/foo", result.toUriString());
}
@Test @Test
public void queryParams() throws URISyntaxException { public void queryParams() throws URISyntaxException {
UriComponentsBuilder builder = UriComponentsBuilder.newInstance(); UriComponentsBuilder builder = UriComponentsBuilder.newInstance();
@ -197,4 +227,20 @@ public class UriComponentsBuilderTests {
assertEquals(expectedQueryParams, result.getQueryParams()); assertEquals(expectedQueryParams, result.getQueryParams());
} }
@Test
public void replaceQueryParam() {
UriComponentsBuilder builder = UriComponentsBuilder.newInstance().queryParam("baz", "qux", 42);
builder.replaceQueryParam("baz", "xuq", 24);
UriComponents result = builder.build();
assertEquals("baz=xuq&baz=24", result.getQuery());
builder = UriComponentsBuilder.newInstance().queryParam("baz", "qux", 42);
builder.replaceQueryParam("baz");
result = builder.build();
assertEquals("baz", result.getQuery());
}
} }

View File

@ -1792,7 +1792,9 @@ public class EditPetForm {
so no attributes should be passed on to so no attributes should be passed on to
<classname>RedirectView</classname>. Both the MVC namespace and the <classname>RedirectView</classname>. Both the MVC namespace and the
MVC Java config (via <interfacename>@EnableWebMvc</interfacename>) MVC Java config (via <interfacename>@EnableWebMvc</interfacename>)
automatically set this flag to <literal>true</literal>.</para> keep this flag set to <literal>false</literal> in order to maintain
backwards compatibility. However, for new applications we recommend
setting it to <literal>true</literal></para>
<para>The <interfacename>RedirectAttributes</interfacename> interface <para>The <interfacename>RedirectAttributes</interfacename> interface
can also be used to add flash attributes. Unlike other redirect can also be used to add flash attributes. Unlike other redirect

View File

@ -467,5 +467,25 @@
linkend="mvc-multipart-forms-non-browsers" /> and <xref linkend="mvc-multipart-forms-non-browsers" /> and <xref
linkend="mvc-multipart" />.</para> linkend="mvc-multipart" />.</para>
</section> </section>
<section>
<title><classname>UriComponentsBuilder</classname> and <classname>UriComponents</classname></title>
<para>A new <classname>UriComponents</classname> class has been added,
which is an immutable container of URI components providing
access to all contained URI components.
A nenw <classname>UriComponentsBuilder</classname> class is also
provided to help create <classname>UriComponents</classname> instances.
Together the two classes give fine-grained control over all
aspects of preparing a URI including construction, expansion
from URI template variables, and encoding.</para>
<para>In most cases the new classes can be used as a more flexible
alternative to the existing <classname>UriTemplate</classname>
especially since <classname>UriTemplate</classname> relies on those
same classes internally.
</para>
</section>
</section> </section>
</chapter> </chapter>

View File

@ -1434,6 +1434,56 @@ URI location = template.postForLocation(uri, booking, "1");
information on using the execute method and the meaning of its other information on using the execute method and the meaning of its other
method arguments.</para> method arguments.</para>
<section>
<title>Working with the URI</title>
<para>For each of the main HTTP methods, the <classname>RestTemplate</classname>
provides variants that either take a String URI or <classname>java.net.URI</classname>
as the first argument.
</para>
<para>The String URI variants accept template arguments as a
String variable length argument or as a <classname>Map&lt;String,String&gt;</classname>.
They also assume the URL String is not encoded and needs to be encoded.
For example the following:
</para>
<programlisting language="java">restTemplate.getForObject("http://example.com/hotel list", String.class);</programlisting>
<para>will perform a GET on <filename>http://example.com/hotel%20list</filename>.
That means if the input URL String is already encoded, it will be encoded twice --
i.e. <filename>http://example.com/hotel%20list</filename> will become
<filename>http://example.com/hotel%2520list</filename>.
If this is not the intended effect, use the
<classname>java.net.URI</classname> method variant, which assumes
the URL is already encoded is also generally useful if you want
to reuse a single (fully expanded) <classname>URI</classname>
multiple times.</para>
<para>The <classname>UriComponentsBuilder</classname> class can be used
to build and encode the <classname>URI</classname> including support
for URI templates. For example you can start with a URL String:
</para>
<programlisting language="java">UriComponents uriComponents =
UriComponentsBuilder.fromUriString("http://example.com/hotels/{hotel}/bookings/{booking}").build()
.expand("42", "21")
.encode();
URI uri = uriComponents.toUri();</programlisting>
<para>Or specify each URI component indiviudally:</para>
<programlisting language="java">UriComponents uriComponents =
UriComponentsBuilder.newInstance()
.scheme("http").host("example.com").path("/hotels/{hotel}/bookings/{booking}").build()
.expand("42", "21")
.encode();
URI uri = uriComponents.toUri();</programlisting>
</section>
<section> <section>
<title>Dealing with request and response headers</title> <title>Dealing with request and response headers</title>