org.springframework.web.util.DefaultUriBuilderFactory Maven / Gradle / Ivy
/*
* Copyright 2002-2020 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
*
* https://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.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import org.springframework.lang.Nullable;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* {@code UriBuilderFactory} that relies on {@link UriComponentsBuilder} for
* the actual building of the URI.
*
* Provides options to create {@link UriBuilder} instances with a common
* base URI, alternative encoding mode strategies, among others.
*
* @author Rossen Stoyanchev
* @since 5.0
* @see UriComponentsBuilder
*/
public class DefaultUriBuilderFactory implements UriBuilderFactory {
@Nullable
private final UriComponentsBuilder baseUri;
private EncodingMode encodingMode = EncodingMode.TEMPLATE_AND_VALUES;
private final Map defaultUriVariables = new HashMap<>();
private boolean parsePath = true;
/**
* Default constructor without a base URI.
* The target address must be specified on each UriBuilder.
*/
public DefaultUriBuilderFactory() {
this.baseUri = null;
}
/**
* Constructor with a base URI.
*
The given URI template is parsed via
* {@link UriComponentsBuilder#fromUriString} and then applied as a base URI
* to every UriBuilder via {@link UriComponentsBuilder#uriComponents} unless
* the UriBuilder itself was created with a URI template that already has a
* target address.
* @param baseUriTemplate the URI template to use a base URL
*/
public DefaultUriBuilderFactory(String baseUriTemplate) {
this.baseUri = UriComponentsBuilder.fromUriString(baseUriTemplate);
}
/**
* Variant of {@link #DefaultUriBuilderFactory(String)} with a
* {@code UriComponentsBuilder}.
*/
public DefaultUriBuilderFactory(UriComponentsBuilder baseUri) {
this.baseUri = baseUri;
}
/**
* Set the {@link EncodingMode encoding mode} to use.
*
By default this is set to {@link EncodingMode#TEMPLATE_AND_VALUES
* EncodingMode.TEMPLATE_AND_VALUES}.
*
Note: Prior to 5.1 the default was
* {@link EncodingMode#URI_COMPONENT EncodingMode.URI_COMPONENT}
* therefore the {@code WebClient} {@code RestTemplate} have switched their
* default behavior.
* @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;
}
/**
* Provide default URI variable values to use when expanding URI templates
* with a Map of variables.
* @param defaultUriVariables default URI variable values
*/
public void setDefaultUriVariables(@Nullable Map defaultUriVariables) {
this.defaultUriVariables.clear();
if (defaultUriVariables != null) {
this.defaultUriVariables.putAll(defaultUriVariables);
}
}
/**
* Return the configured default URI variable values.
*/
public Map getDefaultUriVariables() {
return Collections.unmodifiableMap(this.defaultUriVariables);
}
/**
* Whether to parse the input path into path segments if the encoding mode
* is set to {@link EncodingMode#URI_COMPONENT EncodingMode.URI_COMPONENT},
* which ensures that URI variables in the path are encoded according to
* path segment rules and for example a '/' is encoded.
* By default this is set to {@code true}.
* @param parsePath whether to parse the path into path segments
*/
public void setParsePath(boolean parsePath) {
this.parsePath = parsePath;
}
/**
* Whether to parse the path into path segments if the encoding mode is set
* to {@link EncodingMode#URI_COMPONENT EncodingMode.URI_COMPONENT}.
*/
public boolean shouldParsePath() {
return this.parsePath;
}
// UriTemplateHandler
@Override
public URI expand(String uriTemplate, Map uriVars) {
return uriString(uriTemplate).build(uriVars);
}
@Override
public URI expand(String uriTemplate, Object... uriVars) {
return uriString(uriTemplate).build(uriVars);
}
// UriBuilderFactory
@Override
public UriBuilder uriString(String uriTemplate) {
return new DefaultUriBuilder(uriTemplate);
}
@Override
public UriBuilder builder() {
return new DefaultUriBuilder("");
}
/**
* Enum to represent multiple URI encoding strategies. The following are
* available:
*
* - {@link #TEMPLATE_AND_VALUES}
*
- {@link #VALUES_ONLY}
*
- {@link #URI_COMPONENT}
*
- {@link #NONE}
*
* @see #setEncodingMode
*/
public enum EncodingMode {
/**
* Pre-encode the URI template first, then strictly encode URI variables
* when expanded, with the following rules:
*
* - For the URI template replace only non-ASCII and illegal
* (within a given URI component type) characters with escaped octets.
*
- For URI variables do the same and also replace characters with
* reserved meaning.
*
* For most cases, this mode is most likely to give the expected
* result because in treats URI variables as opaque data to be fully
* encoded, while {@link #URI_COMPONENT} by comparison is useful only
* if intentionally expanding URI variables with reserved characters.
* @since 5.0.8
* @see UriComponentsBuilder#encode()
*/
TEMPLATE_AND_VALUES,
/**
* Does not encode the URI template and instead applies strict encoding
* to URI variables via {@link UriUtils#encodeUriVariables} prior to
* expanding them into the template.
* @see UriUtils#encodeUriVariables(Object...)
* @see UriUtils#encodeUriVariables(Map)
*/
VALUES_ONLY,
/**
* Expand URI variables first, and then encode the resulting URI
* component values, replacing only non-ASCII and illegal
* (within a given URI component type) characters, but not characters
* with reserved meaning.
* @see UriComponents#encode()
*/
URI_COMPONENT,
/**
* No encoding should be applied.
*/
NONE
}
/**
* {@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) {
UriComponentsBuilder result;
if (!StringUtils.hasLength(uriTemplate)) {
result = (baseUri != null ? baseUri.cloneBuilder() : UriComponentsBuilder.newInstance());
}
else if (baseUri != null) {
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(uriTemplate);
UriComponents uri = builder.build();
result = (uri.getHost() == null ? baseUri.cloneBuilder().uriComponents(uri) : builder);
}
else {
result = UriComponentsBuilder.fromUriString(uriTemplate);
}
if (encodingMode.equals(EncodingMode.TEMPLATE_AND_VALUES)) {
result.encode();
}
parsePathIfNecessary(result);
return result;
}
private void parsePathIfNecessary(UriComponentsBuilder result) {
if (parsePath && encodingMode.equals(EncodingMode.URI_COMPONENT)) {
UriComponents uric = result.build();
String path = uric.getPath();
result.replacePath(null);
for (String segment : uric.getPathSegments()) {
result.pathSegment(segment);
}
if (path != null && path.endsWith("/")) {
result.path("/");
}
}
}
@Override
public DefaultUriBuilder scheme(@Nullable String scheme) {
this.uriComponentsBuilder.scheme(scheme);
return this;
}
@Override
public DefaultUriBuilder userInfo(@Nullable String userInfo) {
this.uriComponentsBuilder.userInfo(userInfo);
return this;
}
@Override
public DefaultUriBuilder host(@Nullable String host) {
this.uriComponentsBuilder.host(host);
return this;
}
@Override
public DefaultUriBuilder port(int port) {
this.uriComponentsBuilder.port(port);
return this;
}
@Override
public DefaultUriBuilder port(@Nullable String port) {
this.uriComponentsBuilder.port(port);
return this;
}
@Override
public DefaultUriBuilder path(String path) {
this.uriComponentsBuilder.path(path);
return this;
}
@Override
public DefaultUriBuilder replacePath(@Nullable 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(@Nullable 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 queryParam(String name, @Nullable Collection> values) {
this.uriComponentsBuilder.queryParam(name, values);
return this;
}
@Override
public DefaultUriBuilder queryParamIfPresent(String name, Optional> value) {
this.uriComponentsBuilder.queryParamIfPresent(name, value);
return this;
}
@Override
public DefaultUriBuilder queryParams(MultiValueMap params) {
this.uriComponentsBuilder.queryParams(params);
return this;
}
@Override
public DefaultUriBuilder replaceQueryParam(String name, Object... values) {
this.uriComponentsBuilder.replaceQueryParam(name, values);
return this;
}
@Override
public DefaultUriBuilder replaceQueryParam(String name, @Nullable Collection> values) {
this.uriComponentsBuilder.replaceQueryParam(name, values);
return this;
}
@Override
public DefaultUriBuilder replaceQueryParams(MultiValueMap params) {
this.uriComponentsBuilder.replaceQueryParams(params);
return this;
}
@Override
public DefaultUriBuilder fragment(@Nullable String fragment) {
this.uriComponentsBuilder.fragment(fragment);
return this;
}
@Override
public URI build(Map uriVars) {
if (!defaultUriVariables.isEmpty()) {
Map map = new HashMap<>();
map.putAll(defaultUriVariables);
map.putAll(uriVars);
uriVars = map;
}
if (encodingMode.equals(EncodingMode.VALUES_ONLY)) {
uriVars = UriUtils.encodeUriVariables(uriVars);
}
UriComponents uric = this.uriComponentsBuilder.build().expand(uriVars);
return createUri(uric);
}
@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 uric = this.uriComponentsBuilder.build().expand(uriVars);
return createUri(uric);
}
private URI createUri(UriComponents uric) {
if (encodingMode.equals(EncodingMode.URI_COMPONENT)) {
uric = uric.encode();
}
return URI.create(uric.toString());
}
}
}