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 3a76cc7e4cc..ab9f65e77b6 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 @@ -34,7 +34,9 @@ import org.springframework.http.codec.HttpMessageWriter; import org.springframework.http.codec.ServerSentEvent; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; /** @@ -176,8 +178,9 @@ public abstract class BodyInserters { } /** - * Return a {@code BodyInserter} that writes the given {@code MultiValueMap} - * as URL-encoded form data. + * Return a {@link FormInserter} that writes the given {@code MultiValueMap} as URL-encoded + * form data. Note that the returned inserter allows for additional entries to be added via + * {@link FormInserter#with(String, Object)}. * *

Note: you can also use the {@code syncBody(Object)} * method in the request builders of both the {@code WebClient} and @@ -185,26 +188,40 @@ public abstract class BodyInserters { * required. Just make sure the map contains String values only. * * @param formData the form data to write to the output message - * @return a {@code BodyInserter} that writes form data + * @return a {@code FormInserter} that writes form data */ - // Note that the returned BodyInserter is parameterized to ClientHttpRequest, not + // Note that the returned FormInserter is parameterized to ClientHttpRequest, not // ReactiveHttpOutputMessage like other methods, since sending form data only typically happens // on the client-side - public static BodyInserter, ClientHttpRequest> fromFormData( - MultiValueMap formData) { + public static FormInserter fromFormData(MultiValueMap formData) { Assert.notNull(formData, "'formData' must not be null"); - return (outputMessage, context) -> { - HttpMessageWriter> messageWriter = - findMessageWriter(context, FORM_TYPE, MediaType.APPLICATION_FORM_URLENCODED); - return messageWriter.write(Mono.just(formData), FORM_TYPE, - MediaType.APPLICATION_FORM_URLENCODED, outputMessage, context.hints()); - }; + + return DefaultFormInserter.forFormData().with(formData); } /** - * Return a {@code BodyInserter} that writes the given {@code MultiValueMap} - * as multipart data. + * Return a {@link FormInserter} that writes the given key-value pair as URL-encoded + * form data. Note that the returned inserter allows for additional entries to be added via + * {@link FormInserter#with(String, Object)}. + * @param key the key to add to the form + * @param value the value to add to the form + * @return a {@code FormInserter} that writes form data + */ + // Note that the returned FormInserter is parameterized to ClientHttpRequest, not + // ReactiveHttpOutputMessage like other methods, since sending form data only typically happens + // on the client-side + public static FormInserter fromFormData(String key, String value) { + Assert.notNull(key, "'key' must not be null"); + Assert.notNull(value, "'value' must not be null"); + + return DefaultFormInserter.forFormData().with(key, value); + } + + /** + * Return a {@code FormInserter} that writes the given {@code MultiValueMap} + * as multipart data. Note that the returned inserter allows for additional entries to be added + * via {@link FormInserter#with(String, Object)}. * *

