From 7035ee7ebb63f14e8947fe8c014bded3adfc028f Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 21 Dec 2017 17:33:55 +0100 Subject: [PATCH] Support Publishers for multipart data in BodyInserters This commit uses the changes in the previous commit to support Publishers as parts for multipart data. Issue: SPR-16307 --- .../web/reactive/function/BodyInserters.java | 174 ++++++++++++++---- .../reactive/function/BodyInsertersTests.java | 18 ++ 2 files changed, 154 insertions(+), 38 deletions(-) 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 ad73923a230..225db83debb 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 @@ -17,6 +17,7 @@ package org.springframework.web.reactive.function; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; @@ -27,8 +28,10 @@ import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ResolvableType; import org.springframework.core.io.Resource; import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.http.HttpEntity; import org.springframework.http.MediaType; import org.springframework.http.ReactiveHttpOutputMessage; +import org.springframework.http.client.MultipartBodyBuilder; import org.springframework.http.client.reactive.ClientHttpRequest; import org.springframework.http.codec.HttpMessageWriter; import org.springframework.http.codec.ServerSentEvent; @@ -204,14 +207,11 @@ public abstract class BodyInserters { * @param formData the form data to write to the output message * @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(MultiValueMap formData) { Assert.notNull(formData, "'formData' must not be null"); - return DefaultFormInserter.forFormData().with(formData); + return new DefaultFormInserter().with(formData); } /** @@ -222,14 +222,11 @@ public abstract class BodyInserters { * @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 new DefaultFormInserter().with(key, value); } /** @@ -251,15 +248,11 @@ public abstract class BodyInserters { * * @param multipartData the form data to write to the output message * @return a {@code BodyInserter} that writes multipart data + * @see MultipartBodyBuilder */ - // 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(MultiValueMap multipartData) { - + public static MultipartInserter fromMultipartData(MultiValueMap multipartData) { Assert.notNull(multipartData, "'multipartData' must not be null"); - - return DefaultFormInserter.forMultipartData().with(multipartData); + return new DefaultMultipartInserter().with(multipartData); } /** @@ -271,14 +264,49 @@ public abstract class BodyInserters { * @return a {@code FormInserter} that can writes the provided multipart * data and also allows adding more parts */ - // 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) { + public static MultipartInserter fromMultipartData(String key, Object value) { Assert.notNull(key, "'key' must not be null"); Assert.notNull(value, "'value' must not be null"); - return DefaultFormInserter.forMultipartData().with(key, value); + return new DefaultMultipartInserter().with(key, value); + } + + /** + * A variant of {@link #fromMultipartData(MultiValueMap)} for adding asynchronous data as a + * part in-line vs building a {@code MultiValueMap} and passing it in. + * @param key the part name + * @param publisher the publisher that forms the part value + * @param elementClass the class contained in the {@code publisher} + * @return a {@code FormInserter} that can writes the provided multipart + * data and also allows adding more parts + */ + public static > MultipartInserter fromMultipartAsyncData(String key, + P publisher, Class elementClass) { + + Assert.notNull(key, "'key' must not be null"); + Assert.notNull(publisher, "'publisher' must not be null"); + Assert.notNull(elementClass, "'elementClass' must not be null"); + + return new DefaultMultipartInserter().withPublisher(key, publisher, elementClass); + } + + /** + * A variant of {@link #fromMultipartData(MultiValueMap)} for adding asynchronous data as a + * part in-line vs building a {@code MultiValueMap} and passing it in. + * @param key the part name + * @param publisher the publisher that forms the part value + * @param typeReference the type contained in the {@code publisher} + * @return a {@code FormInserter} that can writes the provided multipart + * data and also allows adding more parts + */ + public static > MultipartInserter fromMultipartAsyncData(String key, + P publisher, ParameterizedTypeReference typeReference) { + + Assert.notNull(key, "'key' must not be null"); + Assert.notNull(publisher, "'publisher' must not be null"); + Assert.notNull(typeReference, "'typeReference' must not be null"); + + return new DefaultMultipartInserter().withPublisher(key, publisher, typeReference); } /** @@ -350,6 +378,8 @@ public abstract class BodyInserters { * Sub-interface of {@link BodyInserter} that allows for additional (multipart) form data to be * added. */ + // Note that FormInserter is parameterized to ClientHttpRequest, not ReactiveHttpOutputMessage + // like other return values methods, since sending form data only typically happens on the client-side public interface FormInserter extends BodyInserter, ClientHttpRequest> { @@ -370,45 +400,113 @@ public abstract class BodyInserters { } - private static class DefaultFormInserter implements FormInserter { - private final MultiValueMap data = new LinkedMultiValueMap<>(); + /** + * Extension of {@link FormInserter} that has methods for adding asynchronous part data. + */ + public interface MultipartInserter extends FormInserter { + + /** + * Adds the specified publisher as a part. + * + * @param key the key to be added + * @param publisher the publisher to be added as value + * @param elementClass the class of elements contained in {@code publisher} + * @return this inserter + */ + > MultipartInserter withPublisher(String key, P publisher, + Class elementClass); + + /** + * Adds the specified publisher as a part. + * + * @param key the key to be added + * @param publisher the publisher to be added as value + * @param typeReference the type of elements contained in {@code publisher} + * @return this inserter + */ + > MultipartInserter withPublisher(String key, P publisher, + ParameterizedTypeReference typeReference); + + } + + + private static class DefaultFormInserter implements FormInserter { + + private final MultiValueMap data = new LinkedMultiValueMap<>(); + + public DefaultFormInserter() { + } + + @Override + public FormInserter with(String key, @Nullable String 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, FORM_TYPE, MediaType.APPLICATION_FORM_URLENCODED); + return messageWriter.write(Mono.just(this.data), FORM_TYPE, + MediaType.APPLICATION_FORM_URLENCODED, + outputMessage, context.hints()); + } + } - private final ResolvableType type; - private final MediaType mediaType; + private static class DefaultMultipartInserter implements MultipartInserter { + private final MultipartBodyBuilder builder = new MultipartBodyBuilder(); - private DefaultFormInserter(ResolvableType type, MediaType mediaType) { - this.type = type; - this.mediaType = mediaType; + public DefaultMultipartInserter() { } - public static FormInserter forFormData() { - return new DefaultFormInserter<>(FORM_TYPE, MediaType.APPLICATION_FORM_URLENCODED); + @Override + public MultipartInserter with(String key, @Nullable Object value) { + Assert.notNull(value, "'value' must not be null"); + this.builder.part(key, value); + return this; } - public static FormInserter forMultipartData() { - return new DefaultFormInserter<>(MULTIPART_VALUE_TYPE, MediaType.MULTIPART_FORM_DATA); + @Override + public MultipartInserter with(MultiValueMap values) { + Assert.notNull(values, "'values' must not be null"); + for (Map.Entry> entry : values.entrySet()) { + this.builder.part(entry.getKey(), entry.getValue()); + } + return this; } @Override - public FormInserter with(String key, @Nullable T value) { - this.data.add(key, value); + public > MultipartInserter withPublisher(String key, + P publisher, Class elementClass) { + + this.builder.asyncPart(key, publisher, elementClass); return this; } @Override - public FormInserter with(MultiValueMap values) { - this.data.addAll(values); + public > MultipartInserter withPublisher(String key, + P publisher, ParameterizedTypeReference typeReference) { + + this.builder.asyncPart(key, publisher, typeReference); 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, + HttpMessageWriter>> messageWriter = + findMessageWriter(context, MULTIPART_VALUE_TYPE, MediaType.MULTIPART_FORM_DATA); + MultiValueMap> body = this.builder.build(); + return messageWriter.write(Mono.just(body), MULTIPART_VALUE_TYPE, + MediaType.MULTIPART_FORM_DATA, 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 43f1236112f..ba46c35a4f3 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 @@ -53,6 +53,7 @@ import org.springframework.http.codec.ResourceHttpMessageWriter; import org.springframework.http.codec.ServerSentEvent; import org.springframework.http.codec.ServerSentEventHttpMessageWriter; import org.springframework.http.codec.json.Jackson2JsonEncoder; +import org.springframework.http.codec.multipart.MultipartHttpMessageWriter; import org.springframework.http.codec.xml.Jaxb2XmlEncoder; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; @@ -89,6 +90,7 @@ public class BodyInsertersTests { messageWriters.add(new ServerSentEventHttpMessageWriter(jsonEncoder)); messageWriters.add(new FormHttpMessageWriter()); messageWriters.add(new EncoderHttpMessageWriter<>(CharSequenceEncoder.allMimeTypes())); + messageWriters.add(new MultipartHttpMessageWriter(messageWriters)); this.context = new BodyInserter.Context() { @Override @@ -302,6 +304,22 @@ public class BodyInsertersTests { } + @Test + public void fromMultipartData() throws Exception { + MultiValueMap map = new LinkedMultiValueMap<>(); + map.set("name 3", "value 3"); + + BodyInserters.FormInserter inserter = + BodyInserters.fromMultipartData("name 1", "value 1") + .withPublisher("name 2", Flux.just("foo", "bar", "baz"), String.class) + .with(map); + + MockClientHttpRequest request = new MockClientHttpRequest(HttpMethod.GET, URI.create("http://example.com")); + Mono result = inserter.insert(request, this.context); + StepVerifier.create(result).expectComplete().verify(); + + } + @Test public void ofDataBuffers() throws Exception { DefaultDataBufferFactory factory = new DefaultDataBufferFactory();