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
This commit is contained in:
parent
f23612c3a3
commit
7035ee7ebb
|
|
@ -17,6 +17,7 @@
|
||||||
package org.springframework.web.reactive.function;
|
package org.springframework.web.reactive.function;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
|
@ -27,8 +28,10 @@ import org.springframework.core.ParameterizedTypeReference;
|
||||||
import org.springframework.core.ResolvableType;
|
import org.springframework.core.ResolvableType;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.core.io.buffer.DataBuffer;
|
import org.springframework.core.io.buffer.DataBuffer;
|
||||||
|
import org.springframework.http.HttpEntity;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ReactiveHttpOutputMessage;
|
import org.springframework.http.ReactiveHttpOutputMessage;
|
||||||
|
import org.springframework.http.client.MultipartBodyBuilder;
|
||||||
import org.springframework.http.client.reactive.ClientHttpRequest;
|
import org.springframework.http.client.reactive.ClientHttpRequest;
|
||||||
import org.springframework.http.codec.HttpMessageWriter;
|
import org.springframework.http.codec.HttpMessageWriter;
|
||||||
import org.springframework.http.codec.ServerSentEvent;
|
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
|
* @param formData the form data to write to the output message
|
||||||
* @return a {@code FormInserter} that writes form data
|
* @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<String> fromFormData(MultiValueMap<String, String> formData) {
|
public static FormInserter<String> fromFormData(MultiValueMap<String, String> formData) {
|
||||||
|
|
||||||
Assert.notNull(formData, "'formData' must not be null");
|
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
|
* @param value the value to add to the form
|
||||||
* @return a {@code FormInserter} that writes form data
|
* @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<String> fromFormData(String key, String value) {
|
public static FormInserter<String> fromFormData(String key, String value) {
|
||||||
Assert.notNull(key, "'key' must not be null");
|
Assert.notNull(key, "'key' must not be null");
|
||||||
Assert.notNull(value, "'value' 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
|
* @param multipartData the form data to write to the output message
|
||||||
* @return a {@code BodyInserter} that writes multipart data
|
* @return a {@code BodyInserter} that writes multipart data
|
||||||
|
* @see MultipartBodyBuilder
|
||||||
*/
|
*/
|
||||||
// Note that the returned BodyInserter is parameterized to ClientHttpRequest, not
|
public static MultipartInserter fromMultipartData(MultiValueMap<String, Object> multipartData) {
|
||||||
// ReactiveHttpOutputMessage like other methods, since sending form data only typically happens
|
|
||||||
// on the client-side
|
|
||||||
public static <T> FormInserter<T> fromMultipartData(MultiValueMap<String, T> multipartData) {
|
|
||||||
|
|
||||||
Assert.notNull(multipartData, "'multipartData' must not be null");
|
Assert.notNull(multipartData, "'multipartData' must not be null");
|
||||||
|
return new DefaultMultipartInserter().with(multipartData);
|
||||||
return DefaultFormInserter.<T>forMultipartData().with(multipartData);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -271,14 +264,49 @@ public abstract class BodyInserters {
|
||||||
* @return a {@code FormInserter} that can writes the provided multipart
|
* @return a {@code FormInserter} that can writes the provided multipart
|
||||||
* data and also allows adding more parts
|
* data and also allows adding more parts
|
||||||
*/
|
*/
|
||||||
// Note that the returned BodyInserter is parameterized to ClientHttpRequest, not
|
public static MultipartInserter fromMultipartData(String key, Object value) {
|
||||||
// ReactiveHttpOutputMessage like other methods, since sending form data only typically happens
|
|
||||||
// on the client-side
|
|
||||||
public static <T> FormInserter<T> fromMultipartData(String key, T value) {
|
|
||||||
Assert.notNull(key, "'key' must not be null");
|
Assert.notNull(key, "'key' must not be null");
|
||||||
Assert.notNull(value, "'value' must not be null");
|
Assert.notNull(value, "'value' must not be null");
|
||||||
|
|
||||||
return DefaultFormInserter.<T>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 <T, P extends Publisher<T>> MultipartInserter fromMultipartAsyncData(String key,
|
||||||
|
P publisher, Class<T> 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 <T, P extends Publisher<T>> MultipartInserter fromMultipartAsyncData(String key,
|
||||||
|
P publisher, ParameterizedTypeReference<T> 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
|
* Sub-interface of {@link BodyInserter} that allows for additional (multipart) form data to be
|
||||||
* added.
|
* 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<T> extends
|
public interface FormInserter<T> extends
|
||||||
BodyInserter<MultiValueMap<String, T>, ClientHttpRequest> {
|
BodyInserter<MultiValueMap<String, T>, ClientHttpRequest> {
|
||||||
|
|
||||||
|
|
@ -370,45 +400,113 @@ public abstract class BodyInserters {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class DefaultFormInserter<T> implements FormInserter<T> {
|
|
||||||
|
|
||||||
private final MultiValueMap<String, T> data = new LinkedMultiValueMap<>();
|
/**
|
||||||
|
* Extension of {@link FormInserter} that has methods for adding asynchronous part data.
|
||||||
|
*/
|
||||||
|
public interface MultipartInserter extends FormInserter<Object> {
|
||||||
|
|
||||||
private final ResolvableType type;
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
<T, P extends Publisher<T>> MultipartInserter withPublisher(String key, P publisher,
|
||||||
|
Class<T> elementClass);
|
||||||
|
|
||||||
private final MediaType mediaType;
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
<T, P extends Publisher<T>> MultipartInserter withPublisher(String key, P publisher,
|
||||||
|
ParameterizedTypeReference<T> typeReference);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private DefaultFormInserter(ResolvableType type, MediaType mediaType) {
|
private static class DefaultFormInserter implements FormInserter<String> {
|
||||||
this.type = type;
|
|
||||||
this.mediaType = mediaType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static FormInserter<String> forFormData() {
|
private final MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
|
||||||
return new DefaultFormInserter<>(FORM_TYPE, MediaType.APPLICATION_FORM_URLENCODED);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static <T> FormInserter<T> forMultipartData() {
|
public DefaultFormInserter() {
|
||||||
return new DefaultFormInserter<>(MULTIPART_VALUE_TYPE, MediaType.MULTIPART_FORM_DATA);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public FormInserter<T> with(String key, @Nullable T value) {
|
public FormInserter<String> with(String key, @Nullable String value) {
|
||||||
this.data.add(key, value);
|
this.data.add(key, value);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public FormInserter<T> with(MultiValueMap<String, T> values) {
|
public FormInserter<String> with(MultiValueMap<String, String> values) {
|
||||||
this.data.addAll(values);
|
this.data.addAll(values);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<Void> insert(ClientHttpRequest outputMessage, Context context) {
|
public Mono<Void> insert(ClientHttpRequest outputMessage, Context context) {
|
||||||
HttpMessageWriter<MultiValueMap<String, T>> messageWriter =
|
HttpMessageWriter<MultiValueMap<String, String>> messageWriter =
|
||||||
findMessageWriter(context, this.type, this.mediaType);
|
findMessageWriter(context, FORM_TYPE, MediaType.APPLICATION_FORM_URLENCODED);
|
||||||
return messageWriter.write(Mono.just(this.data), this.type, this.mediaType,
|
return messageWriter.write(Mono.just(this.data), FORM_TYPE,
|
||||||
|
MediaType.APPLICATION_FORM_URLENCODED,
|
||||||
|
outputMessage, context.hints());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static class DefaultMultipartInserter implements MultipartInserter {
|
||||||
|
|
||||||
|
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");
|
||||||
|
this.builder.part(key, value);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MultipartInserter with(MultiValueMap<String, Object> values) {
|
||||||
|
Assert.notNull(values, "'values' must not be null");
|
||||||
|
for (Map.Entry<String, List<Object>> entry : values.entrySet()) {
|
||||||
|
this.builder.part(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T, P extends Publisher<T>> MultipartInserter withPublisher(String key,
|
||||||
|
P publisher, Class<T> elementClass) {
|
||||||
|
|
||||||
|
this.builder.asyncPart(key, publisher, elementClass);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T, P extends Publisher<T>> MultipartInserter withPublisher(String key,
|
||||||
|
P publisher, ParameterizedTypeReference<T> typeReference) {
|
||||||
|
|
||||||
|
this.builder.asyncPart(key, publisher, typeReference);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Void> insert(ClientHttpRequest outputMessage, Context context) {
|
||||||
|
HttpMessageWriter<MultiValueMap<String, HttpEntity<?>>> messageWriter =
|
||||||
|
findMessageWriter(context, MULTIPART_VALUE_TYPE, MediaType.MULTIPART_FORM_DATA);
|
||||||
|
MultiValueMap<String, HttpEntity<?>> body = this.builder.build();
|
||||||
|
return messageWriter.write(Mono.just(body), MULTIPART_VALUE_TYPE,
|
||||||
|
MediaType.MULTIPART_FORM_DATA,
|
||||||
outputMessage, context.hints());
|
outputMessage, context.hints());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ import org.springframework.http.codec.ResourceHttpMessageWriter;
|
||||||
import org.springframework.http.codec.ServerSentEvent;
|
import org.springframework.http.codec.ServerSentEvent;
|
||||||
import org.springframework.http.codec.ServerSentEventHttpMessageWriter;
|
import org.springframework.http.codec.ServerSentEventHttpMessageWriter;
|
||||||
import org.springframework.http.codec.json.Jackson2JsonEncoder;
|
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.codec.xml.Jaxb2XmlEncoder;
|
||||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||||
|
|
@ -89,6 +90,7 @@ public class BodyInsertersTests {
|
||||||
messageWriters.add(new ServerSentEventHttpMessageWriter(jsonEncoder));
|
messageWriters.add(new ServerSentEventHttpMessageWriter(jsonEncoder));
|
||||||
messageWriters.add(new FormHttpMessageWriter());
|
messageWriters.add(new FormHttpMessageWriter());
|
||||||
messageWriters.add(new EncoderHttpMessageWriter<>(CharSequenceEncoder.allMimeTypes()));
|
messageWriters.add(new EncoderHttpMessageWriter<>(CharSequenceEncoder.allMimeTypes()));
|
||||||
|
messageWriters.add(new MultipartHttpMessageWriter(messageWriters));
|
||||||
|
|
||||||
this.context = new BodyInserter.Context() {
|
this.context = new BodyInserter.Context() {
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -302,6 +304,22 @@ public class BodyInsertersTests {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void fromMultipartData() throws Exception {
|
||||||
|
MultiValueMap<String, Object> map = new LinkedMultiValueMap<>();
|
||||||
|
map.set("name 3", "value 3");
|
||||||
|
|
||||||
|
BodyInserters.FormInserter<Object> 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<Void> result = inserter.insert(request, this.context);
|
||||||
|
StepVerifier.create(result).expectComplete().verify();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void ofDataBuffers() throws Exception {
|
public void ofDataBuffers() throws Exception {
|
||||||
DefaultDataBufferFactory factory = new DefaultDataBufferFactory();
|
DefaultDataBufferFactory factory = new DefaultDataBufferFactory();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue