diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/client/ClientRequest.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/client/ClientRequest.java
index b0dd1186170..608d17084a7 100644
--- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/client/ClientRequest.java
+++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/client/ClientRequest.java
@@ -17,27 +17,25 @@
package org.springframework.web.reactive.function.client;
import java.net.URI;
-import java.nio.charset.Charset;
-import java.time.ZonedDateTime;
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.Assert;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.BodyInserter;
-import org.springframework.web.util.DefaultUriTemplateHandler;
-import org.springframework.web.util.UriTemplateHandler;
/**
- * Represents a typed, immutable, client-side HTTP request, as executed by the {@link WebClient}.
- * Instances of this interface are created via static builder methods:
- * {@link #method(HttpMethod, String, Object...)}, {@link #GET(String, Object...)}, etc.
+ * Represents a typed, immutable, client-side HTTP request, as executed by the
+ * {@link WebClient}. Instances of this interface can be created via static
+ * builder methods in this class.
*
+ *
Note that applications are more likely to perform requests through
+ * {@link WebClientOperations} rather than using this directly.
+ * :
* @param the type of the body that this request contains
* @author Brian Clozel
* @author Arjen Poutsma
@@ -45,8 +43,6 @@ import org.springframework.web.util.UriTemplateHandler;
*/
public interface ClientRequest {
- // Instance methods
-
/**
* Return the HTTP method.
*/
@@ -81,6 +77,7 @@ public interface ClientRequest {
*/
Mono writeTo(ClientHttpRequest request, WebClientStrategies strategies);
+
// Static builder methods
/**
@@ -89,7 +86,7 @@ public interface ClientRequest {
* @param other the request to copy the method, URI, headers, and cookies from
* @return the created builder
*/
- static BodyBuilder from(ClientRequest> other) {
+ static Builder from(ClientRequest> other) {
Assert.notNull(other, "'other' must not be null");
return new DefaultClientRequestBuilder(other.method(), other.url())
.headers(other.headers())
@@ -102,100 +99,15 @@ public interface ClientRequest {
* @param url the URL
* @return the created builder
*/
- static BodyBuilder method(HttpMethod method, URI url) {
- return new DefaultClientRequestBuilder(method, url);
- }
-
- /**
- * Create a builder with the given method and url template.
- * @param method the HTTP method (GET, POST, etc)
- * @param urlTemplate the URL template
- * @param uriVariables optional variables to expand the template
- * @return the created builder
- */
- static BodyBuilder method(HttpMethod method, String urlTemplate, Object... uriVariables) {
- UriTemplateHandler templateHandler = new DefaultUriTemplateHandler();
- URI url = templateHandler.expand(urlTemplate, uriVariables);
+ static Builder method(HttpMethod method, URI url) {
return new DefaultClientRequestBuilder(method, url);
}
- /**
- * Create an HTTP GET builder with the given url template.
- * @param urlTemplate the URL template
- * @param uriVariables optional variables to expand the template
- * @return the created builder
- */
- static HeadersBuilder> GET(String urlTemplate, Object... uriVariables) {
- return method(HttpMethod.GET, urlTemplate, uriVariables);
- }
-
- /**
- * Create an HTTP HEAD builder with the given url template.
- * @param urlTemplate the URL template
- * @param uriVariables optional variables to expand the template
- * @return the created builder
- */
- static HeadersBuilder> HEAD(String urlTemplate, Object... uriVariables) {
- return method(HttpMethod.HEAD, urlTemplate, uriVariables);
- }
-
- /**
- * Create an HTTP POST builder with the given url template.
- * @param urlTemplate the URL template
- * @param uriVariables optional variables to expand the template
- * @return the created builder
- */
- static BodyBuilder POST(String urlTemplate, Object... uriVariables) {
- return method(HttpMethod.POST, urlTemplate, uriVariables);
- }
-
- /**
- * Create an HTTP PUT builder with the given url template.
- * @param urlTemplate the URL template
- * @param uriVariables optional variables to expand the template
- * @return the created builder
- */
- static BodyBuilder PUT(String urlTemplate, Object... uriVariables) {
- return method(HttpMethod.PUT, urlTemplate, uriVariables);
- }
-
- /**
- * Create an HTTP PATCH builder with the given url template.
- * @param urlTemplate the URL template
- * @param uriVariables optional variables to expand the template
- * @return the created builder
- */
- static BodyBuilder PATCH(String urlTemplate, Object... uriVariables) {
- return method(HttpMethod.PATCH, urlTemplate, uriVariables);
- }
-
- /**
- * Create an HTTP DELETE builder with the given url template.
- * @param urlTemplate the URL template
- * @param uriVariables optional variables to expand the template
- * @return the created builder
- */
- static HeadersBuilder> DELETE(String urlTemplate, Object... uriVariables) {
- return method(HttpMethod.DELETE, urlTemplate, uriVariables);
- }
-
- /**
- * Creates an HTTP OPTIONS builder with the given url template.
- * @param urlTemplate the URL template
- * @param uriVariables optional variables to expand the template
- * @return the created builder
- */
- static HeadersBuilder> OPTIONS(String urlTemplate, Object... uriVariables) {
- return method(HttpMethod.OPTIONS, urlTemplate, uriVariables);
- }
-
/**
- * Defines a builder that adds headers to the request.
- *
- * @param the builder subclass
+ * Defines a builder for a request.
*/
- interface HeadersBuilder> {
+ interface Builder {
/**
* Add the given, single header value under the given name.
@@ -204,7 +116,7 @@ public interface ClientRequest {
* @return this builder
* @see HttpHeaders#add(String, String)
*/
- B header(String headerName, String... headerValues);
+ Builder header(String headerName, String... headerValues);
/**
* Copy the given headers into the entity's headers map.
@@ -212,39 +124,7 @@ public interface ClientRequest {
* @param headers the existing HttpHeaders to copy from
* @return this builder
*/
- B headers(HttpHeaders headers);
-
- /**
- * 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
- */
- B 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
- */
- B acceptCharset(Charset... acceptableCharsets);
-
- /**
- * Set the value of the {@code If-Modified-Since} header.
- * 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
- */
- B ifModifiedSince(ZonedDateTime ifModifiedSince);
-
- /**
- * Set the values of the {@code If-None-Match} header.
- * @param ifNoneMatches the new value of the header
- * @return this builder
- */
- B ifNoneMatch(String... ifNoneMatches);
+ Builder headers(HttpHeaders headers);
/**
* Add a cookie with the given name and value.
@@ -252,7 +132,7 @@ public interface ClientRequest {
* @param value the cookie value
* @return this builder
*/
- B cookie(String name, String value);
+ Builder cookie(String name, String value);
/**
* Copy the given cookies into the entity's cookies map.
@@ -260,39 +140,13 @@ public interface ClientRequest {
* @param cookies the existing cookies to copy from
* @return this builder
*/
- B cookies(MultiValueMap cookies);
+ Builder cookies(MultiValueMap cookies);
/**
* Builds the request entity with no body.
* @return the request entity
*/
ClientRequest build();
- }
-
-
- /**
- * Defines a builder that adds a body to the request entity.
- */
- interface BodyBuilder extends HeadersBuilder {
-
-
- /**
- * 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)
- */
- BodyBuilder 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)
- */
- BodyBuilder contentType(MediaType contentType);
/**
* Set the body of the request to the given {@code BodyInserter} and return it.
@@ -314,5 +168,4 @@ public interface ClientRequest {
}
-
}
diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/client/DefaultClientRequestBuilder.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/client/DefaultClientRequestBuilder.java
index 0af5c1b2c20..e0f65d6db8d 100644
--- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/client/DefaultClientRequestBuilder.java
+++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/client/DefaultClientRequestBuilder.java
@@ -17,11 +17,6 @@
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.Collections;
import java.util.Map;
import java.util.function.Supplier;
@@ -33,7 +28,6 @@ import reactor.core.publisher.Mono;
import org.springframework.http.HttpCookie;
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.http.codec.HttpMessageWriter;
import org.springframework.util.Assert;
@@ -44,12 +38,12 @@ import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.BodyInserters;
/**
- * Default implementation of {@link ClientRequest.BodyBuilder}.
+ * Default implementation of {@link ClientRequest.Builder}.
*
* @author Arjen Poutsma
* @since 5.0
*/
-class DefaultClientRequestBuilder implements ClientRequest.BodyBuilder {
+class DefaultClientRequestBuilder implements ClientRequest.Builder {
private final HttpMethod method;
@@ -66,7 +60,7 @@ class DefaultClientRequestBuilder implements ClientRequest.BodyBuilder {
}
@Override
- public ClientRequest.BodyBuilder header(String headerName, String... headerValues) {
+ public ClientRequest.Builder header(String headerName, String... headerValues) {
for (String headerValue : headerValues) {
this.headers.add(headerName, headerValue);
}
@@ -74,7 +68,7 @@ class DefaultClientRequestBuilder implements ClientRequest.BodyBuilder {
}
@Override
- public ClientRequest.BodyBuilder headers(HttpHeaders headers) {
+ public ClientRequest.Builder headers(HttpHeaders headers) {
if (headers != null) {
this.headers.putAll(headers);
}
@@ -82,39 +76,13 @@ class DefaultClientRequestBuilder implements ClientRequest.BodyBuilder {
}
@Override
- public ClientRequest.BodyBuilder accept(MediaType... acceptableMediaTypes) {
- this.headers.setAccept(Arrays.asList(acceptableMediaTypes));
- return this;
- }
-
- @Override
- public ClientRequest.BodyBuilder acceptCharset(Charset... acceptableCharsets) {
- this.headers.setAcceptCharset(Arrays.asList(acceptableCharsets));
- return this;
- }
-
- @Override
- public ClientRequest.BodyBuilder 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 ClientRequest.BodyBuilder ifNoneMatch(String... ifNoneMatches) {
- this.headers.setIfNoneMatch(Arrays.asList(ifNoneMatches));
- return this;
- }
-
- @Override
- public ClientRequest.BodyBuilder cookie(String name, String value) {
+ public ClientRequest.Builder cookie(String name, String value) {
this.cookies.add(name, value);
return this;
}
@Override
- public ClientRequest.BodyBuilder cookies(MultiValueMap cookies) {
+ public ClientRequest.Builder cookies(MultiValueMap cookies) {
if (cookies != null) {
this.cookies.putAll(cookies);
}
@@ -126,18 +94,6 @@ class DefaultClientRequestBuilder implements ClientRequest.BodyBuilder {
return body(BodyInserters.empty());
}
- @Override
- public ClientRequest.BodyBuilder contentLength(long contentLength) {
- this.headers.setContentLength(contentLength);
- return this;
- }
-
- @Override
- public ClientRequest.BodyBuilder contentType(MediaType contentType) {
- this.headers.setContentType(contentType);
- return this;
- }
-
@Override
public ClientRequest body(BodyInserter inserter) {
Assert.notNull(inserter, "'inserter' must not be null");
diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientOperations.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientOperations.java
new file mode 100644
index 00000000000..0c06efe9dc6
--- /dev/null
+++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientOperations.java
@@ -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 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 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 exchange() {
+ ClientRequest request = this.requestBuilder.headers(this.headers).build();
+ return getWebClient().exchange(request);
+ }
+
+ @Override
+ public Mono exchange(BodyInserter inserter) {
+ ClientRequest request = this.requestBuilder.headers(this.headers).body(inserter);
+ return getWebClient().exchange(request);
+ }
+
+ @Override
+ public > Mono exchange(S publisher, Class elementClass) {
+ ClientRequest request = this.requestBuilder.headers(this.headers).body(publisher, elementClass);
+ return getWebClient().exchange(request);
+ }
+ }
+
+}
diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientOperationsBuilder.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientOperationsBuilder.java
new file mode 100644
index 00000000000..f8c7749837d
--- /dev/null
+++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientOperationsBuilder.java
@@ -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);
+ }
+
+}
diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/client/WebClientOperations.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/client/WebClientOperations.java
new file mode 100644
index 00000000000..7200f8fb8c3
--- /dev/null
+++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/client/WebClientOperations.java
@@ -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.
+ *
+ *
+ *
+ * // 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 result = operations.get()
+ * .uri("/foo")
+ * .exchange()
+ * .then(response -> response.bodyToMono(String.class));
+ *
+ *
+ * @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 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 cookies);
+
+ /**
+ * Set the value of the {@code If-Modified-Since} header.
+ * 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 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 the type contained in the body
+ * @return a {@code Mono} with the response
+ */
+ Mono exchange(BodyInserter 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 the type of the elements contained in the publisher
+ * @param the type of the {@code Publisher}
+ * @return a {@code Mono} with the response
+ */
+ > Mono exchange(S publisher, Class elementClass);
+
+ }
+
+}
diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/FlushingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/FlushingIntegrationTests.java
index 7cd0d14e323..5e72bcea6d1 100644
--- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/FlushingIntegrationTests.java
+++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/FlushingIntegrationTests.java
@@ -37,15 +37,17 @@ import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.bootstrap.RxNettyHttpServer;
import org.springframework.util.Assert;
import org.springframework.web.reactive.function.BodyExtractors;
-import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.WebClient;
+import org.springframework.web.reactive.function.client.WebClientOperations;
+import org.springframework.web.util.DefaultUriBuilderFactory;
+import org.springframework.web.util.UriBuilderFactory;
/**
* @author Sebastien Deleuze
*/
public class FlushingIntegrationTests extends AbstractHttpHandlerIntegrationTests {
- private WebClient webClient;
+ private WebClientOperations operations;
@Before
@@ -55,15 +57,18 @@ public class FlushingIntegrationTests extends AbstractHttpHandlerIntegrationTest
Assume.assumeFalse(this.server instanceof RxNettyHttpServer);
super.setup();
- this.webClient = WebClient.create(new ReactorClientHttpConnector());
+
+ WebClient client = WebClient.create(new ReactorClientHttpConnector());
+ UriBuilderFactory factory = new DefaultUriBuilderFactory("http://localhost:" + this.port);
+ this.operations = WebClientOperations.builder(client).uriBuilderFactory(factory).build();
}
@Test
public void writeAndFlushWith() throws Exception {
- ClientRequest request = ClientRequest.GET("http://localhost:" + port + "/write-and-flush").build();
- Mono result = this.webClient
- .exchange(request)
+ Mono result = this.operations.get()
+ .uri("/write-and-flush")
+ .exchange()
.flatMap(response -> response.body(BodyExtractors.toFlux(String.class)))
.takeUntil(s -> s.endsWith("data1"))
.reduce((s1, s2) -> s1 + s2);
@@ -76,9 +81,9 @@ public class FlushingIntegrationTests extends AbstractHttpHandlerIntegrationTest
@Test // SPR-14991
public void writeAndAutoFlushOnComplete() {
- ClientRequest request = ClientRequest.GET("http://localhost:" + port + "/write-and-complete").build();
- Mono result = this.webClient
- .exchange(request)
+ Mono result = this.operations.get()
+ .uri("/write-and-complete")
+ .exchange()
.flatMap(response -> response.bodyToFlux(String.class))
.reduce((s1, s2) -> s1 + s2);
@@ -90,9 +95,9 @@ public class FlushingIntegrationTests extends AbstractHttpHandlerIntegrationTest
@Test // SPR-14992
public void writeAndAutoFlushBeforeComplete() {
- ClientRequest request = ClientRequest.GET("http://localhost:" + port + "/write-and-never-complete").build();
- Flux result = this.webClient
- .exchange(request)
+ Flux result = this.operations.get()
+ .uri("/write-and-never-complete")
+ .exchange()
.flatMap(response -> response.bodyToFlux(String.class));
StepVerifier.create(result)
diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/client/DefaultClientRequestBuilderTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/client/DefaultClientRequestBuilderTests.java
index a32dbd53918..b16ad3c0c7e 100644
--- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/client/DefaultClientRequestBuilderTests.java
+++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/client/DefaultClientRequestBuilderTests.java
@@ -18,11 +18,7 @@ package org.springframework.web.reactive.function.client;
import java.net.URI;
import java.nio.ByteBuffer;
-import java.nio.charset.Charset;
-import java.time.ZonedDateTime;
import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
import java.util.List;
import org.junit.Test;
@@ -31,8 +27,6 @@ import reactor.core.publisher.Mono;
import org.springframework.core.codec.CharSequenceEncoder;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
-import org.springframework.http.HttpMethod;
-import org.springframework.http.MediaType;
import org.springframework.http.client.reactive.ClientHttpRequest;
import org.springframework.http.codec.EncoderHttpMessageWriter;
import org.springframework.http.codec.HttpMessageWriter;
@@ -45,6 +39,9 @@ import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
+import static org.springframework.http.HttpMethod.DELETE;
+import static org.springframework.http.HttpMethod.GET;
+import static org.springframework.http.HttpMethod.POST;
/**
* @author Arjen Poutsma
@@ -53,12 +50,12 @@ public class DefaultClientRequestBuilderTests {
@Test
public void from() throws Exception {
- ClientRequest other = ClientRequest.GET("http://example.com")
+ ClientRequest other = ClientRequest.method(GET, URI.create("http://example.com"))
.header("foo", "bar")
.cookie("baz", "qux").build();
ClientRequest result = ClientRequest.from(other).build();
assertEquals(new URI("http://example.com"), result.url());
- assertEquals(HttpMethod.GET, result.method());
+ assertEquals(GET, result.method());
assertEquals("bar", result.headers().getFirst("foo"));
assertEquals("qux", result.cookies().getFirst("baz"));
}
@@ -66,112 +63,26 @@ public class DefaultClientRequestBuilderTests {
@Test
public void method() throws Exception {
URI url = new URI("http://example.com");
- ClientRequest result = ClientRequest.method(HttpMethod.DELETE, url).build();
+ ClientRequest result = ClientRequest.method(DELETE, url).build();
assertEquals(url, result.url());
- assertEquals(HttpMethod.DELETE, result.method());
- }
-
- @Test
- public void GET() throws Exception {
- URI url = new URI("http://example.com");
- ClientRequest result = ClientRequest.GET(url.toString()).build();
- assertEquals(url, result.url());
- assertEquals(HttpMethod.GET, result.method());
- }
-
- @Test
- public void HEAD() throws Exception {
- URI url = new URI("http://example.com");
- ClientRequest result = ClientRequest.HEAD(url.toString()).build();
- assertEquals(url, result.url());
- assertEquals(HttpMethod.HEAD, result.method());
- }
-
- @Test
- public void POST() throws Exception {
- URI url = new URI("http://example.com");
- ClientRequest result = ClientRequest.POST(url.toString()).build();
- assertEquals(url, result.url());
- assertEquals(HttpMethod.POST, result.method());
- }
-
- @Test
- public void PUT() throws Exception {
- URI url = new URI("http://example.com");
- ClientRequest result = ClientRequest.PUT(url.toString()).build();
- assertEquals(url, result.url());
- assertEquals(HttpMethod.PUT, result.method());
- }
-
- @Test
- public void PATCH() throws Exception {
- URI url = new URI("http://example.com");
- ClientRequest result = ClientRequest.PATCH(url.toString()).build();
- assertEquals(url, result.url());
- assertEquals(HttpMethod.PATCH, result.method());
- }
-
- @Test
- public void DELETE() throws Exception {
- URI url = new URI("http://example.com");
- ClientRequest result = ClientRequest.DELETE(url.toString()).build();
- assertEquals(url, result.url());
- assertEquals(HttpMethod.DELETE, result.method());
- }
-
- @Test
- public void OPTIONS() throws Exception {
- URI url = new URI("http://example.com");
- ClientRequest result = ClientRequest.OPTIONS(url.toString()).build();
- assertEquals(url, result.url());
- assertEquals(HttpMethod.OPTIONS, result.method());
- }
-
- @Test
- public void accept() throws Exception {
- MediaType json = MediaType.APPLICATION_JSON;
- ClientRequest result = ClientRequest.GET("http://example.com").accept(json).build();
- assertEquals(Collections.singletonList(json), result.headers().getAccept());
- }
-
- @Test
- public void acceptCharset() throws Exception {
- Charset charset = Charset.defaultCharset();
- ClientRequest result = ClientRequest.GET("http://example.com")
- .acceptCharset(charset).build();
- assertEquals(Collections.singletonList(charset), result.headers().getAcceptCharset());
- }
-
- @Test
- public void ifModifiedSince() throws Exception {
- ZonedDateTime now = ZonedDateTime.now();
- ClientRequest result = ClientRequest.GET("http://example.com")
- .ifModifiedSince(now).build();
- assertEquals(now.toInstant().toEpochMilli()/1000, result.headers().getIfModifiedSince()/1000);
- }
-
- @Test
- public void ifNoneMatch() throws Exception {
- ClientRequest result = ClientRequest.GET("http://example.com")
- .ifNoneMatch("\"v2.7\"", "\"v2.8\"").build();
- assertEquals(Arrays.asList("\"v2.7\"", "\"v2.8\""), result.headers().getIfNoneMatch());
+ assertEquals(DELETE, result.method());
}
@Test
public void cookie() throws Exception {
- ClientRequest result = ClientRequest.GET("http://example.com")
+ ClientRequest result = ClientRequest.method(GET, URI.create("http://example.com"))
.cookie("foo", "bar").build();
assertEquals("bar", result.cookies().getFirst("foo"));
}
@Test
public void build() throws Exception {
- ClientRequest result = ClientRequest.GET("http://example.com")
+ ClientRequest result = ClientRequest.method(GET, URI.create("http://example.com"))
.header("MyKey", "MyValue")
.cookie("foo", "bar")
.build();
- MockClientHttpRequest request = new MockClientHttpRequest(HttpMethod.GET, "/");
+ MockClientHttpRequest request = new MockClientHttpRequest(GET, "/");
WebClientStrategies strategies = mock(WebClientStrategies.class);
result.writeTo(request, strategies).block();
@@ -193,7 +104,7 @@ public class DefaultClientRequestBuilderTests {
return response.writeWith(Mono.just(buffer));
};
- ClientRequest result = ClientRequest.POST("http://example.com")
+ ClientRequest result = ClientRequest.method(POST, URI.create("http://example.com"))
.body(inserter);
List> messageWriters = new ArrayList<>();
@@ -202,7 +113,7 @@ public class DefaultClientRequestBuilderTests {
WebClientStrategies strategies = mock(WebClientStrategies.class);
when(strategies.messageWriters()).thenReturn(messageWriters::stream);
- MockClientHttpRequest request = new MockClientHttpRequest(HttpMethod.GET, "/");
+ MockClientHttpRequest request = new MockClientHttpRequest(GET, "/");
result.writeTo(request, strategies).block();
assertNotNull(request.getBody());
}
diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/client/ExchangeFilterFunctionsTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/client/ExchangeFilterFunctionsTests.java
index 299ce98b22b..37bcc3e4c8f 100644
--- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/client/ExchangeFilterFunctionsTests.java
+++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/client/ExchangeFilterFunctionsTests.java
@@ -16,15 +16,19 @@
package org.springframework.web.reactive.function.client;
+import java.net.URI;
+
import org.junit.Test;
import reactor.core.publisher.Mono;
import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
+import static org.springframework.http.HttpMethod.GET;
/**
* @author Arjen Poutsma
@@ -33,7 +37,7 @@ public class ExchangeFilterFunctionsTests {
@Test
public void andThen() throws Exception {
- ClientRequest request = ClientRequest.GET("http://example.com").build();
+ ClientRequest request = ClientRequest.method(GET, URI.create("http://example.com")).build();
ClientResponse response = mock(ClientResponse.class);
ExchangeFunction exchange = r -> Mono.just(response);
@@ -63,7 +67,7 @@ public class ExchangeFilterFunctionsTests {
@Test
public void apply() throws Exception {
- ClientRequest request = ClientRequest.GET("http://example.com").build();
+ ClientRequest request = ClientRequest.method(GET, URI.create("http://example.com")).build();
ClientResponse response = mock(ClientResponse.class);
ExchangeFunction exchange = r -> Mono.just(response);
@@ -82,7 +86,7 @@ public class ExchangeFilterFunctionsTests {
@Test
public void basicAuthentication() throws Exception {
- ClientRequest request = ClientRequest.GET("http://example.com").build();
+ ClientRequest request = ClientRequest.method(GET, URI.create("http://example.com")).build();
ClientResponse response = mock(ClientResponse.class);
ExchangeFunction exchange = r -> {
diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java
index 8cfc60dab99..5dabfc061cd 100644
--- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java
+++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java
@@ -18,7 +18,6 @@ package org.springframework.web.reactive.function.client;
import java.time.Duration;
-import okhttp3.HttpUrl;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
@@ -36,39 +35,51 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.http.codec.Pojo;
-import org.springframework.web.reactive.function.BodyExtractors;
-import org.springframework.web.reactive.function.BodyInserters;
+import org.springframework.web.util.DefaultUriBuilderFactory;
+import org.springframework.web.util.UriBuilderFactory;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
-import static org.springframework.web.reactive.function.BodyExtractors.toFlux;
-import static org.springframework.web.reactive.function.BodyExtractors.toMono;
+import static org.springframework.web.reactive.function.BodyInserters.fromObject;
/**
- * {@link WebClient} integration tests with the {@code Flux} and {@code Mono} API.
+ * Integration tests using a {@link WebClient} through {@link WebClientOperations}.
*
* @author Brian Clozel
+ * @author Rossen Stoyanchev
*/
public class WebClientIntegrationTests {
private MockWebServer server;
- private WebClient webClient;
+ private WebClientOperations operations;
+
@Before
public void setup() {
this.server = new MockWebServer();
- this.webClient = WebClient.create(new ReactorClientHttpConnector());
+
+ WebClient webClient = WebClient.create(new ReactorClientHttpConnector());
+ UriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory(this.server.url("/").toString());
+
+ this.operations = WebClientOperations.builder(webClient)
+ .uriBuilderFactory(uriBuilderFactory)
+ .build();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ this.server.shutdown();
}
+
@Test
public void headers() throws Exception {
- HttpUrl baseUrl = server.url("/greeting?name=Spring");
this.server.enqueue(new MockResponse().setHeader("Content-Type", "text/plain").setBody("Hello Spring!"));
- ClientRequest request = ClientRequest.GET(baseUrl.toString()).build();
- Mono result = this.webClient
- .exchange(request)
+ Mono result = this.operations.get()
+ .uri("/greeting?name=Spring")
+ .exchange()
.map(response -> response.headers().asHttpHeaders());
StepVerifier.create(result)
@@ -88,16 +99,13 @@ public class WebClientIntegrationTests {
@Test
public void plainText() throws Exception {
- HttpUrl baseUrl = server.url("/greeting?name=Spring");
this.server.enqueue(new MockResponse().setBody("Hello Spring!"));
- ClientRequest request = ClientRequest.GET(baseUrl.toString())
+ Mono result = this.operations.get()
+ .uri("/greeting?name=Spring")
.header("X-Test-Header", "testvalue")
- .build();
-
- Mono result = this.webClient
- .exchange(request)
- .then(response -> response.body(toMono(String.class)));
+ .exchange()
+ .then(response -> response.bodyToMono(String.class));
StepVerifier.create(result)
.expectNext("Hello Spring!")
@@ -113,18 +121,15 @@ public class WebClientIntegrationTests {
@Test
public void jsonString() throws Exception {
- HttpUrl baseUrl = server.url("/json");
String content = "{\"bar\":\"barbar\",\"foo\":\"foofoo\"}";
this.server.enqueue(new MockResponse().setHeader("Content-Type", "application/json")
.setBody(content));
- ClientRequest request = ClientRequest.GET(baseUrl.toString())
+ Mono result = this.operations.get()
+ .uri("/json")
.accept(MediaType.APPLICATION_JSON)
- .build();
-
- Mono result = this.webClient
- .exchange(request)
- .then(response -> response.body(toMono(String.class)));
+ .exchange()
+ .then(response -> response.bodyToMono(String.class));
StepVerifier.create(result)
.expectNext(content)
@@ -139,17 +144,14 @@ public class WebClientIntegrationTests {
@Test
public void jsonPojoMono() throws Exception {
- HttpUrl baseUrl = server.url("/pojo");
this.server.enqueue(new MockResponse().setHeader("Content-Type", "application/json")
.setBody("{\"bar\":\"barbar\",\"foo\":\"foofoo\"}"));
- ClientRequest request = ClientRequest.GET(baseUrl.toString())
+ Mono result = this.operations.get()
+ .uri("/pojo")
.accept(MediaType.APPLICATION_JSON)
- .build();
-
- Mono result = this.webClient
- .exchange(request)
- .then(response -> response.body(toMono(Pojo.class)));
+ .exchange()
+ .then(response -> response.bodyToMono(Pojo.class));
StepVerifier.create(result)
.consumeNextWith(p -> assertEquals("barbar", p.getBar()))
@@ -164,17 +166,14 @@ public class WebClientIntegrationTests {
@Test
public void jsonPojoFlux() throws Exception {
- HttpUrl baseUrl = server.url("/pojos");
this.server.enqueue(new MockResponse().setHeader("Content-Type", "application/json")
.setBody("[{\"bar\":\"bar1\",\"foo\":\"foo1\"},{\"bar\":\"bar2\",\"foo\":\"foo2\"}]"));
- ClientRequest request = ClientRequest.GET(baseUrl.toString())
+ Flux result = this.operations.get()
+ .uri("/pojos")
.accept(MediaType.APPLICATION_JSON)
- .build();
-
- Flux result = this.webClient
- .exchange(request)
- .flatMap(response -> response.body(toFlux(Pojo.class)));
+ .exchange()
+ .flatMap(response -> response.bodyToFlux(Pojo.class));
StepVerifier.create(result)
.consumeNextWith(p -> assertThat(p.getBar(), Matchers.is("bar1")))
@@ -190,20 +189,16 @@ public class WebClientIntegrationTests {
@Test
public void postJsonPojo() throws Exception {
- HttpUrl baseUrl = server.url("/pojo/capitalize");
this.server.enqueue(new MockResponse()
.setHeader("Content-Type", "application/json")
.setBody("{\"bar\":\"BARBAR\",\"foo\":\"FOOFOO\"}"));
- Pojo spring = new Pojo("foofoo", "barbar");
- ClientRequest request = ClientRequest.POST(baseUrl.toString())
+ Mono result = this.operations.post()
+ .uri("/pojo/capitalize")
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
- .body(BodyInserters.fromObject(spring));
-
- Mono result = this.webClient
- .exchange(request)
- .then(response -> response.body(BodyExtractors.toMono(Pojo.class)));
+ .exchange(fromObject(new Pojo("foofoo", "barbar")))
+ .then(response -> response.bodyToMono(Pojo.class));
StepVerifier.create(result)
.consumeNextWith(p -> assertEquals("BARBAR", p.getBar()))
@@ -221,17 +216,14 @@ public class WebClientIntegrationTests {
@Test
public void cookies() throws Exception {
- HttpUrl baseUrl = server.url("/test");
this.server.enqueue(new MockResponse()
.setHeader("Content-Type", "text/plain").setBody("test"));
- ClientRequest request = ClientRequest.GET(baseUrl.toString())
+ Mono result = this.operations.get()
+ .uri("/test")
.cookie("testkey", "testvalue")
- .build();
-
- Mono result = this.webClient
- .exchange(request)
- .then(response -> response.body(toMono(String.class)));
+ .exchange()
+ .then(response -> response.bodyToMono(String.class));
StepVerifier.create(result)
.expectNext("test")
@@ -246,19 +238,13 @@ public class WebClientIntegrationTests {
@Test
public void notFound() throws Exception {
- HttpUrl baseUrl = server.url("/greeting?name=Spring");
this.server.enqueue(new MockResponse().setResponseCode(404)
.setHeader("Content-Type", "text/plain").setBody("Not Found"));
- ClientRequest request = ClientRequest.GET(baseUrl.toString()).build();
-
- Mono result = this.webClient
- .exchange(request);
+ Mono result = this.operations.get().uri("/greeting?name=Spring").exchange();
StepVerifier.create(result)
- .consumeNextWith(response -> {
- assertEquals(HttpStatus.NOT_FOUND, response.statusCode());
- })
+ .consumeNextWith(response -> assertEquals(HttpStatus.NOT_FOUND, response.statusCode()))
.expectComplete()
.verify(Duration.ofSeconds(3));
@@ -270,21 +256,18 @@ public class WebClientIntegrationTests {
@Test
public void buildFilter() throws Exception {
- HttpUrl baseUrl = server.url("/greeting?name=Spring");
this.server.enqueue(new MockResponse().setHeader("Content-Type", "text/plain").setBody("Hello Spring!"));
- ExchangeFilterFunction filter = (request, next) -> {
- ClientRequest> filteredRequest = ClientRequest.from(request)
- .header("foo", "bar").build();
- return next.exchange(filteredRequest);
- };
- WebClient filteredClient = WebClient.builder(new ReactorClientHttpConnector())
- .filter(filter).build();
-
- ClientRequest request = ClientRequest.GET(baseUrl.toString()).build();
+ WebClientOperations filteredClient = this.operations.filter(
+ (request, next) -> {
+ ClientRequest> filteredRequest = ClientRequest.from(request).header("foo", "bar").build();
+ return next.exchange(filteredRequest);
+ });
- Mono result = filteredClient.exchange(request)
- .then(response -> response.body(toMono(String.class)));
+ Mono result = filteredClient.get()
+ .uri("/greeting?name=Spring")
+ .exchange()
+ .then(response -> response.bodyToMono(String.class));
StepVerifier.create(result)
.expectNext("Hello Spring!")
@@ -299,21 +282,18 @@ public class WebClientIntegrationTests {
@Test
public void filter() throws Exception {
- HttpUrl baseUrl = server.url("/greeting?name=Spring");
this.server.enqueue(new MockResponse().setHeader("Content-Type", "text/plain").setBody("Hello Spring!"));
- ExchangeFilterFunction filter = (request, next) -> {
- ClientRequest> filteredRequest = ClientRequest.from(request)
- .header("foo", "bar").build();
- return next.exchange(filteredRequest);
- };
- WebClient client = WebClient.create(new ReactorClientHttpConnector());
- WebClient filteredClient = client.filter(filter);
+ WebClientOperations filteredClient = this.operations.filter(
+ (request, next) -> {
+ ClientRequest> filteredRequest = ClientRequest.from(request).header("foo", "bar").build();
+ return next.exchange(filteredRequest);
+ });
- ClientRequest request = ClientRequest.GET(baseUrl.toString()).build();
-
- Mono result = filteredClient.exchange(request)
- .then(response -> response.body(toMono(String.class)));
+ Mono result = filteredClient.get()
+ .uri("/greeting?name=Spring")
+ .exchange()
+ .then(response -> response.bodyToMono(String.class));
StepVerifier.create(result)
.expectNext("Hello Spring!")
@@ -326,8 +306,4 @@ public class WebClientIntegrationTests {
}
- @After
- public void tearDown() throws Exception {
- this.server.shutdown();
- }
}
\ No newline at end of file
diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/server/SseHandlerFunctionIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/server/SseHandlerFunctionIntegrationTests.java
index 23618990b5c..e58fc194310 100644
--- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/server/SseHandlerFunctionIntegrationTests.java
+++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/server/SseHandlerFunctionIntegrationTests.java
@@ -18,22 +18,24 @@ package org.springframework.web.reactive.function.server;
import java.time.Duration;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
import org.junit.Before;
import org.junit.Test;
-import static org.springframework.http.MediaType.TEXT_EVENT_STREAM;
-import static org.springframework.web.reactive.function.BodyExtractors.toFlux;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
-import org.springframework.core.ResolvableType;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.http.codec.ServerSentEvent;
-import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.WebClient;
+import org.springframework.web.reactive.function.client.WebClientOperations;
+import org.springframework.web.util.DefaultUriBuilderFactory;
+import org.springframework.web.util.UriBuilderFactory;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.springframework.core.ResolvableType.forClassWithGenerics;
+import static org.springframework.http.MediaType.TEXT_EVENT_STREAM;
+import static org.springframework.web.reactive.function.BodyExtractors.toFlux;
import static org.springframework.web.reactive.function.BodyInserters.fromServerSentEvents;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
@@ -42,12 +44,15 @@ import static org.springframework.web.reactive.function.server.RouterFunctions.r
*/
public class SseHandlerFunctionIntegrationTests extends AbstractRouterFunctionIntegrationTests {
+ private WebClientOperations operations;
- private WebClient webClient;
@Before
- public void createWebClient() {
- this.webClient = WebClient.create(new ReactorClientHttpConnector());
+ public void setup() throws Exception {
+ super.setup();
+ WebClient client = WebClient.create(new ReactorClientHttpConnector());
+ UriBuilderFactory factory = new DefaultUriBuilderFactory("http://localhost:" + this.port);
+ this.operations = WebClientOperations.builder(client).uriBuilderFactory(factory).build();
}
@Override
@@ -60,13 +65,10 @@ public class SseHandlerFunctionIntegrationTests extends AbstractRouterFunctionIn
@Test
public void sseAsString() throws Exception {
- ClientRequest request = ClientRequest
- .GET("http://localhost:{port}/string", this.port)
- .accept(TEXT_EVENT_STREAM)
- .build();
-
- Flux result = this.webClient
- .exchange(request)
+ Flux result = this.operations.get()
+ .uri("/string")
+ .accept(TEXT_EVENT_STREAM)
+ .exchange()
.flatMap(response -> response.body(toFlux(String.class)));
StepVerifier.create(result)
@@ -77,14 +79,10 @@ public class SseHandlerFunctionIntegrationTests extends AbstractRouterFunctionIn
}
@Test
public void sseAsPerson() throws Exception {
- ClientRequest request =
- ClientRequest
- .GET("http://localhost:{port}/person", this.port)
- .accept(TEXT_EVENT_STREAM)
- .build();
-
- Flux result = this.webClient
- .exchange(request)
+ Flux result = this.operations.get()
+ .uri("/person")
+ .accept(TEXT_EVENT_STREAM)
+ .exchange()
.flatMap(response -> response.body(toFlux(Person.class)));
StepVerifier.create(result)
@@ -96,16 +94,12 @@ public class SseHandlerFunctionIntegrationTests extends AbstractRouterFunctionIn
@Test
public void sseAsEvent() throws Exception {
- ClientRequest request =
- ClientRequest
- .GET("http://localhost:{port}/event", this.port)
- .accept(TEXT_EVENT_STREAM)
- .build();
-
- ResolvableType type = ResolvableType.forClassWithGenerics(ServerSentEvent.class, String.class);
- Flux> result = this.webClient
- .exchange(request)
- .flatMap(response -> response.body(toFlux(type)));
+ Flux> result = this.operations.get()
+ .uri("/event")
+ .accept(TEXT_EVENT_STREAM)
+ .exchange()
+ .flatMap(response -> response.body(toFlux(
+ forClassWithGenerics(ServerSentEvent.class, String.class))));
StepVerifier.create(result)
.consumeNextWith( event -> {
diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java
index 676f30c5648..83387cc0a25 100644
--- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java
+++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java
@@ -18,14 +18,9 @@ package org.springframework.web.reactive.result.method.annotation;
import java.time.Duration;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
import org.junit.Before;
import org.junit.Test;
-import static org.springframework.http.MediaType.TEXT_EVENT_STREAM;
-import static org.springframework.web.reactive.function.BodyExtractors.toFlux;
import reactor.core.publisher.Flux;
-
import reactor.test.StepVerifier;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
@@ -40,9 +35,17 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.DispatcherHandler;
import org.springframework.web.reactive.config.EnableWebReactive;
-import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.WebClient;
+import org.springframework.web.reactive.function.client.WebClientOperations;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
+import org.springframework.web.util.DefaultUriBuilderFactory;
+import org.springframework.web.util.UriBuilderFactory;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.springframework.core.ResolvableType.forClassWithGenerics;
+import static org.springframework.http.MediaType.TEXT_EVENT_STREAM;
+import static org.springframework.web.reactive.function.BodyExtractors.toFlux;
/**
@@ -52,14 +55,16 @@ public class SseIntegrationTests extends AbstractHttpHandlerIntegrationTests {
private AnnotationConfigApplicationContext wac;
- private WebClient webClient;
+ private WebClientOperations operations;
@Override
@Before
public void setup() throws Exception {
super.setup();
- this.webClient = WebClient.create(new ReactorClientHttpConnector());
+ WebClient client = WebClient.create(new ReactorClientHttpConnector());
+ UriBuilderFactory factory = new DefaultUriBuilderFactory("http://localhost:" + this.port + "/sse");
+ this.operations = WebClientOperations.builder(client).uriBuilderFactory(factory).build();
}
@@ -74,14 +79,11 @@ public class SseIntegrationTests extends AbstractHttpHandlerIntegrationTests {
@Test
public void sseAsString() throws Exception {
- ClientRequest request = ClientRequest
- .GET("http://localhost:{port}/sse/string", this.port)
- .accept(TEXT_EVENT_STREAM)
- .build();
-
- Flux result = this.webClient
- .exchange(request)
- .flatMap(response -> response.body(toFlux(String.class)));
+ Flux result = this.operations.get()
+ .uri("/string")
+ .accept(TEXT_EVENT_STREAM)
+ .exchange()
+ .flatMap(response -> response.bodyToFlux(String.class));
StepVerifier.create(result)
.expectNext("foo 0")
@@ -91,15 +93,11 @@ public class SseIntegrationTests extends AbstractHttpHandlerIntegrationTests {
}
@Test
public void sseAsPerson() throws Exception {
- ClientRequest request =
- ClientRequest
- .GET("http://localhost:{port}/sse/person", this.port)
- .accept(TEXT_EVENT_STREAM)
- .build();
-
- Flux result = this.webClient
- .exchange(request)
- .flatMap(response -> response.body(toFlux(Person.class)));
+ Flux result = this.operations.get()
+ .uri("/person")
+ .accept(TEXT_EVENT_STREAM)
+ .exchange()
+ .flatMap(response -> response.bodyToFlux(Person.class));
StepVerifier.create(result)
.expectNext(new Person("foo 0"))
@@ -110,15 +108,11 @@ public class SseIntegrationTests extends AbstractHttpHandlerIntegrationTests {
@Test
public void sseAsEvent() throws Exception {
- ClientRequest request =
- ClientRequest
- .GET("http://localhost:{port}/sse/event", this.port)
- .accept(TEXT_EVENT_STREAM)
- .build();
-
- ResolvableType type = ResolvableType.forClassWithGenerics(ServerSentEvent.class, String.class);
- Flux> result = this.webClient
- .exchange(request)
+ ResolvableType type = forClassWithGenerics(ServerSentEvent.class, String.class);
+ Flux> result = this.operations.get()
+ .uri("/event")
+ .accept(TEXT_EVENT_STREAM)
+ .exchange()
.flatMap(response -> response.body(toFlux(type)));
StepVerifier.create(result)
@@ -142,15 +136,12 @@ public class SseIntegrationTests extends AbstractHttpHandlerIntegrationTests {
@Test
public void sseAsEventWithoutAcceptHeader() throws Exception {
- ClientRequest request =
- ClientRequest
- .GET("http://localhost:{port}/sse/event", this.port)
+ Flux> result = this.operations.get()
+ .uri("/event")
.accept(TEXT_EVENT_STREAM)
- .build();
-
- Flux> result = this.webClient
- .exchange(request)
- .flatMap(response -> response.body(toFlux(ResolvableType.forClassWithGenerics(ServerSentEvent.class, String.class))));
+ .exchange()
+ .flatMap(response -> response.body(toFlux(
+ forClassWithGenerics(ServerSentEvent.class, String.class))));
StepVerifier.create(result)
.consumeNextWith( event -> {
diff --git a/spring-web/src/main/java/org/springframework/web/client/AsyncRestTemplate.java b/spring-web/src/main/java/org/springframework/web/client/AsyncRestTemplate.java
index 351c46e7ad5..00c9a87677a 100644
--- a/spring-web/src/main/java/org/springframework/web/client/AsyncRestTemplate.java
+++ b/spring-web/src/main/java/org/springframework/web/client/AsyncRestTemplate.java
@@ -45,6 +45,7 @@ import org.springframework.util.Assert;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.util.concurrent.ListenableFutureAdapter;
import org.springframework.web.util.AbstractUriTemplateHandler;
+import org.springframework.web.util.DefaultUriBuilderFactory;
import org.springframework.web.util.UriTemplateHandler;
/**
@@ -163,9 +164,16 @@ public class AsyncRestTemplate extends InterceptingAsyncHttpAccessor implements
*/
public void setDefaultUriVariables(Map defaultUriVariables) {
UriTemplateHandler handler = this.syncTemplate.getUriTemplateHandler();
- Assert.isInstanceOf(AbstractUriTemplateHandler.class, handler,
- "Can only use this property in conjunction with a DefaultUriTemplateHandler");
- ((AbstractUriTemplateHandler) handler).setDefaultUriVariables(defaultUriVariables);
+ if (handler instanceof DefaultUriBuilderFactory) {
+ ((DefaultUriBuilderFactory) handler).setDefaultUriVariables(defaultUriVariables);
+ }
+ else if (handler instanceof AbstractUriTemplateHandler) {
+ ((AbstractUriTemplateHandler) handler).setDefaultUriVariables(defaultUriVariables);
+ }
+ else {
+ throw new IllegalArgumentException(
+ "This property is not supported with the configured UriTemplateHandler.");
+ }
}
/**
diff --git a/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java b/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java
index 316a5d2c809..22721dc2128 100644
--- a/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java
+++ b/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java
@@ -53,7 +53,7 @@ import org.springframework.http.converter.xml.SourceHttpMessageConverter;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.web.util.AbstractUriTemplateHandler;
-import org.springframework.web.util.DefaultUriTemplateHandler;
+import org.springframework.web.util.DefaultUriBuilderFactory;
import org.springframework.web.util.UriTemplateHandler;
/**
@@ -149,7 +149,7 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
private ResponseErrorHandler errorHandler = new DefaultResponseErrorHandler();
- private UriTemplateHandler uriTemplateHandler = new DefaultUriTemplateHandler();
+ private UriTemplateHandler uriTemplateHandler = new DefaultUriBuilderFactory();
private final ResponseExtractor headersExtractor = new HeadersExtractor();
@@ -254,24 +254,31 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
/**
* Configure default URI variable values. This is a shortcut for:
*
- * DefaultUriTemplateHandler handler = new DefaultUriTemplateHandler();
+ * DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory();
* handler.setDefaultUriVariables(...);
*
* RestTemplate restTemplate = new RestTemplate();
* restTemplate.setUriTemplateHandler(handler);
*
- * @param defaultUriVariables the default URI variable values
+ * @param uriVars the default URI variable values
* @since 4.3
*/
- public void setDefaultUriVariables(Map defaultUriVariables) {
- Assert.isInstanceOf(AbstractUriTemplateHandler.class, this.uriTemplateHandler,
- "Can only use this property in conjunction with an AbstractUriTemplateHandler");
- ((AbstractUriTemplateHandler) this.uriTemplateHandler).setDefaultUriVariables(defaultUriVariables);
+ public void setDefaultUriVariables(Map uriVars) {
+ if (this.uriTemplateHandler instanceof DefaultUriBuilderFactory) {
+ ((DefaultUriBuilderFactory) this.uriTemplateHandler).setDefaultUriVariables(uriVars);
+ }
+ else if (this.uriTemplateHandler instanceof AbstractUriTemplateHandler) {
+ ((AbstractUriTemplateHandler) this.uriTemplateHandler).setDefaultUriVariables(uriVars);
+ }
+ else {
+ throw new IllegalArgumentException(
+ "This property is not supported with the configured UriTemplateHandler.");
+ }
}
/**
* Configure the {@link UriTemplateHandler} to use to expand URI templates.
- * By default the {@link DefaultUriTemplateHandler} is used which relies on
+ * By default the {@link DefaultUriBuilderFactory} is used which relies on
* Spring's URI template support and exposes several useful properties that
* customize its behavior for encoding and for prepending a common base URL.
* An alternative implementation may be used to plug an external URI
diff --git a/spring-web/src/main/java/org/springframework/web/util/AbstractUriTemplateHandler.java b/spring-web/src/main/java/org/springframework/web/util/AbstractUriTemplateHandler.java
index b4a8cbb3186..d8a88c46375 100644
--- a/spring-web/src/main/java/org/springframework/web/util/AbstractUriTemplateHandler.java
+++ b/spring-web/src/main/java/org/springframework/web/util/AbstractUriTemplateHandler.java
@@ -33,7 +33,9 @@ import org.springframework.util.Assert;
*
* @author Rossen Stoyanchev
* @since 4.3
+ * @deprecated as of 5.0 in favor of {@link DefaultUriBuilderFactory}
*/
+@Deprecated
public abstract class AbstractUriTemplateHandler implements UriTemplateHandler {
private String baseUrl;
diff --git a/spring-web/src/main/java/org/springframework/web/util/DefaultUriBuilderFactory.java b/spring-web/src/main/java/org/springframework/web/util/DefaultUriBuilderFactory.java
new file mode 100644
index 00000000000..8daaf768a01
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/web/util/DefaultUriBuilderFactory.java
@@ -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.
+ *
+ * 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 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".
+ * 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}.
+ *
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 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);
+ }
+
+ /**
+ * Specify the encoding mode to use when building URIs:
+ *
+ * - 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.
+ *
- 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
+ * RFC 3986 Section 2.
+ * This ensures a URI variable value will not contain any characters with a
+ * reserved purpose.
+ *
- NONE -- in this mode no encoding is performed.
+ *
+ * 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 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 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 params) {
+ this.uriComponentsBuilder.queryParams(params);
+ return this;
+ }
+
+ @Override
+ public DefaultUriBuilder replaceQueryParams(MultiValueMap params) {
+ this.uriComponentsBuilder.replaceQueryParams(params);
+ return this;
+ }
+
+ @Override
+ public DefaultUriBuilder fragment(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 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());
+ }
+ }
+
+}
diff --git a/spring-web/src/main/java/org/springframework/web/util/DefaultUriTemplateHandler.java b/spring-web/src/main/java/org/springframework/web/util/DefaultUriTemplateHandler.java
index 8c0ad71504f..9ad9357a30c 100644
--- a/spring-web/src/main/java/org/springframework/web/util/DefaultUriTemplateHandler.java
+++ b/spring-web/src/main/java/org/springframework/web/util/DefaultUriTemplateHandler.java
@@ -33,7 +33,9 @@ import java.util.Map;
*
* @author Rossen Stoyanchev
* @since 4.2
+ * @deprecated as of 5.0 in favor of {@link DefaultUriBuilderFactory}
*/
+@Deprecated
public class DefaultUriTemplateHandler extends AbstractUriTemplateHandler {
private boolean parsePath;
diff --git a/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java b/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java
index 0912430d079..98c9a7cdd15 100644
--- a/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java
+++ b/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java
@@ -446,14 +446,28 @@ 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());
+ }
+ // Avoid parsing the port, may have URI variable..
+ if (this.port != null) {
+ builder.port(this.port);
+ }
+ if (getPath() != null) {
+ this.path.copyToUriComponentsBuilder(builder);
+ }
+ if (!getQueryParams().isEmpty()) {
+ builder.queryParams(getQueryParams());
+ }
+ if (getFragment() != null) {
+ builder.fragment(getFragment());
+ }
}
diff --git a/spring-web/src/main/java/org/springframework/web/util/OpaqueUriComponents.java b/spring-web/src/main/java/org/springframework/web/util/OpaqueUriComponents.java
index bf3c7e34954..f0e1c295754 100644
--- a/spring-web/src/main/java/org/springframework/web/util/OpaqueUriComponents.java
+++ b/spring-web/src/main/java/org/springframework/web/util/OpaqueUriComponents.java
@@ -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());
+ }
}
diff --git a/spring-web/src/main/java/org/springframework/web/util/UriBuilder.java b/spring-web/src/main/java/org/springframework/web/util/UriBuilder.java
new file mode 100644
index 00000000000..2388d325812
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/web/util/UriBuilder.java
@@ -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.
+ *
+ * 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.
+ *
+ *
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.
+ *
Note: 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:
+ *
+ * builder.query("filter={value}").uriString("hot&cold");
+ *
+ * @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 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 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 uriVariables);
+
+}
diff --git a/spring-web/src/main/java/org/springframework/web/util/UriBuilderFactory.java b/spring-web/src/main/java/org/springframework/web/util/UriBuilderFactory.java
new file mode 100644
index 00000000000..504a7d50920
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/web/util/UriBuilderFactory.java
@@ -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}.
+ *
+ * A single {@link UriBuilderFactory} may be created once, configured with
+ * common properties such as a base URI, and then used to create many URIs.
+ *
+ *
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.
+ *
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);
+
+}
diff --git a/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java b/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java
index 09495d39777..9f7694c3265 100644
--- a/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java
+++ b/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java
@@ -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 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
*/
@@ -412,24 +436,25 @@ public class UriComponentsBuilder implements Cloneable {
}
/**
- * 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.
- * @param scheme the URI scheme
+ * Initialize components of this {@link UriComponentsBuilder} from the
+ * components of the given {@link UriComponents}.
+ * @param uriComponents the UriComponents instance
* @return this UriComponentsBuilder
*/
- public UriComponentsBuilder scheme(String scheme) {
- this.scheme = scheme;
+ public UriComponentsBuilder uriComponents(UriComponents uriComponents) {
+ Assert.notNull(uriComponents, "UriComponents must not be null");
+ uriComponents.copyToUriComponentsBuilder(this);
return this;
}
/**
- * Set all components of this URI builder from the given {@link UriComponents}.
- * @param uriComponents the UriComponents instance
+ * 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.
+ * @param scheme the URI scheme
* @return this UriComponentsBuilder
*/
- public UriComponentsBuilder uriComponents(UriComponents uriComponents) {
- Assert.notNull(uriComponents, "UriComponents must not be null");
- uriComponents.copyToUriComponentsBuilder(this);
+ public UriComponentsBuilder scheme(String scheme) {
+ this.scheme = scheme;
return this;
}
diff --git a/spring-web/src/main/java/org/springframework/web/util/UriTemplateHandler.java b/spring-web/src/main/java/org/springframework/web/util/UriTemplateHandler.java
index c03eb191ab4..d394d34f31a 100644
--- a/spring-web/src/main/java/org/springframework/web/util/UriTemplateHandler.java
+++ b/spring-web/src/main/java/org/springframework/web/util/UriTemplateHandler.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2016 the original author or authors.
+ * 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.
@@ -20,18 +20,14 @@ import java.net.URI;
import java.util.Map;
/**
- * Strategy for expanding a URI template with full control over the URI template
- * syntax and the encoding of variables. Also a convenient central point for
- * pre-processing all URI templates for example to insert a common base path.
+ * Strategy for expanding a URI template.
*
* Supported as a property on the {@code RestTemplate} as well as the
- * {@code AsyncRestTemplate}. The {@link DefaultUriTemplateHandler} is built
- * on Spring's URI template support via {@link UriComponentsBuilder}. An
- * alternative implementation may be used to plug external URI template libraries.
+ * {@code AsyncRestTemplate}.
*
* @author Rossen Stoyanchev
* @since 4.2
- * @see org.springframework.web.client.RestTemplate#setUriTemplateHandler
+ * @see DefaultUriBuilderFactory
*/
public interface UriTemplateHandler {
diff --git a/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java b/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java
index 1b3f4fe24c1..1ada39213a4 100644
--- a/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java
+++ b/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java
@@ -42,7 +42,7 @@ import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
-import org.springframework.web.util.DefaultUriTemplateHandler;
+import org.springframework.web.util.DefaultUriBuilderFactory;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -83,7 +83,7 @@ public class RestTemplateTests {
response = mock(ClientHttpResponse.class);
errorHandler = mock(ResponseErrorHandler.class);
converter = mock(HttpMessageConverter.class);
- template = new RestTemplate(Collections.>singletonList(converter));
+ template = new RestTemplate(Collections.singletonList(converter));
template.setRequestFactory(requestFactory);
template.setErrorHandler(errorHandler);
}
@@ -273,8 +273,7 @@ public class RestTemplateTests {
@Test
public void getForObjectWithCustomUriTemplateHandler() throws Exception {
- DefaultUriTemplateHandler uriTemplateHandler = new DefaultUriTemplateHandler();
- uriTemplateHandler.setParsePath(true);
+ DefaultUriBuilderFactory uriTemplateHandler = new DefaultUriBuilderFactory();
template.setUriTemplateHandler(uriTemplateHandler);
URI expectedUri = new URI("http://example.com/hotels/1/pic/pics%2Flogo.png/size/150x150");
diff --git a/spring-web/src/test/java/org/springframework/web/util/DefaultUriBuilderFactoryTests.java b/spring-web/src/test/java/org/springframework/web/util/DefaultUriBuilderFactoryTests.java
new file mode 100644
index 00000000000..c7481d7de51
--- /dev/null
+++ b/spring-web/src/test/java/org/springframework/web/util/DefaultUriBuilderFactoryTests.java
@@ -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 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());
+ }
+
+}