diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java index 0eefdceeb8f..92f0f8ba936 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java @@ -34,6 +34,7 @@ import reactor.core.publisher.Mono; import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.http.client.reactive.ClientHttpConnector; import org.springframework.http.client.reactive.ClientHttpRequest; @@ -97,42 +98,45 @@ class DefaultWebTestClient implements WebTestClient { @Override - public UriSpec get() { - return toUriSpec(WebClient::get); + public UriSpec> get() { + return toUriSpec(wc -> wc.method(HttpMethod.GET)); } @Override - public UriSpec head() { - return toUriSpec(WebClient::head); + public UriSpec> head() { + return toUriSpec(wc -> wc.method(HttpMethod.HEAD)); } @Override - public UriSpec post() { - return toUriSpec(WebClient::post); + public UriSpec post() { + return toUriSpec(wc -> wc.method(HttpMethod.POST)); } @Override - public UriSpec put() { - return toUriSpec(WebClient::put); + public UriSpec put() { + return toUriSpec(wc -> wc.method(HttpMethod.PUT)); } @Override - public UriSpec patch() { - return toUriSpec(WebClient::patch); + public UriSpec patch() { + return toUriSpec(wc -> wc.method(HttpMethod.PATCH)); } @Override - public UriSpec delete() { - return toUriSpec(WebClient::delete); + public UriSpec> delete() { + return toUriSpec(wc -> wc.method(HttpMethod.DELETE)); } @Override - public UriSpec options() { - return toUriSpec(WebClient::options); + public UriSpec> options() { + return toUriSpec(wc -> wc.method(HttpMethod.OPTIONS)); } - private UriSpec toUriSpec(Function function) { - return new DefaultUriSpec(function.apply(this.webClient)); + @SuppressWarnings("unchecked") + private > UriSpec toUriSpec( + Function> function) { + + return new DefaultUriSpec<>(function.apply(this.webClient)); } @@ -156,123 +160,132 @@ class DefaultWebTestClient implements WebTestClient { } - private class DefaultUriSpec implements UriSpec { + @SuppressWarnings("unchecked") + private class DefaultUriSpec> implements UriSpec { - private final WebClient.UriSpec uriSpec; + private final WebClient.UriSpec uriSpec; - DefaultUriSpec(WebClient.UriSpec spec) { + DefaultUriSpec(WebClient.UriSpec spec) { this.uriSpec = spec; } @Override - public HeaderSpec uri(URI uri) { - return new DefaultHeaderSpec(this.uriSpec.uri(uri)); + public S uri(URI uri) { + return (S) new DefaultRequestBodySpec(this.uriSpec.uri(uri)); } @Override - public HeaderSpec uri(String uriTemplate, Object... uriVariables) { - return new DefaultHeaderSpec(this.uriSpec.uri(uriTemplate, uriVariables)); + public S uri(String uriTemplate, Object... uriVariables) { + return (S) new DefaultRequestBodySpec(this.uriSpec.uri(uriTemplate, uriVariables)); } @Override - public HeaderSpec uri(String uriTemplate, Map uriVariables) { - return new DefaultHeaderSpec(this.uriSpec.uri(uriTemplate, uriVariables)); + public S uri(String uriTemplate, Map uriVariables) { + return (S) new DefaultRequestBodySpec(this.uriSpec.uri(uriTemplate, uriVariables)); } @Override - public HeaderSpec uri(Function uriBuilder) { - return new DefaultHeaderSpec(this.uriSpec.uri(uriBuilder)); + public S uri(Function uriBuilder) { + return (S) new DefaultRequestBodySpec(this.uriSpec.uri(uriBuilder)); } } - private class DefaultHeaderSpec implements WebTestClient.HeaderSpec { + private class DefaultRequestBodySpec implements RequestBodySpec { - private final WebClient.HeaderSpec headerSpec; + private final WebClient.RequestBodySpec bodySpec; private final String requestId; - DefaultHeaderSpec(WebClient.HeaderSpec spec) { - this.headerSpec = spec; + DefaultRequestBodySpec(WebClient.RequestBodySpec spec) { + this.bodySpec = spec; this.requestId = String.valueOf(requestIndex.incrementAndGet()); - this.headerSpec.header(WiretapConnector.REQUEST_ID_HEADER_NAME, this.requestId); + this.bodySpec.header(WiretapConnector.REQUEST_ID_HEADER_NAME, this.requestId); } @Override - public DefaultHeaderSpec header(String headerName, String... headerValues) { - this.headerSpec.header(headerName, headerValues); + public RequestBodySpec header(String headerName, String... headerValues) { + this.bodySpec.header(headerName, headerValues); return this; } @Override - public DefaultHeaderSpec headers(HttpHeaders headers) { - this.headerSpec.headers(headers); + public RequestBodySpec headers(HttpHeaders headers) { + this.bodySpec.headers(headers); return this; } @Override - public DefaultHeaderSpec accept(MediaType... acceptableMediaTypes) { - this.headerSpec.accept(acceptableMediaTypes); + public RequestBodySpec accept(MediaType... acceptableMediaTypes) { + this.bodySpec.accept(acceptableMediaTypes); return this; } @Override - public DefaultHeaderSpec acceptCharset(Charset... acceptableCharsets) { - this.headerSpec.acceptCharset(acceptableCharsets); + public RequestBodySpec acceptCharset(Charset... acceptableCharsets) { + this.bodySpec.acceptCharset(acceptableCharsets); return this; } @Override - public DefaultHeaderSpec contentType(MediaType contentType) { - this.headerSpec.contentType(contentType); + public RequestBodySpec contentType(MediaType contentType) { + this.bodySpec.contentType(contentType); return this; } @Override - public DefaultHeaderSpec contentLength(long contentLength) { - this.headerSpec.contentLength(contentLength); + public RequestBodySpec contentLength(long contentLength) { + this.bodySpec.contentLength(contentLength); return this; } @Override - public DefaultHeaderSpec cookie(String name, String value) { - this.headerSpec.cookie(name, value); + public RequestBodySpec cookie(String name, String value) { + this.bodySpec.cookie(name, value); return this; } @Override - public DefaultHeaderSpec cookies(MultiValueMap cookies) { - this.headerSpec.cookies(cookies); + public RequestBodySpec cookies(MultiValueMap cookies) { + this.bodySpec.cookies(cookies); return this; } @Override - public DefaultHeaderSpec ifModifiedSince(ZonedDateTime ifModifiedSince) { - this.headerSpec.ifModifiedSince(ifModifiedSince); + public RequestBodySpec ifModifiedSince(ZonedDateTime ifModifiedSince) { + this.bodySpec.ifModifiedSince(ifModifiedSince); return this; } @Override - public DefaultHeaderSpec ifNoneMatch(String... ifNoneMatches) { - this.headerSpec.ifNoneMatch(ifNoneMatches); + public RequestBodySpec ifNoneMatch(String... ifNoneMatches) { + this.bodySpec.ifNoneMatch(ifNoneMatches); return this; } @Override public ResponseSpec exchange() { - return toResponseSpec(this.headerSpec.exchange()); + return toResponseSpec(this.bodySpec.exchange()); } @Override - public ResponseSpec exchange(BodyInserter inserter) { - return toResponseSpec(this.headerSpec.exchange(inserter)); + public RequestHeadersSpec body(BodyInserter inserter) { + this.bodySpec.body(inserter); + return this; } @Override - public > ResponseSpec exchange(S publisher, Class elementClass) { - return toResponseSpec(this.headerSpec.exchange(publisher, elementClass)); + public > RequestHeadersSpec body(S publisher, Class elementClass) { + this.bodySpec.body(publisher, elementClass); + return this; + } + + @Override + public RequestHeadersSpec body(T body) { + this.bodySpec.body(body); + return this; } private DefaultResponseSpec toResponseSpec(Mono mono) { diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java index 8b1750ecc12..155107aa67a 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java @@ -78,43 +78,43 @@ public interface WebTestClient { * Prepare an HTTP GET request. * @return a spec for specifying the target URL */ - UriSpec get(); + UriSpec> get(); /** * Prepare an HTTP HEAD request. * @return a spec for specifying the target URL */ - UriSpec head(); + UriSpec> head(); /** * Prepare an HTTP POST request. * @return a spec for specifying the target URL */ - UriSpec post(); + UriSpec post(); /** * Prepare an HTTP PUT request. * @return a spec for specifying the target URL */ - UriSpec put(); + UriSpec put(); /** * Prepare an HTTP PATCH request. * @return a spec for specifying the target URL */ - UriSpec patch(); + UriSpec patch(); /** * Prepare an HTTP DELETE request. * @return a spec for specifying the target URL */ - UriSpec delete(); + UriSpec> delete(); /** * Prepare an HTTP OPTIONS request. * @return a spec for specifying the target URL */ - UriSpec options(); + UriSpec> options(); /** @@ -327,13 +327,13 @@ public interface WebTestClient { /** * Specification for providing the URI of a request. */ - interface UriSpec { + interface UriSpec> { /** * Specify the URI using an absolute, fully constructed {@link URI}. * @return spec to add headers or perform the exchange */ - HeaderSpec uri(URI uri); + S uri(URI uri); /** * Specify the URI for the request using a URI template and URI variables. @@ -341,7 +341,7 @@ public interface WebTestClient { * with a base URI) it will be used to expand the URI template. * @return spec to add headers or perform the exchange */ - HeaderSpec uri(String uri, Object... uriVariables); + S uri(String uri, Object... uriVariables); /** * Specify the URI for the request using a URI template and URI variables. @@ -349,21 +349,21 @@ public interface WebTestClient { * with a base URI) it will be used to expand the URI template. * @return spec to add headers or perform the exchange */ - HeaderSpec uri(String uri, Map uriVariables); + S uri(String uri, Map uriVariables); /** * Build the URI for the request with a {@link UriBuilder} obtained * through the {@link UriBuilderFactory} configured for this client. * @return spec to add headers or perform the exchange */ - HeaderSpec uri(Function uriFunction); + S uri(Function uriFunction); } /** * Specification for adding request headers and performing an exchange. */ - interface HeaderSpec { + interface RequestHeadersSpec> { /** * Set the list of acceptable {@linkplain MediaType media types}, as @@ -371,7 +371,7 @@ public interface WebTestClient { * @param acceptableMediaTypes the acceptable media types * @return the same instance */ - HeaderSpec accept(MediaType... acceptableMediaTypes); + S accept(MediaType... acceptableMediaTypes); /** * Set the list of acceptable {@linkplain Charset charsets}, as specified @@ -379,25 +379,7 @@ public interface WebTestClient { * @param acceptableCharsets the acceptable charsets * @return the same instance */ - 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 the same instance - * @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 the same instance - * @see HttpHeaders#setContentType(MediaType) - */ - HeaderSpec contentType(MediaType contentType); + S acceptCharset(Charset... acceptableCharsets); /** * Add a cookie with the given name and value. @@ -405,7 +387,7 @@ public interface WebTestClient { * @param value the cookie value * @return the same instance */ - HeaderSpec cookie(String name, String value); + S cookie(String name, String value); /** * Copy the given cookies into the entity's cookies map. @@ -413,7 +395,7 @@ public interface WebTestClient { * @param cookies the existing cookies to copy from * @return the same instance */ - HeaderSpec cookies(MultiValueMap cookies); + S cookies(MultiValueMap cookies); /** * Set the value of the {@code If-Modified-Since} header. @@ -422,14 +404,14 @@ public interface WebTestClient { * @param ifModifiedSince the new value of the header * @return the same instance */ - HeaderSpec ifModifiedSince(ZonedDateTime ifModifiedSince); + S ifModifiedSince(ZonedDateTime ifModifiedSince); /** * Set the values of the {@code If-None-Match} header. * @param ifNoneMatches the new value of the header * @return the same instance */ - HeaderSpec ifNoneMatch(String... ifNoneMatches); + S ifNoneMatch(String... ifNoneMatches); /** * Add the given, single header value under the given name. @@ -437,14 +419,14 @@ public interface WebTestClient { * @param headerValues the header value(s) * @return the same instance */ - HeaderSpec header(String headerName, String... headerValues); + S header(String headerName, String... headerValues); /** * Copy the given headers into the entity's headers map. * @param headers the existing headers to copy from * @return the same instance */ - HeaderSpec headers(HttpHeaders headers); + S headers(HttpHeaders headers); /** * Perform the exchange without a request body. @@ -452,26 +434,55 @@ public interface WebTestClient { */ ResponseSpec exchange(); + } + + interface RequestBodySpec extends RequestHeadersSpec { /** - * Perform the exchange with the body for the request populated using - * a {@link BodyInserter}. + * Set the length of the body in bytes, as specified by the + * {@code Content-Length} header. + * @param contentLength the content length + * @return the same instance + * @see HttpHeaders#setContentLength(long) + */ + RequestBodySpec 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 the same instance + * @see HttpHeaders#setContentType(MediaType) + */ + RequestBodySpec contentType(MediaType contentType); + + /** + * Set the body of the request to the given {@code BodyInserter}. * @param inserter the inserter * @param the body type, or the the element type (for a stream) * @return spec for decoding the response * @see org.springframework.web.reactive.function.BodyInserters */ - ResponseSpec exchange(BodyInserter inserter); + RequestHeadersSpec body(BodyInserter inserter); /** - * Perform the exchange and use the given {@code Publisher} for the - * request body. + * Set the body of the request to the given {@code Publisher}. * @param publisher the request body data * @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 spec for decoding the response */ - > ResponseSpec exchange(S publisher, Class elementClass); + > RequestHeadersSpec body(S publisher, Class elementClass); + + /** + * Set the body of the request to the given {@code Object} and + * perform the request. + * @param body the {@code Object} to write to the request + * @param the type contained in the body + * @return a {@code Mono} with the response + */ + RequestHeadersSpec body(T body); + } /** diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ResponseEntityTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ResponseEntityTests.java index bc7c71bbb39..e0255eb9dcf 100644 --- a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ResponseEntityTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ResponseEntityTests.java @@ -41,7 +41,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import static org.hamcrest.CoreMatchers.endsWith; -import static org.junit.Assert.assertThat; +import static org.junit.Assert.*; import static org.springframework.http.MediaType.TEXT_EVENT_STREAM; /** @@ -115,7 +115,8 @@ public class ResponseEntityTests { @Test public void postEntity() throws Exception { this.client.post().uri("/persons") - .exchange(Mono.just(new Person("John")), Person.class) + .body(Mono.just(new Person("John")), Person.class) + .exchange() .expectStatus().isCreated() .expectHeader().valueEquals("location", "/persons/John") .expectBody().isEmpty(); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java index 00922bc7cdb..44de4ed7530 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java @@ -36,6 +36,7 @@ import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.reactive.function.BodyInserter; +import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.util.DefaultUriBuilderFactory; import org.springframework.web.util.UriBuilder; import org.springframework.web.util.UriBuilderFactory; @@ -70,42 +71,48 @@ class DefaultWebClient implements WebClient { @Override - public UriSpec get() { - return method(HttpMethod.GET); + public UriSpec> get() { + return methodInternal(HttpMethod.GET); } @Override - public UriSpec head() { - return method(HttpMethod.HEAD); + public UriSpec> head() { + return methodInternal(HttpMethod.HEAD); } @Override - public UriSpec post() { - return method(HttpMethod.POST); + public UriSpec post() { + return methodInternal(HttpMethod.POST); } @Override - public UriSpec put() { - return method(HttpMethod.PUT); + public UriSpec put() { + return methodInternal(HttpMethod.PUT); } @Override - public UriSpec patch() { - return method(HttpMethod.PATCH); + public UriSpec patch() { + return methodInternal(HttpMethod.PATCH); } @Override - public UriSpec delete() { - return method(HttpMethod.DELETE); + public UriSpec> delete() { + return methodInternal(HttpMethod.DELETE); } @Override - public UriSpec options() { - return method(HttpMethod.OPTIONS); + public UriSpec> options() { + return methodInternal(HttpMethod.OPTIONS); } - private UriSpec method(HttpMethod httpMethod) { - return new DefaultUriSpec(httpMethod); + @Override + public UriSpec method(HttpMethod httpMethod) { + return methodInternal(httpMethod); + } + + @SuppressWarnings("unchecked") + private > UriSpec methodInternal(HttpMethod httpMethod) { + return new DefaultUriSpec<>(httpMethod); } @Override @@ -116,7 +123,7 @@ class DefaultWebClient implements WebClient { } - private class DefaultUriSpec implements UriSpec { + private class DefaultUriSpec> implements UriSpec { private final HttpMethod httpMethod; @@ -126,28 +133,29 @@ class DefaultWebClient implements WebClient { } @Override - public HeaderSpec uri(String uriTemplate, Object... uriVariables) { + public S uri(String uriTemplate, Object... uriVariables) { return uri(uriBuilderFactory.expand(uriTemplate, uriVariables)); } @Override - public HeaderSpec uri(String uriTemplate, Map uriVariables) { + public S uri(String uriTemplate, Map uriVariables) { return uri(uriBuilderFactory.expand(uriTemplate, uriVariables)); } @Override - public HeaderSpec uri(Function uriFunction) { + public S uri(Function uriFunction) { return uri(uriFunction.apply(uriBuilderFactory.builder())); } @Override - public HeaderSpec uri(URI uri) { - return new DefaultHeaderSpec(this.httpMethod, uri); + @SuppressWarnings("unchecked") + public S uri(URI uri) { + return (S) new DefaultRequestBodySpec(this.httpMethod, uri); } } - private class DefaultHeaderSpec implements HeaderSpec { + private class DefaultRequestBodySpec implements RequestBodySpec { private final HttpMethod httpMethod; @@ -157,7 +165,9 @@ class DefaultWebClient implements WebClient { private MultiValueMap cookies; - DefaultHeaderSpec(HttpMethod httpMethod, URI uri) { + private BodyInserter inserter; + + DefaultRequestBodySpec(HttpMethod httpMethod, URI uri) { this.httpMethod = httpMethod; this.uri = uri; } @@ -177,7 +187,7 @@ class DefaultWebClient implements WebClient { } @Override - public DefaultHeaderSpec header(String headerName, String... headerValues) { + public DefaultRequestBodySpec header(String headerName, String... headerValues) { for (String headerValue : headerValues) { getHeaders().add(headerName, headerValue); } @@ -185,7 +195,7 @@ class DefaultWebClient implements WebClient { } @Override - public DefaultHeaderSpec headers(HttpHeaders headers) { + public DefaultRequestBodySpec headers(HttpHeaders headers) { if (headers != null) { getHeaders().putAll(headers); } @@ -193,37 +203,37 @@ class DefaultWebClient implements WebClient { } @Override - public DefaultHeaderSpec accept(MediaType... acceptableMediaTypes) { + public DefaultRequestBodySpec accept(MediaType... acceptableMediaTypes) { getHeaders().setAccept(Arrays.asList(acceptableMediaTypes)); return this; } @Override - public DefaultHeaderSpec acceptCharset(Charset... acceptableCharsets) { + public DefaultRequestBodySpec acceptCharset(Charset... acceptableCharsets) { getHeaders().setAcceptCharset(Arrays.asList(acceptableCharsets)); return this; } @Override - public DefaultHeaderSpec contentType(MediaType contentType) { + public DefaultRequestBodySpec contentType(MediaType contentType) { getHeaders().setContentType(contentType); return this; } @Override - public DefaultHeaderSpec contentLength(long contentLength) { + public DefaultRequestBodySpec contentLength(long contentLength) { getHeaders().setContentLength(contentLength); return this; } @Override - public DefaultHeaderSpec cookie(String name, String value) { + public DefaultRequestBodySpec cookie(String name, String value) { getCookies().add(name, value); return this; } @Override - public DefaultHeaderSpec cookies(MultiValueMap cookies) { + public DefaultRequestBodySpec cookies(MultiValueMap cookies) { if (cookies != null) { getCookies().putAll(cookies); } @@ -231,7 +241,7 @@ class DefaultWebClient implements WebClient { } @Override - public DefaultHeaderSpec ifModifiedSince(ZonedDateTime ifModifiedSince) { + public DefaultRequestBodySpec ifModifiedSince(ZonedDateTime ifModifiedSince) { ZonedDateTime gmt = ifModifiedSince.withZoneSameInstant(ZoneId.of("GMT")); String headerValue = DateTimeFormatter.RFC_1123_DATE_TIME.format(gmt); getHeaders().set(HttpHeaders.IF_MODIFIED_SINCE, headerValue); @@ -239,26 +249,36 @@ class DefaultWebClient implements WebClient { } @Override - public DefaultHeaderSpec ifNoneMatch(String... ifNoneMatches) { + public DefaultRequestBodySpec ifNoneMatch(String... ifNoneMatches) { getHeaders().setIfNoneMatch(Arrays.asList(ifNoneMatches)); return this; } + @Override + public RequestHeadersSpec body(BodyInserter inserter) { + this.inserter = inserter; + return this; + } + + @Override + public > RequestHeadersSpec body(S publisher, Class elementClass) { + this.inserter = BodyInserters.fromPublisher(publisher, elementClass); + return this; + } + + @Override + public RequestHeadersSpec body(T body) { + this.inserter = BodyInserters.fromObject(body); + return this; + } + @Override public Mono exchange() { - ClientRequest request = this.initRequestBuilder().build(); - return exchangeFunction.exchange(request); - } - @Override - public Mono exchange(BodyInserter inserter) { - ClientRequest request = this.initRequestBuilder().body(inserter).build(); - return exchangeFunction.exchange(request); - } + ClientRequest request = this.inserter != null ? + initRequestBuilder().body(this.inserter).build() : + initRequestBuilder().build(); - @Override - public > Mono exchange(S publisher, Class elementClass) { - ClientRequest request = initRequestBuilder().headers(this.headers).body(publisher, elementClass).build(); return exchangeFunction.exchange(request); } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java index 2d1b2e733f9..0f608835c2d 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java @@ -26,6 +26,7 @@ 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.ClientHttpConnector; import org.springframework.http.client.reactive.ClientHttpRequest; @@ -58,43 +59,49 @@ public interface WebClient { * Prepare an HTTP GET request. * @return a spec for specifying the target URL */ - UriSpec get(); + UriSpec> get(); /** * Prepare an HTTP HEAD request. * @return a spec for specifying the target URL */ - UriSpec head(); + UriSpec> head(); /** * Prepare an HTTP POST request. * @return a spec for specifying the target URL */ - UriSpec post(); + UriSpec post(); /** * Prepare an HTTP PUT request. * @return a spec for specifying the target URL */ - UriSpec put(); + UriSpec put(); /** * Prepare an HTTP PATCH request. * @return a spec for specifying the target URL */ - UriSpec patch(); + UriSpec patch(); /** * Prepare an HTTP DELETE request. * @return a spec for specifying the target URL */ - UriSpec delete(); + UriSpec> delete(); /** * Prepare an HTTP OPTIONS request. * @return a spec for specifying the target URL */ - UriSpec options(); + UriSpec> options(); + + /** + * Prepare a request for the specified {@code HttpMethod}. + * @return a spec for specifying the target URL + */ + UriSpec method(HttpMethod method); /** @@ -266,38 +273,38 @@ public interface WebClient { /** * Contract for specifying the URI for a request. */ - interface UriSpec { + interface UriSpec> { /** * Specify the URI using an absolute, fully constructed {@link URI}. */ - HeaderSpec uri(URI uri); + S 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. */ - HeaderSpec uri(String uri, Object... uriVariables); + S uri(String uri, Object... uriVariables); /** * 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. */ - HeaderSpec uri(String uri, Map uriVariables); + S uri(String uri, Map uriVariables); /** * Build the URI for the request using the {@link UriBuilderFactory} * configured for this client. */ - HeaderSpec uri(Function uriFunction); + S uri(Function uriFunction); } /** * Contract for specifying request headers leading up to the exchange. */ - interface HeaderSpec { + interface RequestHeadersSpec> { /** * Set the list of acceptable {@linkplain MediaType media types}, as @@ -305,7 +312,7 @@ public interface WebClient { * @param acceptableMediaTypes the acceptable media types * @return this builder */ - HeaderSpec accept(MediaType... acceptableMediaTypes); + S accept(MediaType... acceptableMediaTypes); /** * Set the list of acceptable {@linkplain Charset charsets}, as specified @@ -313,25 +320,7 @@ public interface WebClient { * @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); + S acceptCharset(Charset... acceptableCharsets); /** * Add a cookie with the given name and value. @@ -339,14 +328,14 @@ public interface WebClient { * @param value the cookie value * @return this builder */ - HeaderSpec cookie(String name, String value); + S 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); + S cookies(MultiValueMap cookies); /** * Set the value of the {@code If-Modified-Since} header. @@ -355,14 +344,14 @@ public interface WebClient { * @param ifModifiedSince the new value of the header * @return this builder */ - HeaderSpec ifModifiedSince(ZonedDateTime ifModifiedSince); + S 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); + S ifNoneMatch(String... ifNoneMatches); /** * Add the given, single header value under the given name. @@ -370,40 +359,75 @@ public interface WebClient { * @param headerValues the header value(s) * @return this builder */ - HeaderSpec header(String headerName, String... headerValues); + S 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); + S headers(HttpHeaders headers); /** - * Perform the request without a request body. + * Exchange the built request for a delayed {@code ClientResponse}. * @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); + } + + interface RequestBodySpec extends RequestHeadersSpec { /** - * Set the body of the request to the given {@code Publisher} and - * perform the request. + * 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) + */ + RequestBodySpec 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) + */ + RequestBodySpec contentType(MediaType contentType); + + /** + * Set the body of the request to the given {@code BodyInserter}. + * @param inserter the {@code BodyInserter} that writes to the request + * @param the type contained in the body + * @return this builder + */ + RequestHeadersSpec body(BodyInserter inserter); + + /** + * Set the body of the request to the given {@code Publisher}. + *

This method is a convenient shortcut for {@link #body(BodyInserter)} with a + * {@linkplain org.springframework.web.reactive.function.BodyInserters#fromPublisher} + * Publisher body inserter}. * @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 + * @return this builder */ - > Mono exchange(S publisher, Class elementClass); + > RequestHeadersSpec body(S publisher, Class elementClass); + + /** + * Set the body of the request to the given {@code Object}. + *

This method is a convenient shortcut for {@link #body(BodyInserter)} with a + * {@linkplain org.springframework.web.reactive.function.BodyInserters#fromObject + * Object body inserter}. + * @param body the {@code Object} to write to the request + * @param the type contained in the body + * @return this builder + */ + RequestHeadersSpec body(T body); + } } diff --git a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/client/WebClientExtensions.kt b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/client/WebClientExtensions.kt index 3018c605faf..1b41cfaf0b6 100644 --- a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/client/WebClientExtensions.kt +++ b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/client/WebClientExtensions.kt @@ -1,13 +1,29 @@ +/* + * 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.reactivestreams.Publisher /** - * Extension for [WebClient.HeaderSpec.exchange] providing a variant without explicit class + * Extension for [WebClient.RequestHeadersSpec.exchangePublisher] providing a variant without explicit class * parameter thanks to Kotlin reified type parameters. * * @author Sebastien Deleuze * @since 5.0 */ -inline fun > WebClient.HeaderSpec.exchange(publisher: S) = - exchange(publisher, T::class.java) +inline fun > WebClient.RequestBodySpec.body(publisher: S) = + body(publisher, T::class.java) diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java index c3945a2d72c..8c6e34694c7 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java @@ -35,8 +35,8 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.codec.Pojo; -import static org.junit.Assert.*; -import static org.springframework.web.reactive.function.BodyInserters.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; /** * Integration tests using a {@link ExchangeFunction} through {@link WebClient}. @@ -188,7 +188,8 @@ public class WebClientIntegrationTests { .uri("/pojo/capitalize") .accept(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON) - .exchange(fromObject(new Pojo("foofoo", "barbar"))) + .body(new Pojo("foofoo", "barbar")) + .exchange() .then(response -> response.bodyToMono(Pojo.class)); StepVerifier.create(result)