Skip to content

Commit dedcb19

Browse files
committed
Document PartEvent API
Closes #29170
1 parent cf2b102 commit dedcb19

File tree

4 files changed

+186
-27
lines changed

4 files changed

+186
-27
lines changed

spring-web/src/main/java/org/springframework/http/codec/multipart/PartEvent.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,9 @@
3535
*
3636
* Each part in a multipart HTTP message produces at least one
3737
* {@code PartEvent} containing both {@link #headers() headers} and a
38-
* {@linkplain PartEvent#content() buffer} with content of the part.
38+
* {@linkplain PartEvent#content() buffer} with the contents of the part.
3939
* <ul>
40-
* <li>Form field will produce a <em>single</em> {@link FormPartEvent},
40+
* <li>Form fields will produce a <em>single</em> {@link FormPartEvent},
4141
* containing the {@linkplain FormPartEvent#value() value} of the field.</li>
4242
* <li>File uploads will produce <em>one or more</em> {@link FilePartEvent}s,
4343
* containing the {@linkplain FilePartEvent#filename() filename} used when
@@ -65,12 +65,12 @@
6565
* // handle form field
6666
* }
6767
* else if (event instanceof FilePartEvent fileEvent) {
68-
* String filename filename = fileEvent.filename();
68+
* String filename = fileEvent.filename();
6969
* Flux&lt;DataBuffer&gt; contents = partEvents.map(PartEvent::content);
7070
* // handle file upload
7171
* }
7272
* else {
73-
* return Mono.error("Unexpected event: " + event);
73+
* return Mono.error(new RuntimeException("Unexpected event: " + event));
7474
* }
7575
* }
7676
* else {
@@ -103,7 +103,7 @@
103103
* .post()
104104
* .uri("https://example.com")
105105
* .body(Flux.concat(
106-
* FormEventPart.create("field", "field value"),
106+
* FormPartEvent.create("field", "field value"),
107107
* FilePartEvent.create("file", resource)
108108
* ), PartEvent.class)
109109
* .retrieve()

src/docs/asciidoc/web/webflux-functional.adoc

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ as the following example shows:
4040
PersonRepository repository = ...
4141
PersonHandler handler = new PersonHandler(repository);
4242
43-
RouterFunction<ServerResponse> route = route()
43+
RouterFunction<ServerResponse> route = route() <1>
4444
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
4545
.GET("/person", accept(APPLICATION_JSON), handler::listPeople)
4646
.POST("/person", handler::createPerson)
@@ -64,6 +64,7 @@ as the following example shows:
6464
}
6565
}
6666
----
67+
<1> Create router using `route()`.
6768

6869
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
6970
.Kotlin
@@ -202,21 +203,62 @@ Mono<MultiValueMap<String, Part>> map = request.multipartData();
202203
val map = request.awaitMultipartData()
203204
----
204205

205-
The following example shows how to access multiparts, one at a time, in streaming fashion:
206+
The following example shows how to access multipart data, one at a time, in streaming fashion:
206207

207-
[source,java,role="primary"]
208+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
208209
.Java
209210
----
210-
Flux<Part> parts = request.body(BodyExtractors.toParts());
211+
Flux<PartEvent> allPartEvents = request.bodyToFlux(PartEvent.class);
212+
allPartsEvents.windowUntil(PartEvent::isLast)
213+
.concatMap(p -> p.switchOnFirst((signal, partEvents) -> {
214+
if (signal.hasValue()) {
215+
PartEvent event = signal.get();
216+
if (event instanceof FormPartEvent formEvent) {
217+
String value = formEvent.value();
218+
// handle form field
219+
}
220+
else if (event instanceof FilePartEvent fileEvent) {
221+
String filename = fileEvent.filename();
222+
Flux<DataBuffer> contents = partEvents.map(PartEvent::content);
223+
// handle file upload
224+
}
225+
else {
226+
return Mono.error(new RuntimeException("Unexpected event: " + event));
227+
}
228+
}
229+
else {
230+
return partEvents; // either complete or error signal
231+
}
232+
}));
211233
----
212-
[source,kotlin,role="secondary"]
234+
235+
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
213236
.Kotlin
214237
----
215-
val parts = request.body(BodyExtractors.toParts()).asFlow()
238+
val parts = request.bodyToFlux<PartEvent>()
239+
allPartsEvents.windowUntil(PartEvent::isLast)
240+
.concatMap {
241+
it.switchOnFirst { signal, partEvents ->
242+
if (signal.hasValue()) {
243+
val event = signal.get()
244+
if (event is FormPartEvent) {
245+
val value: String = event.value();
246+
// handle form field
247+
} else if (event is FilePartEvent) {
248+
val filename: String = event.filename();
249+
val contents: Flux<DataBuffer> = partEvents.map(PartEvent::content);
250+
// handle file upload
251+
} else {
252+
return Mono.error(RuntimeException("Unexpected event: " + event));
253+
}
254+
} else {
255+
return partEvents; // either complete or error signal
256+
}
257+
}
258+
}
259+
}
216260
----
217261

218-
219-
220262
[[webflux-fn-response]]
221263
=== ServerResponse
222264

src/docs/asciidoc/web/webflux-webclient.adoc

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -859,6 +859,54 @@ inline-style, through the built-in `BodyInserters`, as the following example sho
859859
.awaitBody<Unit>()
860860
----
861861

862+
==== `PartEvent`
863+
864+
To stream multipart data sequentially, you can provide multipart content through `PartEvent`
865+
objects.
866+
867+
- Form fields can be created via `FormPartEvent::create`.
868+
- File uploads can be created via `FilePartEvent::create`.
869+
870+
You can concatenate the streams returned from methods via `Flux::concat`, and create a request for
871+
the `WebClient`.
872+
873+
For instance, this sample will POST a multipart form containing a form field and a file.
874+
875+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
876+
.Java
877+
----
878+
Resource resource = ...
879+
Mono<String> result = webClient
880+
.post()
881+
.uri("https://example.com")
882+
.body(Flux.concat(
883+
FormPartEvent.create("field", "field value"),
884+
FilePartEvent.create("file", resource)
885+
), PartEvent.class)
886+
.retrieve()
887+
.bodyToMono(String.class);
888+
----
889+
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
890+
.Kotlin
891+
----
892+
var resource: Resource = ...
893+
var result: Mono<String> = webClient
894+
.post()
895+
.uri("https://example.com")
896+
.body(
897+
Flux.concat(
898+
FormPartEvent.create("field", "field value"),
899+
FilePartEvent.create("file", resource)
900+
)
901+
)
902+
.retrieve()
903+
.bodyToMono()
904+
----
905+
906+
On the server side, `PartEvent` objects that are received via `@RequestBody` or
907+
`ServerRequest::bodyToFlux(PartEvent.class)` can be relayed to another service
908+
via the `WebClient`.
909+
862910

863911

864912
[[webflux-client-filter]]

src/docs/asciidoc/web/webflux.adoc

Lines changed: 83 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -619,11 +619,11 @@ https://github.com/synchronoss/nio-multipart[Synchronoss NIO Multipart] library.
619619
Both are configured through the `ServerCodecConfigurer` bean
620620
(see the <<webflux-web-handler-api, Web Handler API>>).
621621

622-
To parse multipart data in streaming fashion, you can use the `Flux<Part>` returned from an
623-
`HttpMessageReader<Part>` instead. For example, in an annotated controller, use of
624-
`@RequestPart` implies `Map`-like access to individual parts by name and, hence, requires
625-
parsing multipart data in full. By contrast, you can use `@RequestBody` to decode the
626-
content to `Flux<Part>` without collecting to a `MultiValueMap`.
622+
To parse multipart data in streaming fashion, you can use the `Flux<PartEvent>` returned from the
623+
`PartEventHttpMessageReader` instead of using `@RequestPart`, as that implies `Map`-like access
624+
to individual parts by name and, hence, requires parsing multipart data in full.
625+
By contrast, you can use `@RequestBody` to decode the content to `Flux<PartEvent>` without
626+
collecting to a `MultiValueMap`.
627627

628628

629629
[[webflux-forwarded-headers]]
@@ -2825,29 +2825,98 @@ as the following example shows:
28252825
----
28262826
<1> Using `@RequestBody`.
28272827

2828+
===== `PartEvent`
28282829

2829-
To access multipart data sequentially, in streaming fashion, you can use `@RequestBody` with
2830-
`Flux<Part>` (or `Flow<Part>` in Kotlin) instead, as the following example shows:
2830+
To access multipart data sequentially, in a streaming fashion, you can use `@RequestBody` with
2831+
`Flux<PartEvent>` (or `Flow<PartEvent>` in Kotlin).
2832+
Each part in a multipart HTTP message will produce at
2833+
least one `PartEvent` containing both headers and a buffer with the contents of the part.
2834+
2835+
- Form fields will produce a *single* `FormPartEvent`, containing the value of the field.
2836+
- File uploads will produce *one or more* `FilePartEvent` objects, containing the filename used
2837+
when uploading. If the file is large enough to be split across multiple buffers, the first
2838+
`FilePartEvent` will be followed by subsequent events.
2839+
2840+
2841+
For example:
28312842

28322843
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
28332844
.Java
28342845
----
2835-
@PostMapping("/")
2836-
public String handle(@RequestBody Flux<Part> parts) { <1>
2837-
// ...
2838-
}
2846+
@PostMapping("/")
2847+
public void handle(@RequestBody Flux<PartEvent> allPartsEvents) { <1>
2848+
allPartsEvents.windowUntil(PartEvent::isLast) <2>
2849+
.concatMap(p -> p.switchOnFirst((signal, partEvents) -> { <3>
2850+
if (signal.hasValue()) {
2851+
PartEvent event = signal.get();
2852+
if (event instanceof FormPartEvent formEvent) { <4>
2853+
String value = formEvent.value();
2854+
// handle form field
2855+
}
2856+
else if (event instanceof FilePartEvent fileEvent) { <5>
2857+
String filename = fileEvent.filename();
2858+
Flux<DataBuffer> contents = partEvents.map(PartEvent::content);
2859+
// handle file upload
2860+
}
2861+
else {
2862+
return Mono.error(new RuntimeException("Unexpected event: " + event));
2863+
}
2864+
}
2865+
else {
2866+
return partEvents; // either complete or error signal
2867+
}
2868+
}));
2869+
}
28392870
----
28402871
<1> Using `@RequestBody`.
2872+
<2> The final `PartEvent` for a particular part will have `isLast()` set to `true`, and can be
2873+
followed by additional events belonging to subsequent parts.
2874+
This makes the `isLast` property suitable as a predicate for the `Flux::windowUntil` operator, to
2875+
split events from all parts into windows that each belong to a single part.
2876+
<3> The `Flux::switchOnFirst` operator allows you to see whether you are handling a form field or
2877+
file upload.
2878+
<4> Handling the form field.
2879+
<5> Handling the file upload.
28412880

28422881
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
28432882
.Kotlin
28442883
----
28452884
@PostMapping("/")
2846-
fun handle(@RequestBody parts: Flow<Part>): String { // <1>
2847-
// ...
2848-
}
2885+
fun handle(@RequestBody allPartsEvents: Flux<PartEvent>) = { // <1>
2886+
allPartsEvents.windowUntil(PartEvent::isLast) <2>
2887+
.concatMap {
2888+
it.switchOnFirst { signal, partEvents -> <3>
2889+
if (signal.hasValue()) {
2890+
val event = signal.get()
2891+
if (event is FormPartEvent) { <4>
2892+
val value: String = event.value();
2893+
// handle form field
2894+
} else if (event is FilePartEvent) { <5>
2895+
val filename: String = event.filename();
2896+
val contents: Flux<DataBuffer> = partEvents.map(PartEvent::content);
2897+
// handle file upload
2898+
} else {
2899+
return Mono.error(RuntimeException("Unexpected event: " + event));
2900+
}
2901+
} else {
2902+
return partEvents; // either complete or error signal
2903+
}
2904+
}
2905+
}
2906+
}
28492907
----
28502908
<1> Using `@RequestBody`.
2909+
<2> The final `PartEvent` for a particular part will have `isLast()` set to `true`, and can be
2910+
followed by additional events belonging to subsequent parts.
2911+
This makes the `isLast` property suitable as a predicate for the `Flux::windowUntil` operator, to
2912+
split events from all parts into windows that each belong to a single part.
2913+
<3> The `Flux::switchOnFirst` operator allows you to see whether you are handling a form field or
2914+
file upload.
2915+
<4> Handling the form field.
2916+
<5> Handling the file upload.
2917+
2918+
Received part events can also be relayed to another service by using the `WebClient`.
2919+
See <<webflux-client-body-multipart>>.
28512920

28522921

28532922
[[webflux-ann-requestbody]]

0 commit comments

Comments
 (0)