24 changed files with 1525 additions and 543 deletions
@ -0,0 +1,238 @@
@@ -0,0 +1,238 @@
|
||||
/* |
||||
* 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.reactive.function.client; |
||||
|
||||
import java.net.URI; |
||||
import java.nio.charset.Charset; |
||||
import java.time.ZoneId; |
||||
import java.time.ZonedDateTime; |
||||
import java.time.format.DateTimeFormatter; |
||||
import java.util.Arrays; |
||||
import java.util.function.Function; |
||||
|
||||
import org.jetbrains.annotations.NotNull; |
||||
import org.reactivestreams.Publisher; |
||||
import reactor.core.publisher.Mono; |
||||
|
||||
import org.springframework.http.HttpHeaders; |
||||
import org.springframework.http.HttpMethod; |
||||
import org.springframework.http.MediaType; |
||||
import org.springframework.http.client.reactive.ClientHttpRequest; |
||||
import org.springframework.util.MultiValueMap; |
||||
import org.springframework.web.reactive.function.BodyInserter; |
||||
import org.springframework.web.util.DefaultUriBuilderFactory; |
||||
import org.springframework.web.util.UriBuilderFactory; |
||||
|
||||
|
||||
/** |
||||
* Default implementation of {@link WebClientOperations}. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
* @since 5.0 |
||||
*/ |
||||
class DefaultWebClientOperations implements WebClientOperations { |
||||
|
||||
private final WebClient webClient; |
||||
|
||||
private final UriBuilderFactory uriBuilderFactory; |
||||
|
||||
|
||||
DefaultWebClientOperations(WebClient webClient, UriBuilderFactory factory) { |
||||
this.webClient = webClient; |
||||
this.uriBuilderFactory = (factory != null ? factory : new DefaultUriBuilderFactory()); |
||||
} |
||||
|
||||
|
||||
private WebClient getWebClient() { |
||||
return this.webClient; |
||||
} |
||||
|
||||
private UriBuilderFactory getUriBuilderFactory() { |
||||
return this.uriBuilderFactory; |
||||
} |
||||
|
||||
|
||||
@Override |
||||
public UriSpec get() { |
||||
return method(HttpMethod.GET); |
||||
} |
||||
|
||||
@Override |
||||
public UriSpec head() { |
||||
return method(HttpMethod.HEAD); |
||||
} |
||||
|
||||
@Override |
||||
public UriSpec post() { |
||||
return method(HttpMethod.POST); |
||||
} |
||||
|
||||
@Override |
||||
public UriSpec put() { |
||||
return method(HttpMethod.PUT); |
||||
} |
||||
|
||||
@Override |
||||
public UriSpec patch() { |
||||
return method(HttpMethod.PATCH); |
||||
} |
||||
|
||||
@Override |
||||
public UriSpec delete() { |
||||
return method(HttpMethod.DELETE); |
||||
} |
||||
|
||||
@Override |
||||
public UriSpec options() { |
||||
return method(HttpMethod.OPTIONS); |
||||
} |
||||
|
||||
@NotNull |
||||
private UriSpec method(HttpMethod httpMethod) { |
||||
return new DefaultUriSpec(httpMethod); |
||||
} |
||||
|
||||
|
||||
@Override |
||||
public WebClientOperations filter(ExchangeFilterFunction filterFunction) { |
||||
WebClient filteredWebClient = this.webClient.filter(filterFunction); |
||||
return new DefaultWebClientOperations(filteredWebClient, this.uriBuilderFactory); |
||||
} |
||||
|
||||
|
||||
private class DefaultUriSpec implements UriSpec { |
||||
|
||||
private final HttpMethod httpMethod; |
||||
|
||||
|
||||
DefaultUriSpec(HttpMethod httpMethod) { |
||||
this.httpMethod = httpMethod; |
||||
} |
||||
|
||||
@Override |
||||
public HeaderSpec uri(URI uri) { |
||||
return new DefaultHeaderSpec(ClientRequest.method(this.httpMethod, uri)); |
||||
} |
||||
|
||||
@Override |
||||
public HeaderSpec uri(String uriTemplate, Object... uriVariables) { |
||||
return uri(getUriBuilderFactory().expand(uriTemplate)); |
||||
} |
||||
|
||||
@Override |
||||
public HeaderSpec uri(Function<UriBuilderFactory, URI> uriFunction) { |
||||
return uri(uriFunction.apply(getUriBuilderFactory())); |
||||
} |
||||
} |
||||
|
||||
private class DefaultHeaderSpec implements HeaderSpec { |
||||
|
||||
private final ClientRequest.Builder requestBuilder; |
||||
|
||||
private final HttpHeaders headers = new HttpHeaders(); |
||||
|
||||
|
||||
DefaultHeaderSpec(ClientRequest.Builder requestBuilder) { |
||||
this.requestBuilder = requestBuilder; |
||||
} |
||||
|
||||
|
||||
@Override |
||||
public DefaultHeaderSpec header(String headerName, String... headerValues) { |
||||
for (String headerValue : headerValues) { |
||||
this.headers.add(headerName, headerValue); |
||||
} |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public DefaultHeaderSpec headers(HttpHeaders headers) { |
||||
if (headers != null) { |
||||
this.headers.putAll(headers); |
||||
} |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public DefaultHeaderSpec accept(MediaType... acceptableMediaTypes) { |
||||
this.headers.setAccept(Arrays.asList(acceptableMediaTypes)); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public DefaultHeaderSpec acceptCharset(Charset... acceptableCharsets) { |
||||
this.headers.setAcceptCharset(Arrays.asList(acceptableCharsets)); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public DefaultHeaderSpec contentType(MediaType contentType) { |
||||
this.headers.setContentType(contentType); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public DefaultHeaderSpec contentLength(long contentLength) { |
||||
this.headers.setContentLength(contentLength); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public DefaultHeaderSpec cookie(String name, String value) { |
||||
this.requestBuilder.cookie(name, value); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public DefaultHeaderSpec cookies(MultiValueMap<String, String> cookies) { |
||||
this.requestBuilder.cookies(cookies); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public DefaultHeaderSpec ifModifiedSince(ZonedDateTime ifModifiedSince) { |
||||
ZonedDateTime gmt = ifModifiedSince.withZoneSameInstant(ZoneId.of("GMT")); |
||||
String headerValue = DateTimeFormatter.RFC_1123_DATE_TIME.format(gmt); |
||||
this.headers.set(HttpHeaders.IF_MODIFIED_SINCE, headerValue); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public DefaultHeaderSpec ifNoneMatch(String... ifNoneMatches) { |
||||
this.headers.setIfNoneMatch(Arrays.asList(ifNoneMatches)); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public Mono<ClientResponse> exchange() { |
||||
ClientRequest<Void> request = this.requestBuilder.headers(this.headers).build(); |
||||
return getWebClient().exchange(request); |
||||
} |
||||
|
||||
@Override |
||||
public <T> Mono<ClientResponse> exchange(BodyInserter<T, ? super ClientHttpRequest> inserter) { |
||||
ClientRequest<T> request = this.requestBuilder.headers(this.headers).body(inserter); |
||||
return getWebClient().exchange(request); |
||||
} |
||||
|
||||
@Override |
||||
public <T, S extends Publisher<T>> Mono<ClientResponse> exchange(S publisher, Class<T> elementClass) { |
||||
ClientRequest<S> request = this.requestBuilder.headers(this.headers).body(publisher, elementClass); |
||||
return getWebClient().exchange(request); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,51 @@
@@ -0,0 +1,51 @@
|
||||
/* |
||||
* 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.reactive.function.client; |
||||
|
||||
import org.springframework.util.Assert; |
||||
import org.springframework.web.util.UriBuilderFactory; |
||||
|
||||
/** |
||||
* Default implementation of {@link WebClientOperations.Builder}. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
* @since 5.0 |
||||
*/ |
||||
class DefaultWebClientOperationsBuilder implements WebClientOperations.Builder { |
||||
|
||||
private final WebClient webClient; |
||||
|
||||
private UriBuilderFactory uriBuilderFactory; |
||||
|
||||
|
||||
public DefaultWebClientOperationsBuilder(WebClient webClient) { |
||||
Assert.notNull(webClient, "WebClient is required"); |
||||
this.webClient = webClient; |
||||
} |
||||
|
||||
|
||||
@Override |
||||
public WebClientOperations.Builder uriBuilderFactory(UriBuilderFactory uriBuilderFactory) { |
||||
this.uriBuilderFactory = uriBuilderFactory; |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public WebClientOperations build() { |
||||
return new DefaultWebClientOperations(this.webClient, this.uriBuilderFactory); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,295 @@
@@ -0,0 +1,295 @@
|
||||
/* |
||||
* 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.reactive.function.client; |
||||
|
||||
import java.net.URI; |
||||
import java.nio.charset.Charset; |
||||
import java.time.ZonedDateTime; |
||||
import java.util.function.Function; |
||||
|
||||
import org.reactivestreams.Publisher; |
||||
import reactor.core.publisher.Mono; |
||||
|
||||
import org.springframework.http.HttpHeaders; |
||||
import org.springframework.http.MediaType; |
||||
import org.springframework.http.client.reactive.ClientHttpRequest; |
||||
import org.springframework.util.MultiValueMap; |
||||
import org.springframework.web.reactive.function.BodyInserter; |
||||
import org.springframework.web.util.UriBuilderFactory; |
||||
|
||||
/** |
||||
* The main class for performing requests through a WebClient. |
||||
* |
||||
* <pre class="code"> |
||||
* |
||||
* // Create WebClient (application-wide)
|
||||
* |
||||
* ClientHttpConnector connector = new ReactorClientHttpConnector(); |
||||
* WebClient webClient = WebClient.create(connector); |
||||
* |
||||
* // Create WebClientOperations (per base URI)
|
||||
* |
||||
* String baseUri = "http://abc.com"; |
||||
* UriBuilderFactory factory = new DefaultUriBuilderFactory(baseUri); |
||||
* WebClientOperations operations = WebClientOperations.create(webClient, factory); |
||||
* |
||||
* // Perform requests...
|
||||
* |
||||
* Mono<String> result = operations.get() |
||||
* .uri("/foo") |
||||
* .exchange() |
||||
* .then(response -> response.bodyToMono(String.class)); |
||||
* </pre> |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
* @since 5.0 |
||||
*/ |
||||
public interface WebClientOperations { |
||||
|
||||
/** |
||||
* Prepare an HTTP GET request. |
||||
* @return a spec for specifying the target URL |
||||
*/ |
||||
UriSpec get(); |
||||
|
||||
/** |
||||
* Prepare an HTTP HEAD request. |
||||
* @return a spec for specifying the target URL |
||||
*/ |
||||
UriSpec head(); |
||||
|
||||
/** |
||||
* Prepare an HTTP POST request. |
||||
* @return a spec for specifying the target URL |
||||
*/ |
||||
UriSpec post(); |
||||
|
||||
/** |
||||
* Prepare an HTTP PUT request. |
||||
* @return a spec for specifying the target URL |
||||
*/ |
||||
UriSpec put(); |
||||
|
||||
/** |
||||
* Prepare an HTTP PATCH request. |
||||
* @return a spec for specifying the target URL |
||||
*/ |
||||
UriSpec patch(); |
||||
|
||||
/** |
||||
* Prepare an HTTP DELETE request. |
||||
* @return a spec for specifying the target URL |
||||
*/ |
||||
UriSpec delete(); |
||||
|
||||
/** |
||||
* Prepare an HTTP OPTIONS request. |
||||
* @return a spec for specifying the target URL |
||||
*/ |
||||
UriSpec options(); |
||||
|
||||
|
||||
/** |
||||
* Filter the client with the given {@code ExchangeFilterFunction}. |
||||
* @param filterFunction the filter to apply to this client |
||||
* @return the filtered client |
||||
* @see ExchangeFilterFunction#apply(ExchangeFunction) |
||||
*/ |
||||
WebClientOperations filter(ExchangeFilterFunction filterFunction); |
||||
|
||||
|
||||
// Static, factory methods
|
||||
|
||||
/** |
||||
* Create {@link WebClientOperations} that wraps the given {@link WebClient}. |
||||
* @param webClient the underlying client to use |
||||
*/ |
||||
static WebClientOperations create(WebClient webClient) { |
||||
return builder(webClient).build(); |
||||
} |
||||
|
||||
/** |
||||
* Create {@link WebClientOperations} with a builder for additional |
||||
* configuration options. |
||||
* @param webClient the underlying client to use |
||||
*/ |
||||
static WebClientOperations.Builder builder(WebClient webClient) { |
||||
return new DefaultWebClientOperationsBuilder(webClient); |
||||
} |
||||
|
||||
|
||||
/** |
||||
* A mutable builder for a {@link WebClientOperations}. |
||||
*/ |
||||
interface Builder { |
||||
|
||||
/** |
||||
* Configure a {@code UriBuilderFactory} for use with this client for |
||||
* example to define a common "base" URI. |
||||
* @param uriBuilderFactory the URI builder factory |
||||
*/ |
||||
Builder uriBuilderFactory(UriBuilderFactory uriBuilderFactory); |
||||
|
||||
/** |
||||
* Builder the {@link WebClient} instance. |
||||
*/ |
||||
WebClientOperations build(); |
||||
|
||||
} |
||||
|
||||
|
||||
/** |
||||
* Contract for specifying the URI for a request. |
||||
*/ |
||||
interface UriSpec { |
||||
|
||||
/** |
||||
* Specify the URI using an absolute, fully constructed {@link URI}. |
||||
*/ |
||||
HeaderSpec uri(URI uri); |
||||
|
||||
/** |
||||
* Specify the URI for the request using a URI template and URI variables. |
||||
* If a {@link UriBuilderFactory} was configured for the client (e.g. |
||||
* with a base URI) it will be used to expand the URI template. |
||||
* @see Builder#uriBuilderFactory(UriBuilderFactory) |
||||
*/ |
||||
HeaderSpec uri(String uri, Object... uriVariables); |
||||
|
||||
/** |
||||
* Build the URI for the request using the {@link UriBuilderFactory} |
||||
* configured for this client. |
||||
* @see Builder#uriBuilderFactory(UriBuilderFactory) |
||||
*/ |
||||
HeaderSpec uri(Function<UriBuilderFactory, URI> uriFunction); |
||||
|
||||
} |
||||
|
||||
/** |
||||
* Contract for specifying request headers leading up to the exchange. |
||||
*/ |
||||
interface HeaderSpec { |
||||
|
||||
/** |
||||
* Set the list of acceptable {@linkplain MediaType media types}, as |
||||
* specified by the {@code Accept} header. |
||||
* @param acceptableMediaTypes the acceptable media types |
||||
* @return this builder |
||||
*/ |
||||
HeaderSpec accept(MediaType... acceptableMediaTypes); |
||||
|
||||
/** |
||||
* Set the list of acceptable {@linkplain Charset charsets}, as specified |
||||
* by the {@code Accept-Charset} header. |
||||
* @param acceptableCharsets the acceptable charsets |
||||
* @return this builder |
||||
*/ |
||||
HeaderSpec acceptCharset(Charset... acceptableCharsets); |
||||
|
||||
/** |
||||
* Set the length of the body in bytes, as specified by the |
||||
* {@code Content-Length} header. |
||||
* @param contentLength the content length |
||||
* @return this builder |
||||
* @see HttpHeaders#setContentLength(long) |
||||
*/ |
||||
HeaderSpec contentLength(long contentLength); |
||||
|
||||
/** |
||||
* Set the {@linkplain MediaType media type} of the body, as specified |
||||
* by the {@code Content-Type} header. |
||||
* @param contentType the content type |
||||
* @return this builder |
||||
* @see HttpHeaders#setContentType(MediaType) |
||||
*/ |
||||
HeaderSpec contentType(MediaType contentType); |
||||
|
||||
/** |
||||
* Add a cookie with the given name and value. |
||||
* @param name the cookie name |
||||
* @param value the cookie value |
||||
* @return this builder |
||||
*/ |
||||
HeaderSpec cookie(String name, String value); |
||||
|
||||
/** |
||||
* Copy the given cookies into the entity's cookies map. |
||||
* |
||||
* @param cookies the existing cookies to copy from |
||||
* @return this builder |
||||
*/ |
||||
HeaderSpec cookies(MultiValueMap<String, String> cookies); |
||||
|
||||
/** |
||||
* Set the value of the {@code If-Modified-Since} header. |
||||
* <p>The date should be specified as the number of milliseconds since |
||||
* January 1, 1970 GMT. |
||||
* @param ifModifiedSince the new value of the header |
||||
* @return this builder |
||||
*/ |
||||
HeaderSpec ifModifiedSince(ZonedDateTime ifModifiedSince); |
||||
|
||||
/** |
||||
* Set the values of the {@code If-None-Match} header. |
||||
* @param ifNoneMatches the new value of the header |
||||
* @return this builder |
||||
*/ |
||||
HeaderSpec ifNoneMatch(String... ifNoneMatches); |
||||
|
||||
/** |
||||
* Add the given, single header value under the given name. |
||||
* @param headerName the header name |
||||
* @param headerValues the header value(s) |
||||
* @return this builder |
||||
*/ |
||||
HeaderSpec header(String headerName, String... headerValues); |
||||
|
||||
/** |
||||
* Copy the given headers into the entity's headers map. |
||||
* @param headers the existing headers to copy from |
||||
* @return this builder |
||||
*/ |
||||
HeaderSpec headers(HttpHeaders headers); |
||||
|
||||
/** |
||||
* Perform the request without a request body. |
||||
* @return a {@code Mono} with the response |
||||
*/ |
||||
Mono<ClientResponse> exchange(); |
||||
|
||||
/** |
||||
* Set the body of the request to the given {@code BodyInserter} and |
||||
* perform the request. |
||||
* @param inserter the {@code BodyInserter} that writes to the request |
||||
* @param <T> the type contained in the body |
||||
* @return a {@code Mono} with the response |
||||
*/ |
||||
<T> Mono<ClientResponse> exchange(BodyInserter<T, ? super ClientHttpRequest> inserter); |
||||
|
||||
/** |
||||
* Set the body of the request to the given {@code Publisher} and |
||||
* perform the request. |
||||
* @param publisher the {@code Publisher} to write to the request |
||||
* @param elementClass the class of elements contained in the publisher |
||||
* @param <T> the type of the elements contained in the publisher |
||||
* @param <S> the type of the {@code Publisher} |
||||
* @return a {@code Mono} with the response |
||||
*/ |
||||
<T, S extends Publisher<T>> Mono<ClientResponse> exchange(S publisher, Class<T> elementClass); |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,299 @@
@@ -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()); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,171 @@
@@ -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("filter={value}").uriString("hot&cold"); |
||||
* </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); |
||||
|
||||
} |
||||
@ -0,0 +1,43 @@
@@ -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); |
||||
|
||||
} |
||||
@ -0,0 +1,136 @@
@@ -0,0 +1,136 @@
|
||||
/* |
||||
* 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.HashMap; |
||||
import java.util.Map; |
||||
|
||||
import org.junit.Test; |
||||
|
||||
import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode; |
||||
|
||||
import static java.util.Collections.singletonMap; |
||||
import static junit.framework.TestCase.assertEquals; |
||||
|
||||
/** |
||||
* Unit tests for {@link DefaultUriBuilderFactory}. |
||||
* @author Rossen Stoyanchev |
||||
*/ |
||||
public class DefaultUriBuilderFactoryTests { |
||||
|
||||
@Test |
||||
public void defaultSettings() throws Exception { |
||||
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(); |
||||
URI uri = factory.uriString("/foo").pathSegment("{id}").build("a/b"); |
||||
assertEquals("/foo/a%2Fb", uri.toString()); |
||||
} |
||||
|
||||
@Test |
||||
public void baseUri() throws Exception { |
||||
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory("http://foo.com/bar?id=123"); |
||||
URI uri = factory.uriString("/baz").port(8080).build(); |
||||
assertEquals("http://foo.com:8080/bar/baz?id=123", uri.toString()); |
||||
} |
||||
|
||||
@Test |
||||
public void baseUriWithPathOverride() throws Exception { |
||||
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory("http://foo.com/bar"); |
||||
URI uri = factory.uriString("").replacePath("/baz").build(); |
||||
assertEquals("http://foo.com/baz", uri.toString()); |
||||
} |
||||
|
||||
@Test |
||||
public void defaultUriVars() throws Exception { |
||||
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory("http://{host}/bar"); |
||||
factory.setDefaultUriVariables(singletonMap("host", "foo.com")); |
||||
URI uri = factory.uriString("/{id}").build(singletonMap("id", "123")); |
||||
assertEquals("http://foo.com/bar/123", uri.toString()); |
||||
} |
||||
|
||||
@Test |
||||
public void defaultUriVarsWithOverride() throws Exception { |
||||
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory("http://{host}/bar"); |
||||
factory.setDefaultUriVariables(singletonMap("host", "spring.io")); |
||||
URI uri = factory.uriString("/baz").build(singletonMap("host", "docs.spring.io")); |
||||
assertEquals("http://docs.spring.io/bar/baz", uri.toString()); |
||||
} |
||||
|
||||
@Test |
||||
public void defaultUriVarsWithEmptyVarArg() throws Exception { |
||||
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory("http://{host}/bar"); |
||||
factory.setDefaultUriVariables(singletonMap("host", "foo.com")); |
||||
URI uri = factory.uriString("/baz").build(); |
||||
assertEquals("Expected delegation to build(Map) method", "http://foo.com/bar/baz", uri.toString()); |
||||
} |
||||
|
||||
@Test |
||||
public void defaultUriVarsSpr14147() throws Exception { |
||||
Map<String, String> defaultUriVars = new HashMap<>(2); |
||||
defaultUriVars.put("host", "api.example.com"); |
||||
defaultUriVars.put("port", "443"); |
||||
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(); |
||||
factory.setDefaultUriVariables(defaultUriVars); |
||||
|
||||
URI uri = factory.expand("https://{host}:{port}/v42/customers/{id}", singletonMap("id", 123L)); |
||||
assertEquals("https://api.example.com:443/v42/customers/123", uri.toString()); |
||||
} |
||||
|
||||
@Test |
||||
public void encodingValuesOnly() throws Exception { |
||||
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(); |
||||
factory.setEncodingMode(EncodingMode.VALUES_ONLY); |
||||
UriBuilder uriBuilder = factory.uriString("/foo/a%2Fb/{id}"); |
||||
|
||||
String id = "c/d"; |
||||
String expected = "/foo/a%2Fb/c%2Fd"; |
||||
|
||||
assertEquals(expected, uriBuilder.build(id).toString()); |
||||
assertEquals(expected, uriBuilder.build(singletonMap("id", id)).toString()); |
||||
} |
||||
|
||||
@Test |
||||
public void encodingValuesOnlySpr14147() throws Exception { |
||||
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(); |
||||
factory.setEncodingMode(EncodingMode.VALUES_ONLY); |
||||
factory.setDefaultUriVariables(singletonMap("host", "www.example.com")); |
||||
UriBuilder uriBuilder = factory.uriString("http://{host}/user/{userId}/dashboard"); |
||||
|
||||
assertEquals("http://www.example.com/user/john%3Bdoe/dashboard", |
||||
uriBuilder.build(singletonMap("userId", "john;doe")).toString()); |
||||
} |
||||
|
||||
@Test |
||||
public void encodingNone() throws Exception { |
||||
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(); |
||||
factory.setEncodingMode(EncodingMode.NONE); |
||||
UriBuilder uriBuilder = factory.uriString("/foo/a%2Fb/{id}"); |
||||
|
||||
String id = "c%2Fd"; |
||||
String expected = "/foo/a%2Fb/c%2Fd"; |
||||
|
||||
assertEquals(expected, uriBuilder.build(id).toString()); |
||||
assertEquals(expected, uriBuilder.build(singletonMap("id", id)).toString()); |
||||
} |
||||
|
||||
@Test |
||||
public void initialPathSplitIntoPathSegments() throws Exception { |
||||
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory("/foo/{bar}"); |
||||
URI uri = factory.uriString("/baz/{id}").build("a/b", "c/d"); |
||||
assertEquals("/foo/a%2Fb/baz/c%2Fd", uri.toString()); |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue