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 22d39766aca..f9429225d94 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 @@ -48,6 +48,7 @@ import org.springframework.util.MultiValueMap; * * @author Arjen Poutsma + * @author Rossen Stoyanchev * @since 5.0.2 * @see RFC 7578 */ @@ -64,38 +65,22 @@ public final class MultipartBodyBuilder { /** - * Builds the multipart body. - * @return the built body - */ - public MultiValueMap> build() { - MultiValueMap> result = new LinkedMultiValueMap<>(this.parts.size()); - for (Map.Entry> entry : this.parts.entrySet()) { - for (DefaultPartBuilder builder : entry.getValue()) { - HttpEntity entity = builder.build(); - result.add(entry.getKey(), entity); - } - } - return result; - } - - /** - * Adds a part to this builder, allowing for further header customization with the returned - * {@link PartBuilder}. - * @param name the name of the part to add (may not be empty) - * @param part the part to add - * @return a builder that allows for further header customization + * Add a part from an Object. + * @param name the name of the part to add + * @param part the part data + * @return builder that allows for further customization of part headers */ public PartBuilder part(String name, Object part) { return part(name, part, null); } /** - * Adds a part to this builder, allowing for further header customization with the returned - * {@link PartBuilder}. - * @param name the name of the part to add (may not be empty) - * @param part the part to add - * @param contentType the {@code Content-Type} header for the part (may be {@code null}) - * @return a builder that allows for further header customization + * Variant of {@link #part(String, Object)} that also accepts a MediaType + * which is used to determine how to encode the part. + * @param name the name of the part to add + * @param part the part data + * @param contentType the media type for the part + * @return builder that allows for further customization of part headers */ public PartBuilder part(String name, Object part, @Nullable MediaType contentType) { Assert.hasLength(name, "'name' must not be empty"); @@ -121,18 +106,18 @@ public final class MultipartBodyBuilder { if (contentType != null) { partHeaders.setContentType(contentType); } - DefaultPartBuilder builder = new DefaultPartBuilder(partBody, partHeaders); + + DefaultPartBuilder builder = new DefaultPartBuilder(partHeaders, partBody); this.parts.add(name, builder); return builder; } /** - * Adds a {@link Publisher} part to this builder, allowing for further header customization with - * the returned {@link PartBuilder}. - * @param name the name of the part to add (may not be empty) - * @param publisher the contents of the part to add - * @param elementClass the class of elements contained in the publisher - * @return a builder that allows for further header customization + * Add an asynchronous part with {@link Publisher}-based content. + * @param name the name of the part to add + * @param publisher the part contents + * @param elementClass the type of elements contained in the publisher + * @return builder that allows for further customization of part headers */ public > PartBuilder asyncPart(String name, P publisher, Class elementClass) { @@ -143,21 +128,21 @@ public final class MultipartBodyBuilder { Assert.notNull(publisher, "'publisher' must not be null"); Assert.notNull(elementType, "'elementType' must not be null"); - HttpHeaders partHeaders = new HttpHeaders(); - PublisherPartBuilder builder = - new PublisherPartBuilder<>(publisher, elementClass, partHeaders); + HttpHeaders headers = new HttpHeaders(); + PublisherPartBuilder builder = new PublisherPartBuilder<>(headers, publisher, elementClass); this.parts.add(name, builder); return builder; } /** - * Adds a {@link Publisher} part to this builder, allowing for further header customization with - * the returned {@link PartBuilder}. - * @param name the name of the part to add (may not be empty) - * @param publisher the contents of the part to add + * Variant of {@link #asyncPart(String, Publisher, Class)} that accepts a + * {@link ParameterizedTypeReference} for the element type, which allows + * specifying generic type information. + * @param name the name of the part to add + * @param publisher the part contents * @param typeReference the type of elements contained in the publisher - * @return a builder that allows for further header customization + * @return builder that allows for further customization of part headers */ public > PartBuilder asyncPart(String name, P publisher, ParameterizedTypeReference typeReference) { @@ -168,30 +153,44 @@ public final class MultipartBodyBuilder { Assert.notNull(publisher, "'publisher' must not be null"); Assert.notNull(elementType1, "'typeReference' must not be null"); - HttpHeaders partHeaders = new HttpHeaders(); - PublisherPartBuilder builder = - new PublisherPartBuilder<>(publisher, typeReference, partHeaders); + HttpHeaders headers = new HttpHeaders(); + PublisherPartBuilder builder = new PublisherPartBuilder<>(publisher, typeReference, headers); this.parts.add(name, builder); return builder; } /** - * Builder interface that allows for customization of part headers. + * Return a {@code MultiValueMap} with the configured parts. + */ + public MultiValueMap> build() { + MultiValueMap> result = new LinkedMultiValueMap<>(this.parts.size()); + for (Map.Entry> entry : this.parts.entrySet()) { + for (DefaultPartBuilder builder : entry.getValue()) { + HttpEntity entity = builder.build(); + result.add(entry.getKey(), entity); + } + } + return result; + } + + + /** + * Builder that allows for further customization of part headers. */ public interface PartBuilder { /** - * Add the given part-specific header values under the given name. + * Add part header values. * @param headerName the part header name * @param headerValues the part header value(s) * @return this builder - * @see HttpHeaders#add(String, String) + * @see HttpHeaders#addAll(String, List) */ PartBuilder header(String headerName, String... headerValues); /** - * Manipulate the part's headers with the given consumer. - * @param headersConsumer a function that consumes the {@code HttpHeaders} + * Manipulate the part headers through the given consumer. + * @param headersConsumer consumer to manipulate the part headers with * @return this builder */ PartBuilder headers(Consumer headersConsumer); @@ -200,14 +199,15 @@ public final class MultipartBodyBuilder { private static class DefaultPartBuilder implements PartBuilder { + protected final HttpHeaders headers; + @Nullable protected final Object body; - protected final HttpHeaders headers; - public DefaultPartBuilder(@Nullable Object body, HttpHeaders headers) { - this.body = body; + public DefaultPartBuilder(HttpHeaders headers, @Nullable Object body) { this.headers = headers; + this.body = body; } @Override @@ -228,20 +228,19 @@ public final class MultipartBodyBuilder { } } - private static class PublisherPartBuilder> - extends DefaultPartBuilder { + + private static class PublisherPartBuilder> extends DefaultPartBuilder { private final ResolvableType resolvableType; - public PublisherPartBuilder(P body, Class elementClass, HttpHeaders headers) { - super(body, headers); + + public PublisherPartBuilder(HttpHeaders headers, P body, Class elementClass) { + super(headers, body); this.resolvableType = ResolvableType.forClass(elementClass); } - public PublisherPartBuilder(P body, ParameterizedTypeReference typeReference, - HttpHeaders headers) { - - super(body, headers); + public PublisherPartBuilder(P body, ParameterizedTypeReference typeReference, HttpHeaders headers) { + super(headers, body); this.resolvableType = ResolvableType.forType(typeReference); } @@ -250,14 +249,15 @@ public final class MultipartBodyBuilder { public HttpEntity build() { P publisher = (P) this.body; Assert.state(publisher != null, "'publisher' must not be null"); - return new PublisherEntity<>(publisher, this.resolvableType, this.headers); + return new PublisherEntity<>(this.headers, publisher, this.resolvableType); } } /** - * Specific subtype of {@link HttpEntity} for containing {@link Publisher}s as body. - * Exposes the type contained in the publisher through {@link #getResolvableType()}. + * Specialization of {@link HttpEntity} for use with a + * {@link Publisher}-based body, for which we also need to keep track of + * the element type. * @param The type contained in the publisher * @param

The publisher */ @@ -266,8 +266,9 @@ public final class MultipartBodyBuilder { private final ResolvableType resolvableType; - PublisherEntity(P publisher, ResolvableType resolvableType, - @Nullable MultiValueMap headers) { + private PublisherEntity(@Nullable MultiValueMap headers, P publisher, + ResolvableType resolvableType) { + super(publisher, headers); Assert.notNull(publisher, "'publisher' must not be null"); Assert.notNull(resolvableType, "'resolvableType' must not be null"); @@ -275,7 +276,7 @@ public final class MultipartBodyBuilder { } /** - * Return the resolvable type for this entry. + * Return the element type for the {@code Publisher} body. */ public ResolvableType getResolvableType() { return this.resolvableType; diff --git a/spring-web/src/test/java/org/springframework/http/client/MultipartBodyBuilderTests.java b/spring-web/src/test/java/org/springframework/http/client/MultipartBodyBuilderTests.java index 0fc970b8f9c..d948cd6fd2b 100644 --- a/spring-web/src/test/java/org/springframework/http/client/MultipartBodyBuilderTests.java +++ b/spring-web/src/test/java/org/springframework/http/client/MultipartBodyBuilderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -37,48 +37,58 @@ import static org.junit.Assert.*; public class MultipartBodyBuilderTests { @Test - public void builder() throws Exception { + public void builder() { + + MultipartBodyBuilder builder = new MultipartBodyBuilder(); + MultiValueMap multipartData = new LinkedMultiValueMap<>(); multipartData.add("form field", "form value"); + builder.part("key", multipartData).header("foo", "bar"); + Resource logo = new ClassPathResource("/org/springframework/http/converter/logo.jpg"); + builder.part("logo", logo).header("baz", "qux"); + HttpHeaders entityHeaders = new HttpHeaders(); entityHeaders.add("foo", "bar"); HttpEntity entity = new HttpEntity<>("body", entityHeaders); - Publisher publisher = Flux.just("foo", "bar", "baz"); - - MultipartBodyBuilder builder = new MultipartBodyBuilder(); - builder.part("key", multipartData).header("foo", "bar"); - builder.part("logo", logo).header("baz", "qux"); builder.part("entity", entity).header("baz", "qux"); + + Publisher publisher = Flux.just("foo", "bar", "baz"); builder.asyncPart("publisherClass", publisher, String.class).header("baz", "qux"); builder.asyncPart("publisherPtr", publisher, new ParameterizedTypeReference() {}).header("baz", "qux"); MultiValueMap> result = builder.build(); assertEquals(5, result.size()); - assertNotNull(result.getFirst("key")); - assertEquals(multipartData, result.getFirst("key").getBody()); - assertEquals("bar", result.getFirst("key").getHeaders().getFirst("foo")); + HttpEntity resultEntity = result.getFirst("key"); + assertNotNull(resultEntity); + assertEquals(multipartData, resultEntity.getBody()); + assertEquals("bar", resultEntity.getHeaders().getFirst("foo")); - assertNotNull(result.getFirst("logo")); - assertEquals(logo, result.getFirst("logo").getBody()); - assertEquals("qux", result.getFirst("logo").getHeaders().getFirst("baz")); + resultEntity = result.getFirst("logo"); + assertNotNull(resultEntity); + assertEquals(logo, resultEntity.getBody()); + assertEquals("qux", resultEntity.getHeaders().getFirst("baz")); - assertNotNull(result.getFirst("entity")); - assertEquals("body", result.getFirst("entity").getBody()); - assertEquals("bar", result.getFirst("entity").getHeaders().getFirst("foo")); - assertEquals("qux", result.getFirst("entity").getHeaders().getFirst("baz")); + resultEntity = result.getFirst("entity"); + assertNotNull(resultEntity); + assertEquals("body", resultEntity.getBody()); + assertEquals("bar", resultEntity.getHeaders().getFirst("foo")); + assertEquals("qux", resultEntity.getHeaders().getFirst("baz")); - assertNotNull(result.getFirst("publisherClass")); - assertEquals(publisher, result.getFirst("publisherClass").getBody()); - assertEquals(ResolvableType.forClass(String.class), ((MultipartBodyBuilder.PublisherEntity) result.getFirst("publisherClass")).getResolvableType()); - assertEquals("qux", result.getFirst("publisherClass").getHeaders().getFirst("baz")); + resultEntity = result.getFirst("publisherClass"); + assertNotNull(resultEntity); + assertEquals(publisher, resultEntity.getBody()); + assertEquals(ResolvableType.forClass(String.class), + ((MultipartBodyBuilder.PublisherEntity) resultEntity).getResolvableType()); + assertEquals("qux", resultEntity.getHeaders().getFirst("baz")); - assertNotNull(result.getFirst("publisherPtr")); - assertEquals(publisher, result.getFirst("publisherPtr").getBody()); - assertEquals(ResolvableType.forClass(String.class), ((MultipartBodyBuilder.PublisherEntity) result.getFirst("publisherPtr")).getResolvableType()); - assertEquals("qux", result.getFirst("publisherPtr").getHeaders().getFirst("baz")); + resultEntity = result.getFirst("publisherPtr"); + assertNotNull(resultEntity); + assertEquals(publisher, resultEntity.getBody()); + assertEquals(ResolvableType.forClass(String.class), + ((MultipartBodyBuilder.PublisherEntity) resultEntity).getResolvableType()); + assertEquals("qux", resultEntity.getHeaders().getFirst("baz")); } - } \ No newline at end of file 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 15fa32670d2..f1a0cfbdebd 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 @@ -200,17 +200,15 @@ public abstract class BodyInserters { * *

Note that you can also use the {@code syncBody(Object)} method in the * request builders of both the {@code WebClient} and {@code WebTestClient}. - * In that case the setting of the content type is also not required, just - * be sure the map contains String values only or otherwise it would be + * 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 * interpreted as a multipart request. * * @param formData the form data to write to the output message - * @return a {@code FormInserter} that writes form data + * @return the inserter that allows adding more form data */ public static FormInserter fromFormData(MultiValueMap formData) { - Assert.notNull(formData, "'formData' must not be null"); - return new DefaultFormInserter().with(formData); } @@ -218,36 +216,27 @@ public abstract class BodyInserters { * Return a {@link FormInserter} that writes the given key-value pair as * URL-encoded form data. 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 name the key to add to the form * @param value the value to add to the form - * @return a {@code FormInserter} that writes form data + * @return the inserter that allows adding more form data */ - public static FormInserter fromFormData(String key, String value) { - Assert.notNull(key, "'key' must not be null"); + public static FormInserter fromFormData(String name, String value) { + Assert.notNull(name, "'key' must not be null"); Assert.notNull(value, "'value' must not be null"); - - return new DefaultFormInserter().with(key, value); + return new DefaultFormInserter().with(name, value); } /** - * Return a {@code FormInserter} that writes the given {@code MultiValueMap} - * as multipart data. 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 {@link org.springframework.http.client.MultipartBodyBuilder - * MultipartBodyBuilder}. Also the returned inserter allows for additional - * entries to be added via {@link FormInserter#with(String, Object)}. + * Return a {@link MultipartInserter} that writes the given + * {@code MultiValueMap} as multipart data. Values in the map can be an + * Object or an {@link HttpEntity}. * - *

Note that you can also use the {@code syncBody(Object)} method in the - * request builders of both the {@code WebClient} and {@code WebTestClient}. - * In that case the setting of the content type is also not required, just - * be sure the map contains at least one non-String value or otherwise, - * without a content-type header as a hint, it would be interpreted as a - * plain form data request. + *

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}. * * @param multipartData the form data to write to the output message - * @return a {@code BodyInserter} that writes multipart data + * @return the inserter that allows adding more parts * @see MultipartBodyBuilder */ public static MultipartInserter fromMultipartData(MultiValueMap multipartData) { @@ -256,57 +245,69 @@ public abstract class BodyInserters { } /** - * A variant of {@link #fromMultipartData(MultiValueMap)} for adding - * parts as name-value pairs in-line vs building a {@code MultiValueMap} - * and passing it in. - * @param key the part name + * Return a {@link MultipartInserter} that writes the given parts, + * as multipart data. Values in the map can be an 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}. + * + * @param name the part name * @param value the part value, an Object or {@code HttpEntity} - * @return a {@code FormInserter} that can writes the provided multipart - * data and also allows adding more parts + * @return the inserter that allows adding more parts */ - public static MultipartInserter fromMultipartData(String key, Object value) { - Assert.notNull(key, "'key' must not be null"); + public static MultipartInserter fromMultipartData(String name, Object value) { + Assert.notNull(name, "'key' must not be null"); Assert.notNull(value, "'value' must not be null"); - - return new DefaultMultipartInserter().with(key, value); + return new DefaultMultipartInserter().with(name, 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 + * Return a {@link MultipartInserter} that writes the given asynchronous parts, + * 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}. + * + * @param name 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 + * @return the inserter that allows adding more parts */ - public static > MultipartInserter fromMultipartAsyncData(String key, + public static > MultipartInserter fromMultipartAsyncData(String name, P publisher, Class elementClass) { - Assert.notNull(key, "'key' must not be null"); + Assert.notNull(name, "'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); + return new DefaultMultipartInserter().withPublisher(name, 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 + * Variant of {@link #fromMultipartAsyncData(String, Publisher, Class)} that + * accepts a {@link ParameterizedTypeReference} for the element type, which + * 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}. + * + * @param name 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 + * @return the inserter that allows adding more parts */ - public static > MultipartInserter fromMultipartAsyncData(String key, + public static > MultipartInserter fromMultipartAsyncData(String name, P publisher, ParameterizedTypeReference typeReference) { - Assert.notNull(key, "'key' must not be null"); + Assert.notNull(name, "'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); + return new DefaultMultipartInserter().withPublisher(name, publisher, typeReference); } /** @@ -375,26 +376,25 @@ public abstract class BodyInserters { /** - * Sub-interface of {@link BodyInserter} that allows for additional (multipart) form data to be - * added. + * Extension of {@link BodyInserter} that allows for adding form data or + * multipart form data. */ - // 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> { + public interface FormInserter extends BodyInserter, ClientHttpRequest> { + + // FormInserter is parameterized to ClientHttpRequest (for client-side use only) /** * 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 + * @return this inserter for adding more parts */ FormInserter with(String key, @Nullable T value); /** * Adds the specified values to the form. * @param values the values to be added - * @return this inserter + * @return this inserter for adding more parts */ FormInserter with(MultiValueMap values); @@ -402,30 +402,30 @@ public abstract class BodyInserters { /** - * Extension of {@link FormInserter} that has methods for adding asynchronous part data. + * Extension of {@link FormInserter} that allows for adding asynchronous parts. */ 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 + * Add an asynchronous part with {@link Publisher}-based content. + * @param name the name of the part to add + * @param publisher the part contents + * @param elementClass the type of elements contained in the publisher + * @return this inserter for adding more parts */ - > MultipartInserter withPublisher(String key, P publisher, + > MultipartInserter withPublisher(String name, P publisher, Class elementClass); /** - * Adds the specified publisher as a part. - * - * @param key the key to be added + * Variant of {@link #withPublisher(String, Publisher, Class)} that accepts a + * {@link ParameterizedTypeReference} for the element type, which allows + * specifying generic type information. + * @param name 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 + * @return this inserter for adding more parts */ - > MultipartInserter withPublisher(String key, P publisher, + > MultipartInserter withPublisher(String name, P publisher, ParameterizedTypeReference typeReference); } @@ -435,9 +435,11 @@ public abstract class BodyInserters { private final MultiValueMap data = new LinkedMultiValueMap<>(); + public DefaultFormInserter() { } + @Override public FormInserter with(String key, @Nullable String value) { this.data.add(key, value); @@ -465,9 +467,11 @@ public abstract class BodyInserters { private final MultipartBodyBuilder builder = new MultipartBodyBuilder(); + public DefaultMultipartInserter() { } + @Override public MultipartInserter with(String key, @Nullable Object value) { Assert.notNull(value, "'value' must not be null"); @@ -492,18 +496,18 @@ public abstract class BodyInserters { } @Override - public > MultipartInserter withPublisher(String key, + public > MultipartInserter withPublisher(String name, P publisher, Class elementClass) { - this.builder.asyncPart(key, publisher, elementClass); + this.builder.asyncPart(name, publisher, elementClass); return this; } @Override - public > MultipartInserter withPublisher(String key, + public > MultipartInserter withPublisher(String name, P publisher, ParameterizedTypeReference typeReference) { - this.builder.asyncPart(key, publisher, typeReference); + this.builder.asyncPart(name, publisher, typeReference); return this; }