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