diff --git a/spring-test/spring-test.gradle b/spring-test/spring-test.gradle index f46320cf9e..d1e81279ba 100644 --- a/spring-test/spring-test.gradle +++ b/spring-test/spring-test.gradle @@ -53,6 +53,8 @@ dependencies { optional("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}") optional("org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}") optional("io.projectreactor:reactor-test") + optional("org.jetbrains.kotlinx:kotlinx-coroutines-core:${coroutinesVersion}") + optional("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:${coroutinesVersion}") testCompile(project(":spring-context-support")) testCompile(project(":spring-oxm")) testCompile("javax.annotation:javax.annotation-api:1.3.2") 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 a382181e42..3d03e6e5ff 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2019 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. @@ -261,8 +261,20 @@ class DefaultWebTestClient implements WebTestClient { } @Override - public RequestHeadersSpec body(BodyInserter inserter) { - this.bodySpec.body(inserter); + public RequestHeadersSpec body(Object body) { + this.bodySpec.body(body); + return this; + } + + @Override + public RequestHeadersSpec body(Object producer, Class elementClass) { + this.bodySpec.body(producer, elementClass); + return this; + } + + @Override + public RequestHeadersSpec body(Object producer, ParameterizedTypeReference elementType) { + this.bodySpec.body(producer, elementType); return this; } @@ -273,11 +285,23 @@ class DefaultWebTestClient implements WebTestClient { } @Override - public RequestHeadersSpec syncBody(Object body) { - this.bodySpec.syncBody(body); + public > RequestHeadersSpec body(S publisher, ParameterizedTypeReference elementType) { + this.bodySpec.body(publisher, elementType); return this; } + @Override + public RequestHeadersSpec body(BodyInserter inserter) { + this.bodySpec.body(inserter); + return this; + } + + @Override + @Deprecated + public RequestHeadersSpec syncBody(Object body) { + return body(body); + } + @Override public ResponseSpec exchange() { ClientResponse clientResponse = this.bodySpec.exchange().block(getTimeout()); 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 cc2e8ab96f..9180920bcf 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2019 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. @@ -30,6 +30,7 @@ import org.reactivestreams.Publisher; import org.springframework.context.ApplicationContext; import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.format.FormatterRegistry; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -625,26 +626,7 @@ public interface WebTestClient { RequestBodySpec contentType(MediaType contentType); /** - * Set the body of the request to the given {@code BodyInserter}. - * @param inserter the inserter - * @return spec for decoding the response - * @see org.springframework.web.reactive.function.BodyInserters - */ - RequestHeadersSpec body(BodyInserter inserter); - - /** - * Set the body of the request to the given asynchronous {@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 - */ - > RequestHeadersSpec body(S publisher, Class elementClass); - - /** - * Set the body of the request to the given synchronous {@code Object} and - * perform the request. + * Set the body of the request to the given {@code Object} and perform the request. *

This method is a convenient shortcut for: *

 		 * .body(BodyInserters.fromObject(object))
@@ -657,8 +639,83 @@ public interface WebTestClient {
 		 * part with body and headers. The {@code MultiValueMap} can be built
 		 * conveniently using
 		 * @param body the {@code Object} to write to the request
-		 * @return a {@code Mono} with the response
+		 * @return spec for decoding the response
+		 * @since 5.2
 		 */
+		RequestHeadersSpec body(Object body);
+
+		/**
+		 * Set the body of the request to the given producer.
+		 * @param producer the producer to write to the request. This must be a
+		 * {@link Publisher} or another producer adaptable to a
+		 * {@code Publisher} via {@link ReactiveAdapterRegistry}
+		 * @param elementClass the class of elements contained in the producer
+		 * @return spec for decoding the response
+		 * @since 5.2
+		 */
+		RequestHeadersSpec body(Object producer, Class elementClass);
+
+		/**
+		 * Set the body of the request to the given producer.
+		 * @param producer the producer to write to the request. This must be a
+		 * {@link Publisher} or another producer adaptable to a
+		 * {@code Publisher} via {@link ReactiveAdapterRegistry}
+		 * @param elementType the type reference of elements contained in the producer
+		 * @return spec for decoding the response
+		 * @since 5.2
+		 */
+		RequestHeadersSpec body(Object producer, ParameterizedTypeReference elementType);
+
+		/**
+		 * Set the body of the request to the given asynchronous {@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
+		 */
+		> RequestHeadersSpec body(S publisher, Class elementClass);
+
+		/**
+		 * Set the body of the request to the given asynchronous {@code Publisher}.
+		 * @param publisher the request body data
+		 * @param elementType the type reference 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
+		 * @since 5.2
+		 */
+		> RequestHeadersSpec body(S publisher, ParameterizedTypeReference elementType);
+
+		/**
+		 * Set the body of the request to the given {@code BodyInserter}.
+		 * @param inserter the inserter
+		 * @return spec for decoding the response
+		 * @see org.springframework.web.reactive.function.BodyInserters
+		 */
+		RequestHeadersSpec body(BodyInserter inserter);
+
+		/**
+		 * Set the body of the request to the given {@code Object} and perform the request.
+		 * 

This method is a convenient shortcut for: + *

+		 * .body(BodyInserters.fromObject(object))
+		 * 
+ *

The body can be a + * {@link org.springframework.util.MultiValueMap MultiValueMap} to create + * a multipart request. The values in the {@code MultiValueMap} can be + * any Object representing the body of the part, or an + * {@link org.springframework.http.HttpEntity HttpEntity} representing a + * part with body and headers. The {@code MultiValueMap} can be built + * conveniently using + * @param body the {@code Object} to write to the request + * @return spec for decoding the response + * @throws IllegalArgumentException if {@code body} is a {@link Publisher} or an + * instance of a type supported by {@link ReactiveAdapterRegistry#getSharedInstance()}, + * for which {@link #body(Publisher, Class)} or {@link #body(Object, Class)} should be used. + * @deprecated as of Spring Framework 5.2 in favor of {@link #body(Object)} + */ + @Deprecated RequestHeadersSpec syncBody(Object body); } diff --git a/spring-test/src/main/kotlin/org/springframework/test/web/reactive/server/WebTestClientExtensions.kt b/spring-test/src/main/kotlin/org/springframework/test/web/reactive/server/WebTestClientExtensions.kt index d0c09a8cf3..e70ddee89c 100644 --- a/spring-test/src/main/kotlin/org/springframework/test/web/reactive/server/WebTestClientExtensions.kt +++ b/spring-test/src/main/kotlin/org/springframework/test/web/reactive/server/WebTestClientExtensions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2019 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. @@ -16,7 +16,10 @@ package org.springframework.test.web.reactive.server +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.Flow import org.reactivestreams.Publisher +import org.springframework.core.ParameterizedTypeReference import org.springframework.test.util.AssertionErrors.assertEquals import org.springframework.test.web.reactive.server.WebTestClient.* @@ -27,8 +30,49 @@ import org.springframework.test.web.reactive.server.WebTestClient.* * @author Sebastien Deleuze * @since 5.0 */ +@Deprecated("Use 'bodyWithType' instead.", replaceWith = ReplaceWith("bodyWithType(publisher)")) +@Suppress("EXTENSION_SHADOWED_BY_MEMBER") inline fun > RequestBodySpec.body(publisher: S): RequestHeadersSpec<*> - = body(publisher, T::class.java) + = body(publisher, object : ParameterizedTypeReference() {}) + +/** + * Extension for [RequestBodySpec.body] providing a `bodyWithType(Any)` variant + * leveraging Kotlin reified type parameters. This extension is not subject to type + * erasure and retains actual generic type arguments. + * @param producer the producer to write to the request. This must be a + * [Publisher] or another producer adaptable to a + * [Publisher] via [org.springframework.core.ReactiveAdapterRegistry] + * @param the type of the elements contained in the producer + * @author Sebastien Deleuze + * @since 5.2 + */ +inline fun RequestBodySpec.bodyWithType(producer: Any): RequestHeadersSpec<*> + = body(producer, object : ParameterizedTypeReference() {}) + +/** + * Extension for [RequestBodySpec.body] providing a `bodyWithType(Publisher)` variant + * leveraging Kotlin reified type parameters. This extension is not subject to type + * erasure and retains actual generic type arguments. + * @param publisher the [Publisher] to write to the request + * @param the type of the elements contained in the publisher + * @author Sebastien Deleuze + * @since 5.2 + */ +inline fun RequestBodySpec.bodyWithType(publisher: Publisher): RequestHeadersSpec<*> = + body(publisher, object : ParameterizedTypeReference() {}) + +/** + * Extension for [RequestBodySpec.body] providing a `bodyWithType(Flow)` variant + * leveraging Kotlin reified type parameters. This extension is not subject to type + * erasure and retains actual generic type arguments. + * @param flow the [Flow] to write to the request + * @param the type of the elements contained in the publisher + * @author Sebastien Deleuze + * @since 5.2 + */ +@FlowPreview +inline fun RequestBodySpec.bodyWithType(flow: Flow): RequestHeadersSpec<*> = + body(flow, object : ParameterizedTypeReference() {}) /** * Extension for [ResponseSpec.expectBody] providing an `expectBody()` variant and @@ -44,13 +88,11 @@ inline fun ResponseSpec.expectBody(): KotlinBodySpec = object : KotlinBodySpec { override fun isEqualTo(expected: B): KotlinBodySpec = it - .assertWithDiagnostics({ assertEquals("Response body", expected, it.responseBody) }) - .let { this } + .assertWithDiagnostics { assertEquals("Response body", expected, it.responseBody) } + .let { this } override fun consumeWith(consumer: (EntityExchangeResult) -> Unit): KotlinBodySpec = - it - .assertWithDiagnostics({ consumer.invoke(it) }) - .let { this } + it.assertWithDiagnostics { consumer.invoke(it) }.let { this } override fun returnResult(): EntityExchangeResult = it } @@ -88,7 +130,7 @@ interface KotlinBodySpec { * @since 5.0 */ inline fun ResponseSpec.expectBodyList(): ListBodySpec = - expectBodyList(E::class.java) + expectBodyList(object : ParameterizedTypeReference() {}) /** * Extension for [ResponseSpec.returnResult] providing a `returnResult()` variant. @@ -98,4 +140,4 @@ inline fun ResponseSpec.expectBodyList(): ListBodySpec = */ @Suppress("EXTENSION_SHADOWED_BY_MEMBER") inline fun ResponseSpec.returnResult(): FluxExchangeResult = - returnResult(T::class.java) + returnResult(object : ParameterizedTypeReference() {}) diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/ApplicationContextSpecTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/ApplicationContextSpecTests.java index 20dcdd649c..8704ff3266 100644 --- a/spring-test/src/test/java/org/springframework/test/web/reactive/server/ApplicationContextSpecTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/ApplicationContextSpecTests.java @@ -61,7 +61,7 @@ public class ApplicationContextSpecTests { .GET("/sessionClassName", request -> request.session().flatMap(session -> { String className = session.getClass().getSimpleName(); - return ServerResponse.ok().syncBody(className); + return ServerResponse.ok().body(className); })) .build(); } diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ErrorTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ErrorTests.java index 83d61e8de4..c85ff0b1af 100644 --- a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ErrorTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ErrorTests.java @@ -63,7 +63,7 @@ public class ErrorTests { EntityExchangeResult result = this.client.post() .uri("/post") .contentType(MediaType.APPLICATION_JSON) - .syncBody(new Person("Dan")) + .body(new Person("Dan")) .exchange() .expectStatus().isBadRequest() .expectBody().isEmpty(); diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/JsonContentTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/JsonContentTests.java index 0d0f9c7a65..6725a0a267 100644 --- a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/JsonContentTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/JsonContentTests.java @@ -82,7 +82,7 @@ public class JsonContentTests { public void postJsonContent() { this.client.post().uri("/persons") .contentType(MediaType.APPLICATION_JSON) - .syncBody("{\"name\":\"John\"}") + .body("{\"name\":\"John\"}") .exchange() .expectStatus().isCreated() .expectBody().isEmpty(); 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 7a1361cd5a..dda30498fb 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 @@ -145,7 +145,7 @@ public class ResponseEntityTests { @Test public void postEntity() { this.client.post() - .syncBody(new Person("John")) + .body(new Person("John")) .exchange() .expectStatus().isCreated() .expectHeader().valueEquals("location", "/persons/John") diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/XmlContentTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/XmlContentTests.java index faf18f6c6a..08c23635e5 100644 --- a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/XmlContentTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/XmlContentTests.java @@ -116,7 +116,7 @@ public class XmlContentTests { this.client.post().uri("/persons") .contentType(MediaType.APPLICATION_XML) - .syncBody(content) + .body(content) .exchange() .expectStatus().isCreated() .expectHeader().valueEquals(HttpHeaders.LOCATION, "/persons/John") diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/HttpServerTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/HttpServerTests.java index 773a6110e0..752ffca7c9 100644 --- a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/HttpServerTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/HttpServerTests.java @@ -45,7 +45,7 @@ public class HttpServerTests { @Before public void start() throws Exception { HttpHandler httpHandler = RouterFunctions.toHttpHandler( - route(GET("/test"), request -> ServerResponse.ok().syncBody("It works!"))); + route(GET("/test"), request -> ServerResponse.ok().body("It works!"))); this.server = new ReactorHttpServer(); this.server.setHandler(httpHandler); diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/RouterFunctionTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/RouterFunctionTests.java index aebeaf2c15..710ad2355b 100644 --- a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/RouterFunctionTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/RouterFunctionTests.java @@ -41,7 +41,7 @@ public class RouterFunctionTests { public void setUp() throws Exception { RouterFunction route = route(GET("/test"), request -> - ServerResponse.ok().syncBody("It works!")); + ServerResponse.ok().body("It works!")); this.testClient = WebTestClient.bindToRouterFunction(route).build(); } diff --git a/spring-test/src/test/kotlin/org/springframework/test/web/reactive/server/WebTestClientExtensionsTests.kt b/spring-test/src/test/kotlin/org/springframework/test/web/reactive/server/WebTestClientExtensionsTests.kt index 277c855246..6432b95418 100644 --- a/spring-test/src/test/kotlin/org/springframework/test/web/reactive/server/WebTestClientExtensionsTests.kt +++ b/spring-test/src/test/kotlin/org/springframework/test/web/reactive/server/WebTestClientExtensionsTests.kt @@ -18,10 +18,14 @@ package org.springframework.test.web.reactive.server import io.mockk.mockk import io.mockk.verify +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.Flow import org.junit.Assert.assertEquals import org.junit.Test import org.reactivestreams.Publisher +import org.springframework.core.ParameterizedTypeReference import org.springframework.web.reactive.function.server.router +import java.util.concurrent.CompletableFuture /** * Mock object based tests for [WebTestClient] Kotlin extensions @@ -30,16 +34,31 @@ import org.springframework.web.reactive.function.server.router */ class WebTestClientExtensionsTests { - val requestBodySpec = mockk(relaxed = true) + private val requestBodySpec = mockk(relaxed = true) - val responseSpec = mockk(relaxed = true) + private val responseSpec = mockk(relaxed = true) @Test - fun `RequestBodySpec#body with Publisher and reified type parameters`() { + fun `RequestBodySpec#bodyWithType with Publisher and reified type parameters`() { val body = mockk>() - requestBodySpec.body(body) - verify { requestBodySpec.body(body, Foo::class.java) } + requestBodySpec.bodyWithType(body) + verify { requestBodySpec.body(body, object : ParameterizedTypeReference() {}) } + } + + @Test + @FlowPreview + fun `RequestBodySpec#bodyWithType with Flow and reified type parameters`() { + val body = mockk>() + requestBodySpec.bodyWithType(body) + verify { requestBodySpec.body(body, object : ParameterizedTypeReference() {}) } + } + + @Test + fun `RequestBodySpec#bodyWithType with CompletableFuture and reified type parameters`() { + val body = mockk>() + requestBodySpec.bodyWithType(body) + verify { requestBodySpec.body(body, object : ParameterizedTypeReference() {}) } } @Test @@ -51,7 +70,7 @@ class WebTestClientExtensionsTests { @Test fun `KotlinBodySpec#isEqualTo`() { WebTestClient - .bindToRouterFunction( router { GET("/") { ok().syncBody("foo") } } ) + .bindToRouterFunction( router { GET("/") { ok().body("foo") } } ) .build() .get().uri("/").exchange().expectBody().isEqualTo("foo") } @@ -59,7 +78,7 @@ class WebTestClientExtensionsTests { @Test fun `KotlinBodySpec#consumeWith`() { WebTestClient - .bindToRouterFunction( router { GET("/") { ok().syncBody("foo") } } ) + .bindToRouterFunction( router { GET("/") { ok().body("foo") } } ) .build() .get().uri("/").exchange().expectBody().consumeWith { assertEquals("foo", it.responseBody) } } @@ -67,7 +86,7 @@ class WebTestClientExtensionsTests { @Test fun `KotlinBodySpec#returnResult`() { WebTestClient - .bindToRouterFunction( router { GET("/") { ok().syncBody("foo") } } ) + .bindToRouterFunction( router { GET("/") { ok().body("foo") } } ) .build() .get().uri("/").exchange().expectBody().returnResult().apply { assertEquals("foo", responseBody) } } @@ -75,13 +94,13 @@ class WebTestClientExtensionsTests { @Test fun `ResponseSpec#expectBodyList with reified type parameters`() { responseSpec.expectBodyList() - verify { responseSpec.expectBodyList(Foo::class.java) } + verify { responseSpec.expectBodyList(object : ParameterizedTypeReference() {}) } } @Test fun `ResponseSpec#returnResult with reified type parameters`() { responseSpec.returnResult() - verify { responseSpec.returnResult(Foo::class.java) } + verify { responseSpec.returnResult(object : ParameterizedTypeReference() {}) } } class Foo diff --git a/spring-web/src/main/java/org/springframework/http/client/MultipartBodyBuilder.java b/spring-web/src/main/java/org/springframework/http/client/MultipartBodyBuilder.java index 8982cc4f97..d63f65f142 100644 --- a/spring-web/src/main/java/org/springframework/http/client/MultipartBodyBuilder.java +++ b/spring-web/src/main/java/org/springframework/http/client/MultipartBodyBuilder.java @@ -41,7 +41,7 @@ import org.springframework.util.MultiValueMap; /** * Builder for the body of a multipart request, producing * {@code MultiValueMap}, which can be provided to the - * {@code WebClient} through the {@code syncBody} method. + * {@code WebClient} through the {@code body} method. * * Examples: *

@@ -67,7 +67,7 @@ import org.springframework.util.MultiValueMap;
  *
  * Mono<Void> result = webClient.post()
  *     .uri("...")
- *     .syncBody(multipartBody)
+ *     .body(multipartBody)
  *     .retrieve()
  *     .bodyToMono(Void.class)
  * 
diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/BodyInserters.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/BodyInserters.java index 72442eaf78..e869dc55c2 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/BodyInserters.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/BodyInserters.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2019 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. @@ -23,6 +23,8 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.ReactiveAdapter; +import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ResolvableType; import org.springframework.core.io.Resource; import org.springframework.core.io.buffer.DataBuffer; @@ -44,6 +46,7 @@ import org.springframework.util.MultiValueMap; * * @author Arjen Poutsma * @author Rossen Stoyanchev + * @author Sebastien Deleuze * @since 5.0 */ public abstract class BodyInserters { @@ -61,6 +64,8 @@ public abstract class BodyInserters { private static final BodyInserter EMPTY_INSERTER = (response, context) -> response.setComplete(); + private static final ReactiveAdapterRegistry registry = ReactiveAdapterRegistry.getSharedInstance(); + /** * Inserter that does not write. @@ -73,16 +78,68 @@ public abstract class BodyInserters { /** * Inserter to write the given object. - *

Alternatively, consider using the {@code syncBody(Object)} shortcuts on + *

Alternatively, consider using the {@code body(Object)} shortcuts on * {@link org.springframework.web.reactive.function.client.WebClient WebClient} and * {@link org.springframework.web.reactive.function.server.ServerResponse ServerResponse}. * @param body the body to write to the response * @param the type of the body * @return the inserter to write a single object + * @throws IllegalArgumentException if {@code body} is a {@link Publisher} or an + * instance of a type supported by {@link ReactiveAdapterRegistry#getSharedInstance()}, + * for which {@link #fromPublisher(Publisher, Class)} or + * {@link #fromProducer(Object, Class)} should be used. + * @see #fromPublisher(Publisher, Class) + * @see #fromProducer(Object, Class) */ public static BodyInserter fromObject(T body) { + Assert.notNull(body, "Body must not be null"); + Assert.isNull(registry.getAdapter(body.getClass()), "'body' should be an object, for reactive types use a variant specifying a publisher/producer and its related element type"); return (message, context) -> - writeWithMessageWriters(message, context, Mono.just(body), ResolvableType.forInstance(body)); + writeWithMessageWriters(message, context, Mono.just(body), ResolvableType.forInstance(body), null); + } + + /** + * Inserter to write the given producer of value(s) which must be a {@link Publisher} + * or another producer adaptable to a {@code Publisher} via + * {@link ReactiveAdapterRegistry}. + *

Alternatively, consider using the {@code body} shortcuts on + * {@link org.springframework.web.reactive.function.client.WebClient WebClient} and + * {@link org.springframework.web.reactive.function.server.ServerResponse ServerResponse}. + * @param the type of the body + * @param producer the source of body value(s). + * @param elementClass the type of values to be produced + * @return the inserter to write a producer + * @since 5.2 + */ + public static BodyInserter fromProducer(T producer, Class elementClass) { + Assert.notNull(producer, "'producer' must not be null"); + Assert.notNull(elementClass, "'elementClass' must not be null"); + ReactiveAdapter adapter = ReactiveAdapterRegistry.getSharedInstance().getAdapter(producer.getClass()); + Assert.notNull(adapter, "'producer' type is unknown to ReactiveAdapterRegistry"); + return (message, context) -> + writeWithMessageWriters(message, context, producer, ResolvableType.forClass(elementClass), adapter); + } + + /** + * Inserter to write the given producer of value(s) which must be a {@link Publisher} + * or another producer adaptable to a {@code Publisher} via + * {@link ReactiveAdapterRegistry}. + *

Alternatively, consider using the {@code body} shortcuts on + * {@link org.springframework.web.reactive.function.client.WebClient WebClient} and + * {@link org.springframework.web.reactive.function.server.ServerResponse ServerResponse}. + * @param the type of the body + * @param producer the source of body value(s). + * @param elementType the type of values to be produced + * @return the inserter to write a producer + * @since 5.2 + */ + public static BodyInserter fromProducer(T producer, ParameterizedTypeReference elementType) { + Assert.notNull(producer, "'producer' must not be null"); + Assert.notNull(elementType, "'elementType' must not be null"); + ReactiveAdapter adapter = ReactiveAdapterRegistry.getSharedInstance().getAdapter(producer.getClass()); + Assert.notNull(adapter, "'producer' type is unknown to ReactiveAdapterRegistry"); + return (message, context) -> + writeWithMessageWriters(message, context, producer, ResolvableType.forType(elementType), adapter); } /** @@ -102,7 +159,7 @@ public abstract class BodyInserters { Assert.notNull(publisher, "Publisher must not be null"); Assert.notNull(elementClass, "Element Class must not be null"); return (message, context) -> - writeWithMessageWriters(message, context, publisher, ResolvableType.forClass(elementClass)); + writeWithMessageWriters(message, context, publisher, ResolvableType.forClass(elementClass), null); } /** @@ -122,7 +179,7 @@ public abstract class BodyInserters { Assert.notNull(publisher, "Publisher must not be null"); Assert.notNull(typeReference, "ParameterizedTypeReference must not be null"); return (message, context) -> - writeWithMessageWriters(message, context, publisher, ResolvableType.forType(typeReference.getType())); + writeWithMessageWriters(message, context, publisher, ResolvableType.forType(typeReference.getType()), null); } /** @@ -145,8 +202,8 @@ public abstract class BodyInserters { /** * Inserter to write the given {@code ServerSentEvent} publisher. *

Alternatively, you can provide event data objects via - * {@link #fromPublisher(Publisher, Class)}, and set the "Content-Type" to - * {@link MediaType#TEXT_EVENT_STREAM text/event-stream}. + * {@link #fromPublisher(Publisher, Class)} or {@link #fromProducer(Object, Class)}, + * and set the "Content-Type" to {@link MediaType#TEXT_EVENT_STREAM text/event-stream}. * @param eventsPublisher the {@code ServerSentEvent} publisher to write to the response body * @param the type of the data elements in the {@link ServerSentEvent} * @return the inserter to write a {@code ServerSentEvent} publisher @@ -169,7 +226,7 @@ public abstract class BodyInserters { * Return a {@link FormInserter} to write the given {@code MultiValueMap} * as URL-encoded form data. The returned inserter allows for additional * entries to be added via {@link FormInserter#with(String, Object)}. - *

Note that you can also use the {@code syncBody(Object)} method in the + *

Note that you can also use the {@code body(Object)} method in the * request builders of both the {@code WebClient} and {@code WebTestClient}. * In that case the setting of the request content type is also not required, * just be sure the map contains String values only or otherwise it would be @@ -201,7 +258,7 @@ public abstract class BodyInserters { * Object or an {@link HttpEntity}. *

Note that you can also build the multipart data externally with * {@link MultipartBodyBuilder}, and pass the resulting map directly to the - * {@code syncBody(Object)} shortcut method in {@code WebClient}. + * {@code body(Object)} shortcut method in {@code WebClient}. * @param multipartData the form data to write to the output message * @return the inserter that allows adding more parts * @see MultipartBodyBuilder @@ -217,7 +274,7 @@ public abstract class BodyInserters { * {@link HttpEntity}. *

Note that you can also build the multipart data externally with * {@link MultipartBodyBuilder}, and pass the resulting map directly to the - * {@code syncBody(Object)} shortcut method in {@code WebClient}. + * {@code body(Object)} shortcut method in {@code WebClient}. * @param name the part name * @param value the part value, an Object or {@code HttpEntity} * @return the inserter that allows adding more parts @@ -233,7 +290,7 @@ public abstract class BodyInserters { * as multipart data. *

Note that you can also build the multipart data externally with * {@link MultipartBodyBuilder}, and pass the resulting map directly to the - * {@code syncBody(Object)} shortcut method in {@code WebClient}. + * {@code body(Object)} shortcut method in {@code WebClient}. * @param name the part name * @param publisher the publisher that forms the part value * @param elementClass the class contained in the {@code publisher} @@ -251,7 +308,7 @@ public abstract class BodyInserters { * allows specifying generic type information. *

Note that you can also build the multipart data externally with * {@link MultipartBodyBuilder}, and pass the resulting map directly to the - * {@code syncBody(Object)} shortcut method in {@code WebClient}. + * {@code body(Object)} shortcut method in {@code WebClient}. * @param name the part name * @param publisher the publisher that forms the part value * @param typeReference the type contained in the {@code publisher} @@ -278,15 +335,25 @@ public abstract class BodyInserters { } - private static

, M extends ReactiveHttpOutputMessage> Mono writeWithMessageWriters( - M outputMessage, BodyInserter.Context context, P body, ResolvableType bodyType) { + private static Mono writeWithMessageWriters( + M outputMessage, BodyInserter.Context context, Object body, ResolvableType bodyType, @Nullable ReactiveAdapter adapter) { + Publisher publisher; + if (body instanceof Publisher) { + publisher = (Publisher) body; + } + else if (adapter != null) { + publisher = adapter.toPublisher(body); + } + else { + publisher = Mono.just(body); + } MediaType mediaType = outputMessage.getHeaders().getContentType(); return context.messageWriters().stream() .filter(messageWriter -> messageWriter.canWrite(bodyType, mediaType)) .findFirst() .map(BodyInserters::cast) - .map(writer -> write(body, bodyType, mediaType, outputMessage, context, writer)) + .map(writer -> write(publisher, bodyType, mediaType, outputMessage, context, writer)) .orElseGet(() -> Mono.error(unsupportedError(bodyType, context, mediaType))); } 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 81f7bb93fd..2be82f862b 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 @@ -61,6 +61,7 @@ import org.springframework.web.util.UriBuilderFactory; * * @author Rossen Stoyanchev * @author Brian Clozel + * @author Sebastien Deleuze * @since 5.0 */ class DefaultWebClient implements WebClient { @@ -290,16 +291,27 @@ class DefaultWebClient implements WebClient { } @Override - public RequestHeadersSpec body(BodyInserter inserter) { - this.inserter = inserter; + public RequestHeadersSpec body(Object body) { + this.inserter = BodyInserters.fromObject(body); + return this; + } + + @Override + public RequestHeadersSpec body(Object producer, Class elementClass) { + this.inserter = BodyInserters.fromProducer(producer, elementClass); + return this; + } + + @Override + public RequestHeadersSpec body(Object producer, ParameterizedTypeReference elementType) { + this.inserter = BodyInserters.fromProducer(producer, elementType); return this; } @Override public > RequestHeadersSpec body( - P publisher, ParameterizedTypeReference typeReference) { - - this.inserter = BodyInserters.fromPublisher(publisher, typeReference); + P publisher, ParameterizedTypeReference elementType) { + this.inserter = BodyInserters.fromPublisher(publisher, elementType); return this; } @@ -310,13 +322,17 @@ class DefaultWebClient implements WebClient { } @Override - public RequestHeadersSpec syncBody(Object body) { - Assert.isTrue(!(body instanceof Publisher), - "Please specify the element class by using body(Publisher, Class)"); - this.inserter = BodyInserters.fromObject(body); + public RequestHeadersSpec body(BodyInserter inserter) { + this.inserter = inserter; return this; } + @Override + @Deprecated + public RequestHeadersSpec syncBody(Object body) { + return body(body); + } + @Override public Mono exchange() { ClientRequest request = (this.inserter != null ? 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 1a7f366076..b5cdfed4c2 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 @@ -30,6 +30,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; @@ -56,13 +57,15 @@ import org.springframework.web.util.UriBuilderFactory; * *

For examples with a request body see: *

    + *
  • {@link RequestBodySpec#body(Object) body(Object)} *
  • {@link RequestBodySpec#body(Publisher, Class) body(Publisher,Class)} - *
  • {@link RequestBodySpec#syncBody(Object) syncBody(Object)} + *
  • {@link RequestBodySpec#body(Object, Class) body(Object,Class)} *
  • {@link RequestBodySpec#body(BodyInserter) body(BodyInserter)} *
* * @author Rossen Stoyanchev * @author Arjen Poutsma + * @author Sebastien Deleuze * @since 5.0 */ public interface WebClient { @@ -517,23 +520,80 @@ public interface WebClient { RequestBodySpec contentType(MediaType contentType); /** - * Set the body of the request using the given body inserter. - * {@link BodyInserters} provides access to built-in implementations of - * {@link BodyInserter}. - * @param inserter the body inserter to use for the request body + * A shortcut for {@link #body(BodyInserter)} with an + * {@linkplain BodyInserters#fromObject Object inserter}. + * For example: + *

+		 * Person person = ... ;
+		 *
+		 * Mono<Void> result = client.post()
+		 *     .uri("/persons/{id}", id)
+		 *     .contentType(MediaType.APPLICATION_JSON)
+		 *     .body(person)
+		 *     .retrieve()
+		 *     .bodyToMono(Void.class);
+		 * 
+ *

For multipart requests, provide a + * {@link org.springframework.util.MultiValueMap MultiValueMap}. The + * values in the {@code MultiValueMap} can be any Object representing + * the body of the part, or an + * {@link org.springframework.http.HttpEntity HttpEntity} representing + * a part with body and headers. The {@code MultiValueMap} can be built + * with {@link org.springframework.http.client.MultipartBodyBuilder + * MultipartBodyBuilder}. + * @param body the {@code Object} to write to the request * @return this builder - * @see org.springframework.web.reactive.function.BodyInserters + * @throws IllegalArgumentException if {@code body} is a {@link Publisher} or an + * instance of a type supported by {@link ReactiveAdapterRegistry#getSharedInstance()}, + * for which {@link #body(Publisher, Class)} or {@link #body(Object, Class)} should be used. + * @since 5.2 */ - RequestHeadersSpec body(BodyInserter inserter); + RequestHeadersSpec body(Object body); + + /** + * A shortcut for {@link #body(BodyInserter)} with a + * {@linkplain BodyInserters#fromProducer inserter}. + * For example: + *

+		 * Single<Person> personSingle = ... ;
+		 *
+		 * Mono<Void> result = client.post()
+		 *     .uri("/persons/{id}", id)
+		 *     .contentType(MediaType.APPLICATION_JSON)
+		 *     .body(personSingle, Person.class)
+		 *     .retrieve()
+		 *     .bodyToMono(Void.class);
+		 * 
+ * @param producer the producer to write to the request. This must be a + * {@link Publisher} or another producer adaptable to a + * {@code Publisher} via {@link ReactiveAdapterRegistry} + * @param elementClass the class of elements contained in the producer + * @return this builder + * @since 5.2 + */ + RequestHeadersSpec body(Object producer, Class elementClass); + + /** + * A variant of {@link #body(Object, Class)} that allows providing + * element type information that includes generics via a + * {@link ParameterizedTypeReference}. + * @param producer the producer to write to the request. This must be a + * {@link Publisher} or another producer adaptable to a + * {@code Publisher} via {@link ReactiveAdapterRegistry} + * @param elementType the type reference of elements contained in the producer + * @return this builder + * @since 5.2 + */ + RequestHeadersSpec body(Object producer, ParameterizedTypeReference elementType); /** * A shortcut for {@link #body(BodyInserter)} with a * {@linkplain BodyInserters#fromPublisher Publisher inserter}. * For example: *

-		 * Mono personMono = ... ;
+		 * Mono<Person> personMono = ... ;
 		 *
-		 * Mono result = client.post()
+		 * Mono<Void> result = client.post()
 		 *     .uri("/persons/{id}", id)
 		 *     .contentType(MediaType.APPLICATION_JSON)
 		 *     .body(personMono, Person.class)
@@ -553,13 +613,23 @@ public interface WebClient {
 		 * element type information that includes generics via a
 		 * {@link ParameterizedTypeReference}.
 		 * @param publisher the {@code Publisher} to write to the request
-		 * @param typeReference the type reference of elements contained in the publisher
+		 * @param elementType the type reference of elements contained in the publisher
 		 * @param  the type of the elements contained in the publisher
 		 * @param 

the type of the {@code Publisher} * @return this builder */ > RequestHeadersSpec body(P publisher, - ParameterizedTypeReference typeReference); + ParameterizedTypeReference elementType); + + /** + * Set the body of the request using the given body inserter. + * {@link BodyInserters} provides access to built-in implementations of + * {@link BodyInserter}. + * @param inserter the body inserter to use for the request body + * @return this builder + * @see org.springframework.web.reactive.function.BodyInserters + */ + RequestHeadersSpec body(BodyInserter inserter); /** * A shortcut for {@link #body(BodyInserter)} with an @@ -585,7 +655,12 @@ public interface WebClient { * MultipartBodyBuilder}. * @param body the {@code Object} to write to the request * @return this builder + * @throws IllegalArgumentException if {@code body} is a {@link Publisher} or an + * instance of a type supported by {@link ReactiveAdapterRegistry#getSharedInstance()}, + * for which {@link #body(Publisher, Class)} or {@link #body(Object, Class)} should be used. + * @deprecated as of Spring Framework 5.2 in favor of {@link #body(Object)} */ + @Deprecated RequestHeadersSpec syncBody(Object body); } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerResponseBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerResponseBuilder.java index cabbe2bf04..49e35a9cf3 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerResponseBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerResponseBuilder.java @@ -59,6 +59,7 @@ import org.springframework.web.server.ServerWebExchange; * * @author Arjen Poutsma * @author Juergen Hoeller + * @author Sebastien Deleuze * @since 5.0 */ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder { @@ -222,10 +223,43 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder { } @Override - public > Mono body(P publisher, Class elementClass) { - Assert.notNull(publisher, "Publisher must not be null"); - Assert.notNull(elementClass, "Element Class must not be null"); + public Mono body(Object body) { + return new DefaultEntityResponseBuilder<>(body, + BodyInserters.fromObject(body)) + .status(this.statusCode) + .headers(this.headers) + .cookies(cookies -> cookies.addAll(this.cookies)) + .hints(hints -> hints.putAll(this.hints)) + .build() + .map(entityResponse -> entityResponse); + } + @Override + public Mono body(Object producer, Class elementClass) { + return new DefaultEntityResponseBuilder<>(producer, + BodyInserters.fromProducer(producer, elementClass)) + .status(this.statusCode) + .headers(this.headers) + .cookies(cookies -> cookies.addAll(this.cookies)) + .hints(hints -> hints.putAll(this.hints)) + .build() + .map(entityResponse -> entityResponse); + } + + @Override + public Mono body(Object producer, ParameterizedTypeReference elementType) { + return new DefaultEntityResponseBuilder<>(producer, + BodyInserters.fromProducer(producer, elementType)) + .status(this.statusCode) + .headers(this.headers) + .cookies(cookies -> cookies.addAll(this.cookies)) + .hints(hints -> hints.putAll(this.hints)) + .build() + .map(entityResponse -> entityResponse); + } + + @Override + public > Mono body(P publisher, Class elementClass) { return new DefaultEntityResponseBuilder<>(publisher, BodyInserters.fromPublisher(publisher, elementClass)) .status(this.statusCode) @@ -238,13 +272,9 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder { @Override public > Mono body(P publisher, - ParameterizedTypeReference typeReference) { - - Assert.notNull(publisher, "Publisher must not be null"); - Assert.notNull(typeReference, "ParameterizedTypeReference must not be null"); - + ParameterizedTypeReference elementType) { return new DefaultEntityResponseBuilder<>(publisher, - BodyInserters.fromPublisher(publisher, typeReference)) + BodyInserters.fromPublisher(publisher, elementType)) .status(this.statusCode) .headers(this.headers) .cookies(cookies -> cookies.addAll(this.cookies)) @@ -254,19 +284,9 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder { } @Override + @Deprecated public Mono syncBody(Object body) { - Assert.notNull(body, "Body must not be null"); - Assert.isTrue(!(body instanceof Publisher), - "Please specify the element class by using body(Publisher, Class)"); - - return new DefaultEntityResponseBuilder<>(body, - BodyInserters.fromObject(body)) - .status(this.statusCode) - .headers(this.headers) - .cookies(cookies -> cookies.addAll(this.cookies)) - .hints(hints -> hints.putAll(this.hints)) - .build() - .map(entityResponse -> entityResponse); + return body(body); } @Override diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/EntityResponse.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/EntityResponse.java index ab72a30f5a..152b038770 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/EntityResponse.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/EntityResponse.java @@ -64,12 +64,38 @@ public interface EntityResponse extends ServerResponse { /** * Create a builder with the given object. - * @param t the object that represents the body of the response - * @param the type of the elements contained in the publisher + * @param body the object that represents the body of the response + * @param the type of the body * @return the created builder */ - static Builder fromObject(T t) { - return new DefaultEntityResponseBuilder<>(t, BodyInserters.fromObject(t)); + static Builder fromObject(T body) { + return new DefaultEntityResponseBuilder<>(body, BodyInserters.fromObject(body)); + } + + /** + * Create a builder with the given producer. + * @param producer the producer that represents the body of the response + * @param elementClass the class of elements contained in the publisher + * @return the created builder + * @since 5.2 + */ + static Builder fromProducer(T producer, Class elementClass) { + return new DefaultEntityResponseBuilder<>(producer, + BodyInserters.fromProducer(producer, elementClass)); + } + + /** + * Create a builder with the given producer. + * @param producer the producer that represents the body of the response + * @param typeReference the type of elements contained in the producer + * @return the created builder + * @since 5.2 + */ + static Builder fromProducer(T producer, + ParameterizedTypeReference typeReference) { + + return new DefaultEntityResponseBuilder<>(producer, + BodyInserters.fromProducer(producer, typeReference)); } /** diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ServerResponse.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ServerResponse.java index 286845d9da..41a2fce19b 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ServerResponse.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ServerResponse.java @@ -30,6 +30,7 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.http.CacheControl; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -384,6 +385,45 @@ public interface ServerResponse { */ BodyBuilder hints(Consumer> hintsConsumer); + /** + * Set the body of the response to the given {@code Object} and return it. + * This convenience method combines {@link #body(BodyInserter)} and + * {@link BodyInserters#fromObject(Object)}. + * @param body the body of the response + * @return the built response + * @throws IllegalArgumentException if {@code body} is a {@link Publisher} or an + * instance of a type supported by {@link ReactiveAdapterRegistry#getSharedInstance()}, + * for which {@link #body(Publisher, Class)} or {@link #body(Object, Class)} should be used. + * @since 5.2 + */ + Mono body(Object body); + + /** + * Set the body of the response to the given asynchronous {@code Publisher} and return it. + * This convenience method combines {@link #body(BodyInserter)} and + * {@link BodyInserters#fromProducer(Object, Class)}. + * @param producer the producer to write to the response. This must be a + * {@link Publisher} or another producer adaptable to a + * {@code Publisher} via {@link ReactiveAdapterRegistry} + * @param elementClass the class of elements contained in the producer + * @return the built response + * @since 5.2 + */ + Mono body(Object producer, Class elementClass); + + /** + * Set the body of the response to the given asynchronous {@code Publisher} and return it. + * This convenience method combines {@link #body(BodyInserter)} and + * {@link BodyInserters#fromProducer(Object, ParameterizedTypeReference)}. + * @param producer the producer to write to the response. This must be a + * {@link Publisher} or another producer adaptable to a + * {@code Publisher} via {@link ReactiveAdapterRegistry} + * @param typeReference a type reference describing the elements contained in the producer + * @return the built response + * @since 5.2 + */ + Mono body(Object producer, ParameterizedTypeReference typeReference); + /** * Set the body of the response to the given asynchronous {@code Publisher} and return it. * This convenience method combines {@link #body(BodyInserter)} and @@ -399,7 +439,7 @@ public interface ServerResponse { /** * Set the body of the response to the given asynchronous {@code Publisher} and return it. * This convenience method combines {@link #body(BodyInserter)} and - * {@link BodyInserters#fromPublisher(Publisher, Class)}. + * {@link BodyInserters#fromPublisher(Publisher, ParameterizedTypeReference)}. * @param publisher the {@code Publisher} to write to the response * @param typeReference a type reference describing the elements contained in the publisher * @param the type of the elements contained in the publisher @@ -410,23 +450,28 @@ public interface ServerResponse { ParameterizedTypeReference typeReference); /** - * Set the body of the response to the given synchronous {@code Object} and return it. + * Set the body of the response to the given {@code BodyInserter} and return it. + * @param inserter the {@code BodyInserter} that writes to the response + * @return the built response + */ + Mono body(BodyInserter inserter); + + /** + * Set the body of the response to the given {@code Object} and return it. * This convenience method combines {@link #body(BodyInserter)} and * {@link BodyInserters#fromObject(Object)}. * @param body the body of the response * @return the built response * @throws IllegalArgumentException if {@code body} is a {@link Publisher}, for which * {@link #body(Publisher, Class)} should be used. + * @throws IllegalArgumentException if {@code body} is a {@link Publisher} or an + * instance of a type supported by {@link ReactiveAdapterRegistry#getSharedInstance()}, + * for which {@link #body(Publisher, Class)} or {@link #body(Object, Class)} should be used. + * @deprecated as of Spring Framework 5.2 in favor of {@link #body(Object)} */ + @Deprecated Mono syncBody(Object body); - /** - * Set the body of the response to the given {@code BodyInserter} and return it. - * @param inserter the {@code BodyInserter} that writes to the response - * @return the built response - */ - Mono body(BodyInserter inserter); - /** * Render the template with the given {@code name} using the given {@code modelAttributes}. * The model attributes are mapped under a 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 ef4edb4429..fed3f65291 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 @@ -16,14 +16,10 @@ package org.springframework.web.reactive.function.client -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.reactive.awaitSingle import kotlinx.coroutines.reactive.flow.asFlow -import kotlinx.coroutines.reactive.flow.asPublisher -import kotlinx.coroutines.reactor.mono import org.reactivestreams.Publisher import org.springframework.core.ParameterizedTypeReference import org.springframework.web.reactive.function.client.WebClient.RequestBodySpec @@ -39,20 +35,59 @@ import reactor.core.publisher.Mono * @author Sebastien Deleuze * @since 5.0 */ +@Deprecated("Use 'bodyWithType' instead.", replaceWith = ReplaceWith("bodyWithType(publisher)")) +@Suppress("EXTENSION_SHADOWED_BY_MEMBER") inline fun > RequestBodySpec.body(publisher: S): RequestHeadersSpec<*> = body(publisher, object : ParameterizedTypeReference() {}) /** - * Coroutines [Flow] based extension for [WebClient.RequestBodySpec.body] providing a - * body(Flow)` variant leveraging Kotlin reified type parameters. This extension is - * not subject to type erasure and retains actual generic type arguments. - * + * Extension for [WebClient.RequestBodySpec.body] providing a `bodyWithType(Any)` variant + * leveraging Kotlin reified type parameters. This extension is not subject to type + * erasure and retains actual generic type arguments. + * @param producer the producer to write to the request. This must be a + * [Publisher] or another producer adaptable to a + * [Publisher] via [org.springframework.core.ReactiveAdapterRegistry] + * @param the type of the elements contained in the producer + * @author Sebastien Deleuze + * @since 5.2 + */ +inline fun RequestBodySpec.bodyWithType(producer: Any): RequestHeadersSpec<*> = + body(producer, object : ParameterizedTypeReference() {}) + +/** + * Extension for [WebClient.RequestBodySpec.body] providing a `bodyWithType(Publisher)` variant + * leveraging Kotlin reified type parameters. This extension is not subject to type + * erasure and retains actual generic type arguments. + * @param publisher the [Publisher] to write to the request + * @param the type of the elements contained in the publisher + * @author Sebastien Deleuze + * @since 5.2 + */ +inline fun RequestBodySpec.bodyWithType(publisher: Publisher): RequestHeadersSpec<*> = + body(publisher, object : ParameterizedTypeReference() {}) + +/** + * Extension for [WebClient.RequestBodySpec.body] providing a `bodyWithType(Flow)` variant + * leveraging Kotlin reified type parameters. This extension is not subject to type + * erasure and retains actual generic type arguments. + * @param flow the [Flow] to write to the request + * @param the type of the elements contained in the flow * @author Sebastien Deleuze * @since 5.2 */ @FlowPreview -inline fun > RequestBodySpec.body(flow: S): RequestHeadersSpec<*> = - body(flow.asPublisher(), object : ParameterizedTypeReference() {}) +inline fun RequestBodySpec.bodyWithType(flow: Flow): RequestHeadersSpec<*> = + body(flow, object : ParameterizedTypeReference() {}) + +/** + * Coroutines variant of [WebClient.RequestHeadersSpec.exchange]. + * + * @author Sebastien Deleuze + * @since 5.2 + */ +suspend fun RequestHeadersSpec>.awaitExchange(): ClientResponse = + exchange().awaitSingle() + /** * Extension for [WebClient.ResponseSpec.bodyToMono] providing a `bodyToMono()` variant @@ -90,25 +125,6 @@ inline fun WebClient.ResponseSpec.bodyToFlux(): Flux = inline fun WebClient.ResponseSpec.bodyToFlow(batchSize: Int = 1): Flow = bodyToFlux().asFlow(batchSize) - -/** - * Coroutines variant of [WebClient.RequestHeadersSpec.exchange]. - * - * @author Sebastien Deleuze - * @since 5.2 - */ -suspend fun RequestHeadersSpec>.awaitExchange(): ClientResponse = - exchange().awaitSingle() - -/** - * Coroutines variant of [WebClient.RequestBodySpec.body]. - * - * @author Sebastien Deleuze - * @since 5.2 - */ -inline fun RequestBodySpec.body(crossinline supplier: suspend () -> T) - = body(GlobalScope.mono(Dispatchers.Unconfined) { supplier.invoke() }) - /** * Coroutines variant of [WebClient.ResponseSpec.bodyToMono]. * diff --git a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/ServerResponseExtensions.kt b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/ServerResponseExtensions.kt index 8ed19bca04..01b8345f1a 100644 --- a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/ServerResponseExtensions.kt +++ b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/ServerResponseExtensions.kt @@ -19,7 +19,6 @@ package org.springframework.web.reactive.function.server import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.reactive.awaitSingle -import kotlinx.coroutines.reactive.flow.asPublisher import org.reactivestreams.Publisher import org.springframework.core.ParameterizedTypeReference import org.springframework.http.MediaType @@ -33,9 +32,63 @@ import reactor.core.publisher.Mono * @author Sebastien Deleuze * @since 5.0 */ +@Deprecated("Use 'bodyWithType' instead.", replaceWith = ReplaceWith("bodyWithType(publisher)")) +@Suppress("EXTENSION_SHADOWED_BY_MEMBER") inline fun ServerResponse.BodyBuilder.body(publisher: Publisher): Mono = body(publisher, object : ParameterizedTypeReference() {}) +/** + * Extension for [ServerResponse.BodyBuilder.body] providing a `bodyWithType(Any)` variant + * leveraging Kotlin reified type parameters. This extension is not subject to type + * erasure and retains actual generic type arguments. + * @param producer the producer to write to the response. This must be a + * [Publisher] or another producer adaptable to a + * [Publisher] via [org.springframework.core.ReactiveAdapterRegistry] + * @param the type of the elements contained in the producer + * @author Sebastien Deleuze + * @since 5.2 + */ +inline fun ServerResponse.BodyBuilder.bodyWithType(producer: Any): Mono = + body(producer, object : ParameterizedTypeReference() {}) + +/** + * Extension for [ServerResponse.BodyBuilder.body] providing a `bodyWithType(Publisher)` variant + * leveraging Kotlin reified type parameters. This extension is not subject to type + * erasure and retains actual generic type arguments. + * @param publisher the [Publisher] to write to the response + * @param the type of the elements contained in the publisher + * @author Sebastien Deleuze + * @since 5.2 + */ +inline fun ServerResponse.BodyBuilder.bodyWithType(publisher: Publisher): Mono = + body(publisher, object : ParameterizedTypeReference() {}) + +/** + * Coroutines variant of [ServerResponse.BodyBuilder.body] with an [Any] parameter. + * + * Set the body of the response to the given {@code Object} and return it. + * This convenience method combines [body] and + * [org.springframework.web.reactive.function.BodyInserters.fromObject]. + * @param body the body of the response + * @return the built response + * @throws IllegalArgumentException if `body` is a [Publisher] or an + * instance of a type supported by [org.springframework.core.ReactiveAdapterRegistry.getSharedInstance], + */ +suspend fun ServerResponse.BodyBuilder.bodyAndAwait(body: Any): ServerResponse = + body(body).awaitSingle() + +/** + * Coroutines variant of [ServerResponse.BodyBuilder.body] with [Any] and + * [ParameterizedTypeReference] parameters providing a `bodyAndAwait(Flow)` variant. + * This extension is not subject to type erasure and retains actual generic type arguments. + * + * @author Sebastien Deleuze + * @since 5.2 + */ +@FlowPreview +suspend inline fun ServerResponse.BodyBuilder.bodyAndAwait(flow: Flow): ServerResponse = + body(flow, object : ParameterizedTypeReference() {}).awaitSingle() + /** * Extension for [ServerResponse.BodyBuilder.body] providing a * `bodyToServerSentEvents(Publisher)` variant. This extension is not subject to type @@ -44,7 +97,7 @@ inline fun ServerResponse.BodyBuilder.body(publisher: Publishe * @author Sebastien Deleuze * @since 5.0 */ -@Deprecated("Use 'sse().body()' instead.") +@Deprecated("Use 'sse().bodyWithType(publisher)' instead.", replaceWith = ReplaceWith("sse().bodyWithType(publisher)")) inline fun ServerResponse.BodyBuilder.bodyToServerSentEvents(publisher: Publisher): Mono = contentType(MediaType.TEXT_EVENT_STREAM).body(publisher, object : ParameterizedTypeReference() {}) @@ -77,38 +130,7 @@ fun ServerResponse.BodyBuilder.html() = contentType(MediaType.TEXT_HTML) fun ServerResponse.BodyBuilder.sse() = contentType(MediaType.TEXT_EVENT_STREAM) /** - * Coroutines variant of [ServerResponse.HeadersBuilder.build]. - * - * @author Sebastien Deleuze - * @since 5.2 - */ -suspend fun ServerResponse.HeadersBuilder>.buildAndAwait(): ServerResponse = - build().awaitSingle() - -/** - * Coroutines [Flow] based extension for [ServerResponse.BodyBuilder.body] providing a - * `bodyFlowAndAwait(Flow)` variant. This extension is not subject to type erasure and retains - * actual generic type arguments. - * - * @author Sebastien Deleuze - * @since 5.2 - */ -@FlowPreview -suspend inline fun ServerResponse.BodyBuilder.bodyFlowAndAwait(flow: Flow): ServerResponse = - body(flow.asPublisher(), object : ParameterizedTypeReference() {}).awaitSingle() - -/** - * Coroutines variant of [ServerResponse.BodyBuilder.syncBody]. - * - * @author Sebastien Deleuze - * @since 5.2 - */ -suspend fun ServerResponse.BodyBuilder.bodyAndAwait(body: Any): ServerResponse = - syncBody(body).awaitSingle() - -/** - * Coroutines variant of [ServerResponse.BodyBuilder.syncBody] without the sync prefix since it is ok to use it within - * another suspendable function. + * Coroutines variant of [ServerResponse.BodyBuilder.render]. * * @author Sebastien Deleuze * @since 5.2 @@ -117,11 +139,20 @@ suspend fun ServerResponse.BodyBuilder.renderAndAwait(name: String, vararg model render(name, *modelAttributes).awaitSingle() /** - * Coroutines variant of [ServerResponse.BodyBuilder.syncBody] without the sync prefix since it is ok to use it within - * another suspendable function. + * Coroutines variant of [ServerResponse.BodyBuilder.render]. * * @author Sebastien Deleuze * @since 5.2 */ suspend fun ServerResponse.BodyBuilder.renderAndAwait(name: String, model: Map): ServerResponse = render(name, model).awaitSingle() + +/** + * Coroutines variant of [ServerResponse.HeadersBuilder.build]. + * + * @author Sebastien Deleuze + * @since 5.2 + */ +suspend fun ServerResponse.HeadersBuilder>.buildAndAwait(): ServerResponse = + build().awaitSingle() + diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/BodyInsertersTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/BodyInsertersTests.java index 39076cac07..a54bfc85f4 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/BodyInsertersTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/BodyInsertersTests.java @@ -30,6 +30,7 @@ import java.util.Map; import java.util.Optional; import com.fasterxml.jackson.annotation.JsonView; +import io.reactivex.Single; import org.junit.Before; import org.junit.Test; import reactor.core.publisher.Flux; @@ -159,6 +160,51 @@ public class BodyInsertersTests { .verify(); } + @Test + public void ofProducerWithMono() { + Mono body = Mono.just(new User("foo", "bar")); + BodyInserter inserter = BodyInserters.fromProducer(body, User.class); + + MockServerHttpResponse response = new MockServerHttpResponse(); + Mono result = inserter.insert(response, this.context); + StepVerifier.create(result).expectComplete().verify(); + StepVerifier.create(response.getBodyAsString()) + .expectNext("{\"username\":\"foo\",\"password\":\"bar\"}") + .expectComplete() + .verify(); + } + + @Test + public void ofProducerWithFlux() { + Flux body = Flux.just("foo"); + BodyInserter inserter = BodyInserters.fromProducer(body, String.class); + + MockServerHttpResponse response = new MockServerHttpResponse(); + Mono result = inserter.insert(response, this.context); + StepVerifier.create(result).expectComplete().verify(); + StepVerifier.create(response.getBody()) + .consumeNextWith(buf -> { + String actual = DataBufferTestUtils.dumpString(buf, UTF_8); + assertThat(actual).isEqualTo("foo"); + }) + .expectComplete() + .verify(); + } + + @Test + public void ofProducerWithSingle() { + Single body = Single.just(new User("foo", "bar")); + BodyInserter inserter = BodyInserters.fromProducer(body, User.class); + + MockServerHttpResponse response = new MockServerHttpResponse(); + Mono result = inserter.insert(response, this.context); + StepVerifier.create(result).expectComplete().verify(); + StepVerifier.create(response.getBodyAsString()) + .expectNext("{\"username\":\"foo\",\"password\":\"bar\"}") + .expectComplete() + .verify(); + } + @Test public void ofPublisher() { Flux body = Flux.just("foo"); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/MultipartIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/MultipartIntegrationTests.java index 51b19b0677..deeb681a43 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/MultipartIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/MultipartIntegrationTests.java @@ -61,7 +61,7 @@ public class MultipartIntegrationTests extends AbstractRouterFunctionIntegration Mono result = webClient .post() .uri("http://localhost:" + this.port + "/multipartData") - .syncBody(generateBody()) + .body(generateBody()) .exchange(); StepVerifier @@ -75,7 +75,7 @@ public class MultipartIntegrationTests extends AbstractRouterFunctionIntegration Mono result = webClient .post() .uri("http://localhost:" + this.port + "/parts") - .syncBody(generateBody()) + .body(generateBody()) .exchange(); StepVerifier @@ -89,7 +89,7 @@ public class MultipartIntegrationTests extends AbstractRouterFunctionIntegration Mono result = webClient .post() .uri("http://localhost:" + this.port + "/transferTo") - .syncBody(generateBody()) + .body(generateBody()) .retrieve() .bodyToMono(String.class); @@ -169,7 +169,7 @@ public class MultipartIntegrationTests extends AbstractRouterFunctionIntegration Path tempFile = Files.createTempFile("MultipartIntegrationTests", null); return part.transferTo(tempFile) .then(ServerResponse.ok() - .syncBody(tempFile.toString())); + .body(tempFile.toString())); } catch (Exception e) { return Mono.error(e); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultWebClientTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultWebClientTests.java index 99f1a354d3..275d51a3f0 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultWebClientTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultWebClientTests.java @@ -186,7 +186,7 @@ public class DefaultWebClientTests { WebClient client = this.builder.build(); assertThatIllegalArgumentException().isThrownBy(() -> - client.post().uri("https://example.com").syncBody(mono)); + client.post().uri("https://example.com").body(mono)); } @Test 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 12cb207b46..274c545418 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 @@ -354,7 +354,7 @@ public class WebClientIntegrationTests { .uri("/pojo/capitalize") .accept(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON) - .syncBody(new Pojo("foofoo", "barbar")) + .body(new Pojo("foofoo", "barbar")) .retrieve() .bodyToMono(Pojo.class); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultEntityResponseBuilderTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultEntityResponseBuilderTests.java index 81ed266c56..2f0dcc8e0c 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultEntityResponseBuilderTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultEntityResponseBuilderTests.java @@ -24,6 +24,7 @@ import java.util.EnumSet; import java.util.List; import java.util.Set; +import io.reactivex.Single; import org.junit.Test; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -76,6 +77,14 @@ public class DefaultEntityResponseBuilderTests { assertThat(response.entity()).isSameAs(body); } + @Test + public void fromProducer() { + Single body = Single.just("foo"); + ParameterizedTypeReference typeReference = new ParameterizedTypeReference() {}; + EntityResponse> response = EntityResponse.fromProducer(body, typeReference).build().block(); + assertThat(response.entity()).isSameAs(body); + } + @Test public void status() { String body = "foo"; diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultServerResponseBuilderTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultServerResponseBuilderTests.java index 5c0bed8cf1..979abb0302 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultServerResponseBuilderTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultServerResponseBuilderTests.java @@ -308,7 +308,7 @@ public class DefaultServerResponseBuilderTests { public void copyCookies() { Mono serverResponse = ServerResponse.ok() .cookie(ResponseCookie.from("foo", "bar").build()) - .syncBody("body"); + .body("body"); assertThat(serverResponse.block().cookies().isEmpty()).isFalse(); @@ -360,7 +360,7 @@ public class DefaultServerResponseBuilderTests { Mono mono = Mono.empty(); assertThatIllegalArgumentException().isThrownBy(() -> - ServerResponse.ok().syncBody(mono)); + ServerResponse.ok().body(mono)); } @Test @@ -368,7 +368,7 @@ public class DefaultServerResponseBuilderTests { String etag = "\"foo\""; ServerResponse responseMono = ServerResponse.ok() .eTag(etag) - .syncBody("bar") + .body("bar") .block(); MockServerHttpRequest request = MockServerHttpRequest.get("https://example.com") @@ -392,7 +392,7 @@ public class DefaultServerResponseBuilderTests { ServerResponse responseMono = ServerResponse.ok() .lastModified(oneMinuteBeforeNow) - .syncBody("bar") + .body("bar") .block(); MockServerHttpRequest request = MockServerHttpRequest.get("https://example.com") diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/InvalidHttpMethodIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/InvalidHttpMethodIntegrationTests.java index 206f0f7abf..4257e8f040 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/InvalidHttpMethodIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/InvalidHttpMethodIntegrationTests.java @@ -33,8 +33,8 @@ public class InvalidHttpMethodIntegrationTests extends AbstractRouterFunctionInt @Override protected RouterFunction routerFunction() { return RouterFunctions.route(RequestPredicates.GET("/"), - request -> ServerResponse.ok().syncBody("FOO")) - .andRoute(RequestPredicates.all(), request -> ServerResponse.ok().syncBody("BAR")); + request -> ServerResponse.ok().body("FOO")) + .andRoute(RequestPredicates.all(), request -> ServerResponse.ok().body("BAR")); } @Test diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/NestedRouteIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/NestedRouteIntegrationTests.java index 93873d62e3..a8ad88c02b 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/NestedRouteIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/NestedRouteIntegrationTests.java @@ -125,7 +125,7 @@ public class NestedRouteIntegrationTests extends AbstractRouterFunctionIntegrati public Mono pattern(ServerRequest request) { String pattern = matchingPattern(request).getPatternString(); - return ServerResponse.ok().syncBody(pattern); + return ServerResponse.ok().body(pattern); } @SuppressWarnings("unchecked") diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MultipartIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MultipartIntegrationTests.java index be7dc30f42..0926c28af8 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MultipartIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MultipartIntegrationTests.java @@ -85,7 +85,7 @@ public class MultipartIntegrationTests extends AbstractHttpHandlerIntegrationTes Mono result = webClient .post() .uri("/requestPart") - .syncBody(generateBody()) + .body(generateBody()) .exchange(); StepVerifier @@ -99,7 +99,7 @@ public class MultipartIntegrationTests extends AbstractHttpHandlerIntegrationTes Mono result = webClient .post() .uri("/requestBodyMap") - .syncBody(generateBody()) + .body(generateBody()) .retrieve() .bodyToMono(String.class); @@ -113,7 +113,7 @@ public class MultipartIntegrationTests extends AbstractHttpHandlerIntegrationTes Mono result = webClient .post() .uri("/requestBodyFlux") - .syncBody(generateBody()) + .body(generateBody()) .retrieve() .bodyToMono(String.class); @@ -127,7 +127,7 @@ public class MultipartIntegrationTests extends AbstractHttpHandlerIntegrationTes Mono result = webClient .post() .uri("/filePartFlux") - .syncBody(generateBody()) + .body(generateBody()) .retrieve() .bodyToMono(String.class); @@ -141,7 +141,7 @@ public class MultipartIntegrationTests extends AbstractHttpHandlerIntegrationTes Mono result = webClient .post() .uri("/filePartMono") - .syncBody(generateBody()) + .body(generateBody()) .retrieve() .bodyToMono(String.class); @@ -155,7 +155,7 @@ public class MultipartIntegrationTests extends AbstractHttpHandlerIntegrationTes Flux result = webClient .post() .uri("/transferTo") - .syncBody(generateBody()) + .body(generateBody()) .retrieve() .bodyToFlux(String.class); @@ -183,7 +183,7 @@ public class MultipartIntegrationTests extends AbstractHttpHandlerIntegrationTes Mono result = webClient .post() .uri("/modelAttribute") - .syncBody(generateBody()) + .body(generateBody()) .retrieve() .bodyToMono(String.class); diff --git a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/client/WebClientExtensionsTests.kt b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/client/WebClientExtensionsTests.kt index e8e5b9413f..7d3bf3fb74 100644 --- a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/client/WebClientExtensionsTests.kt +++ b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/client/WebClientExtensionsTests.kt @@ -27,6 +27,7 @@ import org.junit.Test import org.reactivestreams.Publisher import org.springframework.core.ParameterizedTypeReference import reactor.core.publisher.Mono +import java.util.concurrent.CompletableFuture /** * Mock object based tests for [WebClient] Kotlin extensions @@ -41,9 +42,9 @@ class WebClientExtensionsTests { @Test - fun `RequestBodySpec#body with Publisher and reified type parameters`() { + fun `RequestBodySpec#bodyWithType with Publisher and reified type parameters`() { val body = mockk>>() - requestBodySpec.body(body) + requestBodySpec.bodyWithType(body) verify { requestBodySpec.body(body, object : ParameterizedTypeReference>() {}) } } @@ -51,8 +52,16 @@ class WebClientExtensionsTests { @FlowPreview fun `RequestBodySpec#body with Flow and reified type parameters`() { val body = mockk>>() - requestBodySpec.body(body) - verify { requestBodySpec.body(ofType>>(), object : ParameterizedTypeReference>() {}) } + requestBodySpec.bodyWithType(body) + verify { requestBodySpec.body(ofType(), object : ParameterizedTypeReference>() {}) } + } + + @Test + @FlowPreview + fun `RequestBodySpec#body with CompletableFuture and reified type parameters`() { + val body = mockk>>() + requestBodySpec.bodyWithType>(body) + verify { requestBodySpec.body(ofType(), object : ParameterizedTypeReference>() {}) } } @Test @@ -83,19 +92,6 @@ class WebClientExtensionsTests { } } - @Test - fun body() { - val headerSpec = mockk>() - val supplier: suspend () -> String = mockk() - every { requestBodySpec.body(ofType>()) } returns headerSpec - runBlocking { - requestBodySpec.body(supplier) - } - verify { - requestBodySpec.body(ofType>()) - } - } - @Test fun awaitBody() { val spec = mockk() diff --git a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/ServerResponseExtensionsTests.kt b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/ServerResponseExtensionsTests.kt index b8b8882b9e..00525f3e32 100644 --- a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/ServerResponseExtensionsTests.kt +++ b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/ServerResponseExtensionsTests.kt @@ -19,6 +19,7 @@ package org.springframework.web.reactive.function.server import io.mockk.every import io.mockk.mockk import io.mockk.verify +import io.reactivex.Flowable import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.runBlocking @@ -28,12 +29,14 @@ import org.reactivestreams.Publisher import org.springframework.core.ParameterizedTypeReference import org.springframework.http.MediaType.* import reactor.core.publisher.Mono +import java.util.concurrent.CompletableFuture /** * Mock object based tests for [ServerResponse] Kotlin extensions * * @author Sebastien Deleuze */ +@Suppress("UnassignedFluxMonoInstance") class ServerResponseExtensionsTests { private val bodyBuilder = mockk(relaxed = true) @@ -42,10 +45,51 @@ class ServerResponseExtensionsTests { @Test fun `BodyBuilder#body with Publisher and reified type parameters`() { val body = mockk>>() - bodyBuilder.body(body) + bodyBuilder.bodyWithType(body) verify { bodyBuilder.body(body, object : ParameterizedTypeReference>() {}) } } + @Test + fun `BodyBuilder#body with CompletableFuture and reified type parameters`() { + val body = mockk>>() + bodyBuilder.bodyWithType>(body) + verify { bodyBuilder.body(body, object : ParameterizedTypeReference>() {}) } + } + + @Test + fun `BodyBuilder#body with Flowable and reified type parameters`() { + val body = mockk>>() + bodyBuilder.bodyWithType(body) + verify { bodyBuilder.body(body, object : ParameterizedTypeReference>() {}) } + } + + @Test + fun `BodyBuilder#bodyAndAwait with object parameter`() { + val response = mockk() + val body = "foo" + every { bodyBuilder.body(ofType()) } returns Mono.just(response) + runBlocking { + bodyBuilder.bodyAndAwait(body) + } + verify { + bodyBuilder.body(ofType()) + } + } + + @Test + @FlowPreview + fun `BodyBuilder#bodyAndAwait with flow parameter`() { + val response = mockk() + val body = mockk>>() + every { bodyBuilder.body(ofType>>(), object : ParameterizedTypeReference>() {}) } returns Mono.just(response) + runBlocking { + bodyBuilder.bodyAndAwait(body) + } + verify { + bodyBuilder.body(ofType>>(), object : ParameterizedTypeReference>() {}) + } + } + @Test fun `BodyBuilder#json`() { bodyBuilder.json() @@ -71,42 +115,7 @@ class ServerResponseExtensionsTests { } @Test - fun await() { - val response = mockk() - val builder = mockk>() - every { builder.build() } returns Mono.just(response) - runBlocking { - assertEquals(response, builder.buildAndAwait()) - } - } - - @Test - fun `bodyAndAwait with object parameter`() { - val response = mockk() - val body = "foo" - every { bodyBuilder.syncBody(ofType()) } returns Mono.just(response) - runBlocking { - bodyBuilder.bodyAndAwait(body) - } - verify { - bodyBuilder.syncBody(ofType()) - } - } - - @Test - @FlowPreview - fun bodyFlowAndAwait() { - val response = mockk() - val body = mockk>>() - every { bodyBuilder.body(ofType>>()) } returns Mono.just(response) - runBlocking { - bodyBuilder.bodyFlowAndAwait(body) - } - verify { bodyBuilder.body(ofType>>(), object : ParameterizedTypeReference>() {}) } - } - - @Test - fun `renderAndAwait with a vararg parameter`() { + fun `BodyBuilder#renderAndAwait with a vararg parameter`() { val response = mockk() every { bodyBuilder.render("foo", any(), any()) } returns Mono.just(response) runBlocking { @@ -118,7 +127,7 @@ class ServerResponseExtensionsTests { } @Test - fun `renderAndAwait with a Map parameter`() { + fun `BodyBuilder#renderAndAwait with a Map parameter`() { val response = mockk() val map = mockk>() every { bodyBuilder.render("foo", map) } returns Mono.just(response) @@ -130,5 +139,15 @@ class ServerResponseExtensionsTests { } } + @Test + fun `HeadersBuilder#buildAndAwait`() { + val response = mockk() + val builder = mockk>() + every { builder.build() } returns Mono.just(response) + runBlocking { + assertEquals(response, builder.buildAndAwait()) + } + } + class Foo } diff --git a/src/docs/asciidoc/web/webflux-webclient.adoc b/src/docs/asciidoc/web/webflux-webclient.adoc index 7050c1cacf..f45c0dadd6 100644 --- a/src/docs/asciidoc/web/webflux-webclient.adoc +++ b/src/docs/asciidoc/web/webflux-webclient.adoc @@ -318,7 +318,8 @@ is closed and is not placed back in the pool. [[webflux-client-body]] == Request Body -The request body can be encoded from an `Object`, as the following example shows: +The request body can be encoded from any asynchronous type handled by `ReactiveAdapterRegistry`, +like `Mono` as the following example shows: [source,java,intent=0] [subs="verbatim,quotes"] @@ -348,7 +349,7 @@ You can also have a stream of objects be encoded, as the following example shows .bodyToMono(Void.class); ---- -Alternatively, if you have the actual value, you can use the `syncBody` shortcut method, +Alternatively, if you have the actual value, you can use the `body` shortcut method, as the following example shows: [source,java,intent=0] @@ -359,7 +360,7 @@ as the following example shows: Mono result = client.post() .uri("/persons/{id}", id) .contentType(MediaType.APPLICATION_JSON) - .syncBody(person) + .body(person) .retrieve() .bodyToMono(Void.class); ---- @@ -380,7 +381,7 @@ content is automatically set to `application/x-www-form-urlencoded` by the Mono result = client.post() .uri("/path", id) - .syncBody(formData) + .body(formData) .retrieve() .bodyToMono(Void.class); ---- @@ -428,7 +429,7 @@ explicitly provide the `MediaType` to use for each part through one of the overl builder `part` methods. Once a `MultiValueMap` is prepared, the easiest way to pass it to the the `WebClient` is -through the `syncBody` method, as the following example shows: +through the `body` method, as the following example shows: [source,java,intent=0] [subs="verbatim,quotes"] @@ -437,7 +438,7 @@ through the `syncBody` method, as the following example shows: Mono result = client.post() .uri("/path", id) - .syncBody(builder.build()) + .body(builder.build()) .retrieve() .bodyToMono(Void.class); ----