Note: you can also use the {@code syncBody(Object)} * method in the request builders of both the {@code WebClient} and @@ -213,21 +230,34 @@ public abstract class BodyInserters { * value or otherwise it would be interpreted as plan form data. * * @param multipartData the form data to write to the output message - * @return a {@code BodyInserter} that writes form data + * @return a {@code BodyInserter} that writes multipart data */ // Note that the returned BodyInserter is parameterized to ClientHttpRequest, not // ReactiveHttpOutputMessage like other methods, since sending form data only typically happens // on the client-side - public static BodyInserter, ClientHttpRequest> fromMultipartData( - MultiValueMap multipartData) { + public static FormInserter fromMultipartData(MultiValueMap multipartData) { Assert.notNull(multipartData, "'multipartData' must not be null"); - return (outputMessage, context) -> { - HttpMessageWriter> messageWriter = - findMessageWriter(context, MULTIPART_VALUE_TYPE, MediaType.MULTIPART_FORM_DATA); - return messageWriter.write(Mono.just(multipartData), FORM_TYPE, - MediaType.MULTIPART_FORM_DATA, outputMessage, context.hints()); - }; + + return DefaultFormInserter.forMultipartData().with(multipartData); + } + + /** + * Return a {@code FormInserter} that writes the key-value pair as multipart data. Note that + * the returned inserter allows for additional entries to be added via + * {@link FormInserter#with(String, Object)}. + * @param key the key to add to the form + * @param value the value to add to the form + * @return a {@code FormInserter} that writes multipart data + */ + // Note that the returned BodyInserter is parameterized to ClientHttpRequest, not + // ReactiveHttpOutputMessage like other methods, since sending form data only typically happens + // on the client-side + public static FormInserter fromMultipartData(String key, T value) { + Assert.notNull(key, "'key' must not be null"); + Assert.notNull(value, "'value' must not be null"); + + return DefaultFormInserter.forMultipartData().with(key, value); } /** @@ -293,4 +323,71 @@ public abstract class BodyInserters { return (HttpMessageWriter) messageWriter; } + + /** + * Sub-interface of {@link BodyInserter} that allows for additional (multipart) form data to be + * added. + */ + public interface FormInserter extends + BodyInserter, ClientHttpRequest> { + + /** + * Adds the specified key-value pair to the form. + * @param key the key to be added + * @param value the value to be added + * @return this inserter + */ + FormInserter with(String key, @Nullable T value); + + /** + * Adds the specified values to the form. + * @param values the values to be added + * @return this inserter + */ + FormInserter with(MultiValueMap values); + + } + + private static class DefaultFormInserter implements FormInserter { + + private final MultiValueMap data = new LinkedMultiValueMap<>(); + + private final ResolvableType type; + + private final MediaType mediaType; + + + private DefaultFormInserter(ResolvableType type, MediaType mediaType) { + this.type = type; + this.mediaType = mediaType; + } + + public static FormInserter forFormData() { + return new DefaultFormInserter<>(FORM_TYPE, MediaType.APPLICATION_FORM_URLENCODED); + } + + public static FormInserter forMultipartData() { + return new DefaultFormInserter<>(MULTIPART_VALUE_TYPE, MediaType.MULTIPART_FORM_DATA); + } + + @Override + public FormInserter with(String key, @Nullable T value) { + this.data.add(key, value); + return this; + } + + @Override + public FormInserter with(MultiValueMap values) { + this.data.addAll(values); + return this; + } + + @Override + public Mono insert(ClientHttpRequest outputMessage, Context context) { + HttpMessageWriter> messageWriter = + findMessageWriter(context, this.type, this.mediaType); + return messageWriter.write(Mono.just(this.data), this.type, this.mediaType, + outputMessage, context.hints()); + } + } } 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 33e77bb1eeb..43f1236112f 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 @@ -39,6 +39,7 @@ import org.springframework.core.codec.CharSequenceEncoder; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.core.io.buffer.DefaultDataBuffer; import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.HttpMethod; @@ -118,8 +119,7 @@ public class BodyInsertersTests { Mono result = inserter.insert(response, this.context); StepVerifier.create(result).expectComplete().verify(); - ByteBuffer byteBuffer = ByteBuffer.wrap(body.getBytes(UTF_8)); - DataBuffer buffer = new DefaultDataBufferFactory().wrap(byteBuffer); + DataBuffer buffer = new DefaultDataBufferFactory().wrap(body.getBytes(UTF_8)); StepVerifier.create(response.getBody()) .expectNext(buffer) .expectComplete() @@ -187,6 +187,7 @@ public class BodyInsertersTests { .consumeNextWith(dataBuffer -> { byte[] resultBytes = new byte[dataBuffer.readableByteCount()]; dataBuffer.read(resultBytes); + DataBufferUtils.release(dataBuffer); assertArrayEquals(expectedBytes, resultBytes); }) .expectComplete() @@ -229,6 +230,7 @@ public class BodyInsertersTests { .consumeNextWith(dataBuffer -> { byte[] resultBytes = new byte[dataBuffer.readableByteCount()]; dataBuffer.read(resultBytes); + DataBufferUtils.release(dataBuffer); assertArrayEquals(expectedBytes, resultBytes); }) .expectComplete() @@ -248,7 +250,7 @@ public class BodyInsertersTests { } @Test - public void ofFormData() throws Exception { + public void fromFormDataMap() throws Exception { MultiValueMap body = new LinkedMultiValueMap<>(); body.set("name 1", "value 1"); body.add("name 2", "value 2+1"); @@ -266,6 +268,32 @@ public class BodyInsertersTests { .consumeNextWith(dataBuffer -> { byte[] resultBytes = new byte[dataBuffer.readableByteCount()]; dataBuffer.read(resultBytes); + DataBufferUtils.release(dataBuffer); + assertArrayEquals("name+1=value+1&name+2=value+2%2B1&name+2=value+2%2B2&name+3".getBytes(StandardCharsets.UTF_8), + resultBytes); + }) + .expectComplete() + .verify(); + + } + + @Test + public void fromFormDataWith() throws Exception { + BodyInserter, ClientHttpRequest> + inserter = BodyInserters.fromFormData("name 1", "value 1") + .with("name 2", "value 2+1") + .with("name 2", "value 2+2") + .with("name 3", null); + + MockClientHttpRequest request = new MockClientHttpRequest(HttpMethod.GET, URI.create("http://example.com")); + Mono result = inserter.insert(request, this.context); + StepVerifier.create(result).expectComplete().verify(); + + StepVerifier.create(request.getBody()) + .consumeNextWith(dataBuffer -> { + byte[] resultBytes = new byte[dataBuffer.readableByteCount()]; + dataBuffer.read(resultBytes); + DataBufferUtils.release(dataBuffer); assertArrayEquals("name+1=value+1&name+2=value+2%2B1&name+2=value+2%2B2&name+3".getBytes(StandardCharsets.UTF_8), resultBytes); })