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()); + } + +}