Skip to content

Commit 7035ee7

Browse files
committed
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
1 parent f23612c commit 7035ee7

File tree

2 files changed

+154
-38
lines changed

2 files changed

+154
-38
lines changed

spring-webflux/src/main/java/org/springframework/web/reactive/function/BodyInserters.java

+136-38
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.web.reactive.function;
1818

1919
import java.util.List;
20+
import java.util.Map;
2021
import java.util.Optional;
2122
import java.util.stream.Collectors;
2223

@@ -27,8 +28,10 @@
2728
import org.springframework.core.ResolvableType;
2829
import org.springframework.core.io.Resource;
2930
import org.springframework.core.io.buffer.DataBuffer;
31+
import org.springframework.http.HttpEntity;
3032
import org.springframework.http.MediaType;
3133
import org.springframework.http.ReactiveHttpOutputMessage;
34+
import org.springframework.http.client.MultipartBodyBuilder;
3235
import org.springframework.http.client.reactive.ClientHttpRequest;
3336
import org.springframework.http.codec.HttpMessageWriter;
3437
import org.springframework.http.codec.ServerSentEvent;
@@ -204,14 +207,11 @@ public static <T, S extends Publisher<ServerSentEvent<T>>> BodyInserter<S, Serve
204207
* @param formData the form data to write to the output message
205208
* @return a {@code FormInserter} that writes form data
206209
*/
207-
// Note that the returned FormInserter is parameterized to ClientHttpRequest, not
208-
// ReactiveHttpOutputMessage like other methods, since sending form data only typically happens
209-
// on the client-side
210210
public static FormInserter<String> fromFormData(MultiValueMap<String, String> formData) {
211211

212212
Assert.notNull(formData, "'formData' must not be null");
213213

214-
return DefaultFormInserter.forFormData().with(formData);
214+
return new DefaultFormInserter().with(formData);
215215
}
216216

217217
/**
@@ -222,14 +222,11 @@ public static FormInserter<String> fromFormData(MultiValueMap<String, String> fo
222222
* @param value the value to add to the form
223223
* @return a {@code FormInserter} that writes form data
224224
*/
225-
// Note that the returned FormInserter is parameterized to ClientHttpRequest, not
226-
// ReactiveHttpOutputMessage like other methods, since sending form data only typically happens
227-
// on the client-side
228225
public static FormInserter<String> fromFormData(String key, String value) {
229226
Assert.notNull(key, "'key' must not be null");
230227
Assert.notNull(value, "'value' must not be null");
231228

232-
return DefaultFormInserter.forFormData().with(key, value);
229+
return new DefaultFormInserter().with(key, value);
233230
}
234231

235232
/**
@@ -251,15 +248,11 @@ public static FormInserter<String> fromFormData(String key, String value) {
251248
*
252249
* @param multipartData the form data to write to the output message
253250
* @return a {@code BodyInserter} that writes multipart data
251+
* @see MultipartBodyBuilder
254252
*/
255-
// Note that the returned BodyInserter is parameterized to ClientHttpRequest, not
256-
// ReactiveHttpOutputMessage like other methods, since sending form data only typically happens
257-
// on the client-side
258-
public static <T> FormInserter<T> fromMultipartData(MultiValueMap<String, T> multipartData) {
259-
253+
public static MultipartInserter fromMultipartData(MultiValueMap<String, Object> multipartData) {
260254
Assert.notNull(multipartData, "'multipartData' must not be null");
261-
262-
return DefaultFormInserter.<T>forMultipartData().with(multipartData);
255+
return new DefaultMultipartInserter().with(multipartData);
263256
}
264257

265258
/**
@@ -271,14 +264,49 @@ public static <T> FormInserter<T> fromMultipartData(MultiValueMap<String, T> mul
271264
* @return a {@code FormInserter} that can writes the provided multipart
272265
* data and also allows adding more parts
273266
*/
274-
// Note that the returned BodyInserter is parameterized to ClientHttpRequest, not
275-
// ReactiveHttpOutputMessage like other methods, since sending form data only typically happens
276-
// on the client-side
277-
public static <T> FormInserter<T> fromMultipartData(String key, T value) {
267+
public static MultipartInserter fromMultipartData(String key, Object value) {
278268
Assert.notNull(key, "'key' must not be null");
279269
Assert.notNull(value, "'value' must not be null");
280270

281-
return DefaultFormInserter.<T>forMultipartData().with(key, value);
271+
return new DefaultMultipartInserter().with(key, value);
272+
}
273+
274+
/**
275+
* A variant of {@link #fromMultipartData(MultiValueMap)} for adding asynchronous data as a
276+
* part in-line vs building a {@code MultiValueMap} and passing it in.
277+
* @param key the part name
278+
* @param publisher the publisher that forms the part value
279+
* @param elementClass the class contained in the {@code publisher}
280+
* @return a {@code FormInserter} that can writes the provided multipart
281+
* data and also allows adding more parts
282+
*/
283+
public static <T, P extends Publisher<T>> MultipartInserter fromMultipartAsyncData(String key,
284+
P publisher, Class<T> elementClass) {
285+
286+
Assert.notNull(key, "'key' must not be null");
287+
Assert.notNull(publisher, "'publisher' must not be null");
288+
Assert.notNull(elementClass, "'elementClass' must not be null");
289+
290+
return new DefaultMultipartInserter().withPublisher(key, publisher, elementClass);
291+
}
292+
293+
/**
294+
* A variant of {@link #fromMultipartData(MultiValueMap)} for adding asynchronous data as a
295+
* part in-line vs building a {@code MultiValueMap} and passing it in.
296+
* @param key the part name
297+
* @param publisher the publisher that forms the part value
298+
* @param typeReference the type contained in the {@code publisher}
299+
* @return a {@code FormInserter} that can writes the provided multipart
300+
* data and also allows adding more parts
301+
*/
302+
public static <T, P extends Publisher<T>> MultipartInserter fromMultipartAsyncData(String key,
303+
P publisher, ParameterizedTypeReference<T> typeReference) {
304+
305+
Assert.notNull(key, "'key' must not be null");
306+
Assert.notNull(publisher, "'publisher' must not be null");
307+
Assert.notNull(typeReference, "'typeReference' must not be null");
308+
309+
return new DefaultMultipartInserter().withPublisher(key, publisher, typeReference);
282310
}
283311

284312
/**
@@ -350,6 +378,8 @@ private static <T> HttpMessageWriter<T> cast(HttpMessageWriter<?> messageWriter)
350378
* Sub-interface of {@link BodyInserter} that allows for additional (multipart) form data to be
351379
* added.
352380
*/
381+
// Note that FormInserter is parameterized to ClientHttpRequest, not ReactiveHttpOutputMessage
382+
// like other return values methods, since sending form data only typically happens on the client-side
353383
public interface FormInserter<T> extends
354384
BodyInserter<MultiValueMap<String, T>, ClientHttpRequest> {
355385

@@ -370,45 +400,113 @@ public interface FormInserter<T> extends
370400

371401
}
372402

373-
private static class DefaultFormInserter<T> implements FormInserter<T> {
374403

375-
private final MultiValueMap<String, T> data = new LinkedMultiValueMap<>();
404+
/**
405+
* Extension of {@link FormInserter} that has methods for adding asynchronous part data.
406+
*/
407+
public interface MultipartInserter extends FormInserter<Object> {
408+
409+
/**
410+
* Adds the specified publisher as a part.
411+
*
412+
* @param key the key to be added
413+
* @param publisher the publisher to be added as value
414+
* @param elementClass the class of elements contained in {@code publisher}
415+
* @return this inserter
416+
*/
417+
<T, P extends Publisher<T>> MultipartInserter withPublisher(String key, P publisher,
418+
Class<T> elementClass);
419+
420+
/**
421+
* Adds the specified publisher as a part.
422+
*
423+
* @param key the key to be added
424+
* @param publisher the publisher to be added as value
425+
* @param typeReference the type of elements contained in {@code publisher}
426+
* @return this inserter
427+
*/
428+
<T, P extends Publisher<T>> MultipartInserter withPublisher(String key, P publisher,
429+
ParameterizedTypeReference<T> typeReference);
430+
431+
}
432+
433+
434+
private static class DefaultFormInserter implements FormInserter<String> {
435+
436+
private final MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
437+
438+
public DefaultFormInserter() {
439+
}
440+
441+
@Override
442+
public FormInserter<String> with(String key, @Nullable String value) {
443+
this.data.add(key, value);
444+
return this;
445+
}
446+
447+
@Override
448+
public FormInserter<String> with(MultiValueMap<String, String> values) {
449+
this.data.addAll(values);
450+
return this;
451+
}
452+
453+
@Override
454+
public Mono<Void> insert(ClientHttpRequest outputMessage, Context context) {
455+
HttpMessageWriter<MultiValueMap<String, String>> messageWriter =
456+
findMessageWriter(context, FORM_TYPE, MediaType.APPLICATION_FORM_URLENCODED);
457+
return messageWriter.write(Mono.just(this.data), FORM_TYPE,
458+
MediaType.APPLICATION_FORM_URLENCODED,
459+
outputMessage, context.hints());
460+
}
461+
}
376462

377-
private final ResolvableType type;
378463

379-
private final MediaType mediaType;
464+
private static class DefaultMultipartInserter implements MultipartInserter {
380465

466+
private final MultipartBodyBuilder builder = new MultipartBodyBuilder();
381467

382-
private DefaultFormInserter(ResolvableType type, MediaType mediaType) {
383-
this.type = type;
384-
this.mediaType = mediaType;
468+
public DefaultMultipartInserter() {
385469
}
386470

387-
public static FormInserter<String> forFormData() {
388-
return new DefaultFormInserter<>(FORM_TYPE, MediaType.APPLICATION_FORM_URLENCODED);
471+
@Override
472+
public MultipartInserter with(String key, @Nullable Object value) {
473+
Assert.notNull(value, "'value' must not be null");
474+
this.builder.part(key, value);
475+
return this;
389476
}
390477

391-
public static <T> FormInserter<T> forMultipartData() {
392-
return new DefaultFormInserter<>(MULTIPART_VALUE_TYPE, MediaType.MULTIPART_FORM_DATA);
478+
@Override
479+
public MultipartInserter with(MultiValueMap<String, Object> values) {
480+
Assert.notNull(values, "'values' must not be null");
481+
for (Map.Entry<String, List<Object>> entry : values.entrySet()) {
482+
this.builder.part(entry.getKey(), entry.getValue());
483+
}
484+
return this;
393485
}
394486

395487
@Override
396-
public FormInserter<T> with(String key, @Nullable T value) {
397-
this.data.add(key, value);
488+
public <T, P extends Publisher<T>> MultipartInserter withPublisher(String key,
489+
P publisher, Class<T> elementClass) {
490+
491+
this.builder.asyncPart(key, publisher, elementClass);
398492
return this;
399493
}
400494

401495
@Override
402-
public FormInserter<T> with(MultiValueMap<String, T> values) {
403-
this.data.addAll(values);
496+
public <T, P extends Publisher<T>> MultipartInserter withPublisher(String key,
497+
P publisher, ParameterizedTypeReference<T> typeReference) {
498+
499+
this.builder.asyncPart(key, publisher, typeReference);
404500
return this;
405501
}
406502

407503
@Override
408504
public Mono<Void> insert(ClientHttpRequest outputMessage, Context context) {
409-
HttpMessageWriter<MultiValueMap<String, T>> messageWriter =
410-
findMessageWriter(context, this.type, this.mediaType);
411-
return messageWriter.write(Mono.just(this.data), this.type, this.mediaType,
505+
HttpMessageWriter<MultiValueMap<String, HttpEntity<?>>> messageWriter =
506+
findMessageWriter(context, MULTIPART_VALUE_TYPE, MediaType.MULTIPART_FORM_DATA);
507+
MultiValueMap<String, HttpEntity<?>> body = this.builder.build();
508+
return messageWriter.write(Mono.just(body), MULTIPART_VALUE_TYPE,
509+
MediaType.MULTIPART_FORM_DATA,
412510
outputMessage, context.hints());
413511
}
414512
}

spring-webflux/src/test/java/org/springframework/web/reactive/function/BodyInsertersTests.java

+18
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
import org.springframework.http.codec.ServerSentEvent;
5454
import org.springframework.http.codec.ServerSentEventHttpMessageWriter;
5555
import org.springframework.http.codec.json.Jackson2JsonEncoder;
56+
import org.springframework.http.codec.multipart.MultipartHttpMessageWriter;
5657
import org.springframework.http.codec.xml.Jaxb2XmlEncoder;
5758
import org.springframework.http.server.reactive.ServerHttpRequest;
5859
import org.springframework.http.server.reactive.ServerHttpResponse;
@@ -89,6 +90,7 @@ public void createContext() {
8990
messageWriters.add(new ServerSentEventHttpMessageWriter(jsonEncoder));
9091
messageWriters.add(new FormHttpMessageWriter());
9192
messageWriters.add(new EncoderHttpMessageWriter<>(CharSequenceEncoder.allMimeTypes()));
93+
messageWriters.add(new MultipartHttpMessageWriter(messageWriters));
9294

9395
this.context = new BodyInserter.Context() {
9496
@Override
@@ -302,6 +304,22 @@ public void fromFormDataWith() throws Exception {
302304

303305
}
304306

307+
@Test
308+
public void fromMultipartData() throws Exception {
309+
MultiValueMap<String, Object> map = new LinkedMultiValueMap<>();
310+
map.set("name 3", "value 3");
311+
312+
BodyInserters.FormInserter<Object> inserter =
313+
BodyInserters.fromMultipartData("name 1", "value 1")
314+
.withPublisher("name 2", Flux.just("foo", "bar", "baz"), String.class)
315+
.with(map);
316+
317+
MockClientHttpRequest request = new MockClientHttpRequest(HttpMethod.GET, URI.create("http://example.com"));
318+
Mono<Void> result = inserter.insert(request, this.context);
319+
StepVerifier.create(result).expectComplete().verify();
320+
321+
}
322+
305323
@Test
306324
public void ofDataBuffers() throws Exception {
307325
DefaultDataBufferFactory factory = new DefaultDataBufferFactory();

0 commit comments

Comments
 (0)