Introduce UriBuilderFactory and UriBuilder

Issue: SPR-15124
This commit is contained in:
Rossen Stoyanchev
2017-01-27 14:07:55 -05:00
parent cb0d992303
commit 7b67b5bc2a
7 changed files with 696 additions and 25 deletions

View File

@@ -0,0 +1,299 @@
/*
* Copyright 2002-2017 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.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ObjectUtils;
/**
* Default implementation of {@link UriBuilderFactory} using
* {@link UriComponentsBuilder} for building, encoding, and expanding URI
* templates.
*
* <p>Exposes configuration properties that customize the creation of all URIs
* built through this factory instance including a base URI, default URI
* variables, and an encoding mode.
*
* @author Rossen Stoyanchev
* @since 5.0
*/
public class DefaultUriBuilderFactory implements UriBuilderFactory {
public enum EncodingMode {URI_COMPONENT, VALUES_ONLY, NONE };
private final UriComponentsBuilder baseUri;
private final Map<String, Object> defaultUriVariables = new HashMap<>();
private EncodingMode encodingMode = EncodingMode.URI_COMPONENT;
/**
* Default constructor without a base URI.
*/
public DefaultUriBuilderFactory() {
this(UriComponentsBuilder.fromPath(null));
}
/**
* Constructor with a String "base URI".
* <p>The String given here is used to create a single "base"
* {@code UriComponentsBuilder}. Each time a new URI is prepared via
* {@link #uriString(String)} a new {@code UriComponentsBuilder} is created and
* merged with a clone of the "base" {@code UriComponentsBuilder}.
* <p>Note that the base URI may contain any or all components of a URI and
* those will apply to every URI.
*/
public DefaultUriBuilderFactory(String baseUri) {
this(UriComponentsBuilder.fromUriString(baseUri));
}
/**
* Alternate constructor with a {@code UriComponentsBuilder} as the base URI.
*/
public DefaultUriBuilderFactory(UriComponentsBuilder baseUri) {
Assert.notNull(baseUri, "'baseUri' is required.");
this.baseUri = baseUri;
}
/**
* Configure default URI variable values to use when expanding a URI with a
* Map of values. The map supplied when expanding a given URI can override
* default values.
* @param defaultUriVariables the default URI variables
*/
public void setDefaultUriVariables(Map<String, ?> defaultUriVariables) {
this.defaultUriVariables.clear();
if (defaultUriVariables != null) {
this.defaultUriVariables.putAll(defaultUriVariables);
}
}
/**
* Return the configured default URI variable values.
*/
public Map<String, ?> getDefaultUriVariables() {
return Collections.unmodifiableMap(this.defaultUriVariables);
}
/**
* Specify the encoding mode to use when building URIs:
* <ul>
* <li>URI_COMPONENT -- expand the URI variables first and then encode all URI
* component (e.g. host, path, query, etc) according to the encoding rules
* for each individual component.
* <li>VALUES_ONLY -- encode URI variable values only, prior to expanding
* them, using a "strict" encoding mode, i.e. encoding all characters
* outside the unreserved set as defined in
* <a href="https://tools.ietf.org/html/rfc3986#section-2">RFC 3986 Section 2</a>.
* This ensures a URI variable value will not contain any characters with a
* reserved purpose.
* <li>NONE -- in this mode no encoding is performed.
* </ul>
* <p>By default this is set to {@code "URI_COMPONENT"}.
* @param encodingMode the encoding mode to use
*/
public void setEncodingMode(EncodingMode encodingMode) {
this.encodingMode = encodingMode;
}
/**
* Return the configured encoding mode.
*/
public EncodingMode getEncodingMode() {
return this.encodingMode;
}
// UriTemplateHandler
public URI expand(String uriTemplate, Map<String, ?> uriVars) {
return uriString(uriTemplate).build(uriVars);
}
public URI expand(String uriTemplate, Object... uriVars) {
return uriString(uriTemplate).build(uriVars);
}
// UriBuilderFactory
public UriBuilder uriString(String uriTemplate) {
return new DefaultUriBuilder(uriTemplate);
}
/**
* {@link DefaultUriBuilderFactory} specific implementation of UriBuilder.
*/
private class DefaultUriBuilder implements UriBuilder {
private final UriComponentsBuilder uriComponentsBuilder;
public DefaultUriBuilder(String uriTemplate) {
this.uriComponentsBuilder = initUriComponentsBuilder(uriTemplate);
}
private UriComponentsBuilder initUriComponentsBuilder(String uriTemplate) {
// Merge base URI with child URI template
UriComponentsBuilder result = baseUri.cloneBuilder();
UriComponents child = UriComponentsBuilder.fromUriString(uriTemplate).build();
result.uriComponents(child);
// Split path into path segments
List<String> pathList = result.build().getPathSegments();
String[] pathArray = pathList.toArray(new String[pathList.size()]);
result.replacePath(null);
result.pathSegment(pathArray);
return result;
}
@Override
public DefaultUriBuilder scheme(String scheme) {
this.uriComponentsBuilder.scheme(scheme);
return this;
}
@Override
public DefaultUriBuilder userInfo(String userInfo) {
this.uriComponentsBuilder.userInfo(userInfo);
return this;
}
@Override
public DefaultUriBuilder host(String host) {
this.uriComponentsBuilder.host(host);
return this;
}
@Override
public DefaultUriBuilder port(int port) {
this.uriComponentsBuilder.port(port);
return this;
}
@Override
public DefaultUriBuilder port(String port) {
this.uriComponentsBuilder.port(port);
return this;
}
@Override
public DefaultUriBuilder path(String path) {
this.uriComponentsBuilder.path(path);
return this;
}
@Override
public DefaultUriBuilder replacePath(String path) {
this.uriComponentsBuilder.replacePath(path);
return this;
}
@Override
public DefaultUriBuilder pathSegment(String... pathSegments) {
this.uriComponentsBuilder.pathSegment(pathSegments);
return this;
}
@Override
public DefaultUriBuilder query(String query) {
this.uriComponentsBuilder.query(query);
return this;
}
@Override
public DefaultUriBuilder replaceQuery(String query) {
this.uriComponentsBuilder.replaceQuery(query);
return this;
}
@Override
public DefaultUriBuilder queryParam(String name, Object... values) {
this.uriComponentsBuilder.queryParam(name, values);
return this;
}
@Override
public DefaultUriBuilder replaceQueryParam(String name, Object... values) {
this.uriComponentsBuilder.replaceQueryParam(name, values);
return this;
}
@Override
public DefaultUriBuilder queryParams(MultiValueMap<String, String> params) {
this.uriComponentsBuilder.queryParams(params);
return this;
}
@Override
public DefaultUriBuilder replaceQueryParams(MultiValueMap<String, String> params) {
this.uriComponentsBuilder.replaceQueryParams(params);
return this;
}
@Override
public DefaultUriBuilder fragment(String fragment) {
this.uriComponentsBuilder.fragment(fragment);
return this;
}
@Override
public URI build(Map<String, ?> uriVars) {
if (!defaultUriVariables.isEmpty()) {
Map<String, Object> map = new HashMap<>();
map.putAll(defaultUriVariables);
map.putAll(uriVars);
uriVars = map;
}
if (encodingMode.equals(EncodingMode.VALUES_ONLY)) {
uriVars = UriUtils.encodeUriVariables(uriVars);
}
UriComponents uriComponents = this.uriComponentsBuilder.build().expand(uriVars);
if (encodingMode.equals(EncodingMode.URI_COMPONENT)) {
uriComponents = uriComponents.encode();
}
return URI.create(uriComponents.toString());
}
@Override
public URI build(Object... uriVars) {
if (ObjectUtils.isEmpty(uriVars) && !defaultUriVariables.isEmpty()) {
return build(Collections.emptyMap());
}
if (encodingMode.equals(EncodingMode.VALUES_ONLY)) {
uriVars = UriUtils.encodeUriVariables(uriVars);
}
UriComponents uriComponents = this.uriComponentsBuilder.build().expand(uriVars);
if (encodingMode.equals(EncodingMode.URI_COMPONENT)) {
uriComponents = uriComponents.encode();
}
return URI.create(uriComponents.toString());
}
}
}

View File

@@ -446,14 +446,27 @@ final class HierarchicalUriComponents extends UriComponents {
@Override
protected void copyToUriComponentsBuilder(UriComponentsBuilder builder) {
builder.scheme(getScheme());
builder.userInfo(getUserInfo());
builder.host(getHost());
builder.port(getPort());
builder.replacePath("");
this.path.copyToUriComponentsBuilder(builder);
builder.replaceQueryParams(getQueryParams());
builder.fragment(getFragment());
if (getScheme() != null) {
builder.scheme(getScheme());
}
if (getUserInfo() != null) {
builder.userInfo(getUserInfo());
}
if (getHost() != null) {
builder.host(getHost());
}
if (getPort() != -1) {
builder.port(getPort());
}
if (getPath() != null) {
this.path.copyToUriComponentsBuilder(builder);
}
if (!getQueryParams().isEmpty()) {
builder.queryParams(getQueryParams());
}
if (getFragment() != null) {
builder.fragment(getFragment());
}
}

View File

@@ -137,9 +137,15 @@ final class OpaqueUriComponents extends UriComponents {
@Override
protected void copyToUriComponentsBuilder(UriComponentsBuilder builder) {
builder.scheme(getScheme());
builder.schemeSpecificPart(getSchemeSpecificPart());
builder.fragment(getFragment());
if (getScheme() != null) {
builder.scheme(getScheme());
}
if (getSchemeSpecificPart() != null) {
builder.schemeSpecificPart(getSchemeSpecificPart());
}
if (getFragment() != null) {
builder.fragment(getFragment());
}
}

View File

@@ -0,0 +1,171 @@
/*
* Copyright 2002-2017 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,
* WITHOUUriBuilder 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.Map;
import org.springframework.util.MultiValueMap;
/**
* Builder-style methods to prepare and expand a URI template with variables.
*
* <p>Effectively a generalization of {@link UriComponentsBuilder} but with
* shortcuts to expand directly into {@link URI} rather than
* {@link UriComponents} and also leaving common concerns such as encoding
* preferences, a base URI, and others as implementation concerns.
*
* <p>Typically obtained via {@link UriBuilderFactory} which serves as a central
* component configured once and used to create many URLs.
*
* @author Rossen Stoyanchev
* @since 5.0
* @see UriBuilderFactory
* @see UriComponentsBuilder
*/
public interface UriBuilder {
/**
* Set the URI scheme which may contain URI template variables,
* and may also be {@code null} to clear the scheme of this builder.
* @param scheme the URI scheme
*/
UriBuilder scheme(String scheme);
/**
* Set the URI user info which 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
*/
UriBuilder userInfo(String userInfo);
/**
* Set the URI host which may contain URI template variables, and may also
* be {@code null} to clear the host of this builder.
* @param host the URI host
*/
UriBuilder host(String host);
/**
* Set the URI port. Passing {@code -1} will clear the port of this builder.
* @param port the URI port
*/
UriBuilder port(int port);
/**
* Set the URI port . Use this method only when the port needs to be
* parameterized with a URI variable. Otherwise use {@link #port(int)}.
* Passing {@code null} will clear the port of this builder.
* @param port the URI port
*/
UriBuilder port(String port);
/**
* Append the given path to the existing path of this builder.
* The given path may contain URI template variables.
* @param path the URI path
*/
UriBuilder path(String path);
/**
* Set the path of this builder overriding the existing path values.
* @param path the URI path or {@code null} for an empty path.
*/
UriBuilder replacePath(String path);
/**
* Append path segments to the existing path. Each path segment may contain
* URI template variables and should not contain any slashes.
* Use {@code path("/")} subsequently to ensure a trailing slash.
* @param pathSegments the URI path segments
*/
UriBuilder pathSegment(String... pathSegments) throws IllegalArgumentException;
/**
* Append the given query to the existing query of this builder.
* The given query may contain URI template variables.
* <p><strong>Note:</strong> The presence of reserved characters can prevent
* correct parsing of the URI string. For example if a query parameter
* contains {@code '='} or {@code '&'} characters, the query string cannot
* be parsed unambiguously. Such values should be substituted for URI
* variables to enable correct parsing:
* <pre class="code">
* builder.query(&quot;filter={value}&quot;).uriString(&quot;hot&amp;cold&quot;);
* </pre>
* @param query the query string
*/
UriBuilder query(String query);
/**
* Set the query of this builder overriding all existing query parameters.
* @param query the query string or {@code null} to remove all query params
*/
UriBuilder replaceQuery(String query);
/**
* Append 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
*/
UriBuilder queryParam(String name, Object... values);
/**
* Add the given query parameters.
* @param params the params
*/
UriBuilder queryParams(MultiValueMap<String, String> params);
/**
* Set the query parameter values overriding all existing query values for
* the same parameter. If no values are given, the query parameter is removed.
* @param name the query parameter name
* @param values the query parameter values
*/
UriBuilder replaceQueryParam(String name, Object... values);
/**
* Set the query parameter values overriding all existing query values.
* @param params the query parameter name
*/
UriBuilder replaceQueryParams(MultiValueMap<String, String> params);
/**
* Set 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
*/
UriBuilder fragment(String fragment);
/**
* Build a {@link URI} instance and replaces URI template variables
* with the values from an array.
* @param uriVariables the map of URI variables
* @return the URI
*/
URI build(Object... uriVariables);
/**
* Build a {@link URI} instance and replaces URI template variables
* with the values from a map.
* @param uriVariables the map of URI variables
* @return the URI
*/
URI build(Map<String, ?> uriVariables);
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2002-2017 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;
/**
* Factory for instances of {@link UriBuilder}.
*
* <p>A single {@link UriBuilderFactory} may be created once, configured with
* common properties such as a base URI, and then used to create many URIs.
*
* <p>Extends {@link UriTemplateHandler} which has a similar purpose but only
* provides shortcuts for expanding URI templates, not builder style methods.
*
* @author Rossen Stoyanchev
* @since 5.0
*/
public interface UriBuilderFactory extends UriTemplateHandler {
/**
* Return a builder that is initialized with the given URI string which may
* be a URI template and represent full URI or just a path.
* <p>Depending on the factory implementation and configuration, the builder
* may merge the given URI string with a base URI and apply other operations.
* Refer to the specific factory implementation for details.
* @param uriTemplate the URI template to create the builder with
* @return the UriBuilder
*/
UriBuilder uriString(String uriTemplate);
}

View File

@@ -57,7 +57,7 @@ import org.springframework.web.util.HierarchicalUriComponents.PathComponent;
* @see #fromPath(String)
* @see #fromUri(URI)
*/
public class UriComponentsBuilder implements Cloneable {
public class UriComponentsBuilder implements UriBuilder, Cloneable {
private static final Pattern QUERY_PARAM_PATTERN = Pattern.compile("([^&=]+)(=?)([^&]+)?");
@@ -360,6 +360,30 @@ public class UriComponentsBuilder implements Cloneable {
return build(false).expand(uriVariableValues);
}
/**
* Build a {@link URI} instance and replaces URI template variables
* with the values from an array.
* @param uriVariables the map of URI variables
* @return the URI
*/
@Override
public URI build(Object... uriVariables) {
return buildAndExpand(uriVariables).encode().toUri();
}
/**
* Build a {@link URI} instance and replaces URI template variables
* with the values from a map.
* @param uriVariables the map of URI variables
* @return the URI
*/
@Override
public URI build(Map<String, ?> uriVariables) {
return buildAndExpand(uriVariables).encode().toUri();
}
/**
* Build a URI String. This is a shortcut method which combines calls
* to {@link #build()}, then {@link UriComponents#encode()} and finally
@@ -372,10 +396,10 @@ public class UriComponentsBuilder implements Cloneable {
}
// URI components methods
// Instance methods
/**
* Initialize all components of this URI builder with the components of the given URI.
* Initialize components of this builder from components of the given URI.
* @param uri the URI
* @return this UriComponentsBuilder
*/
@@ -411,6 +435,18 @@ public class UriComponentsBuilder implements Cloneable {
return this;
}
/**
* Initialize components of this {@link UriComponentsBuilder} from the
* components of the given {@link UriComponents}.
* @param uriComponents the UriComponents instance
* @return this UriComponentsBuilder
*/
public UriComponentsBuilder uriComponents(UriComponents uriComponents) {
Assert.notNull(uriComponents, "UriComponents must not be null");
uriComponents.copyToUriComponentsBuilder(this);
return this;
}
/**
* Set the URI scheme. The given scheme may contain URI template variables,
* and may also be {@code null} to clear the scheme of this builder.
@@ -422,17 +458,6 @@ public class UriComponentsBuilder implements Cloneable {
return this;
}
/**
* Set all components of this URI builder from the given {@link UriComponents}.
* @param uriComponents the UriComponents instance
* @return this UriComponentsBuilder
*/
public UriComponentsBuilder uriComponents(UriComponents uriComponents) {
Assert.notNull(uriComponents, "UriComponents must not be null");
uriComponents.copyToUriComponentsBuilder(this);
return this;
}
/**
* Set the URI scheme-specific-part. When invoked, this method overwrites
* {@linkplain #userInfo(String) user-info}, {@linkplain #host(String) host},