Skip to content

Commit 92981ac

Browse files
committed
Add Flux<Part> ServerRequest.parts()
This commit introduces Flux<Part> ServerRequest.parts() that delegates to ServerWebExchange.getParts() and offers an alternative, streaming way of accessing multipart data. Closes gh-23131
1 parent 11c7907 commit 92981ac

File tree

8 files changed

+133
-25
lines changed

8 files changed

+133
-25
lines changed

spring-test/src/main/java/org/springframework/mock/web/reactive/function/server/MockServerRequest.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2019 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -249,6 +249,13 @@ public Mono<MultiValueMap<String, Part>> multipartData() {
249249
return (Mono<MultiValueMap<String, Part>>) this.body;
250250
}
251251

252+
@Override
253+
@SuppressWarnings("unchecked")
254+
public Flux<Part> parts() {
255+
Assert.state(this.body != null, "No body");
256+
return (Flux<Part>) this.body;
257+
}
258+
252259
@Override
253260
public ServerWebExchange exchange() {
254261
Assert.state(this.exchange != null, "No exchange");

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,11 @@ public Mono<MultiValueMap<String, Part>> multipartData() {
220220
return this.exchange.getMultipartData();
221221
}
222222

223+
@Override
224+
public Flux<Part> parts() {
225+
return this.exchange.getParts();
226+
}
227+
223228
private ServerHttpRequest request() {
224229
return this.exchange.getRequest();
225230
}

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

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2019 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -290,8 +290,7 @@ private static class DelegatingServerWebExchange implements ServerWebExchange {
290290
private static final ResolvableType FORM_DATA_TYPE =
291291
ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class);
292292

293-
private static final ResolvableType MULTIPART_DATA_TYPE = ResolvableType.forClassWithGenerics(
294-
MultiValueMap.class, String.class, Part.class);
293+
private static final ResolvableType PARTS_DATA_TYPE = ResolvableType.forClass(Part.class);
295294

296295
private static final Mono<MultiValueMap<String, String>> EMPTY_FORM_DATA =
297296
Mono.just(CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<String, String>(0))).cache();
@@ -307,13 +306,16 @@ private static class DelegatingServerWebExchange implements ServerWebExchange {
307306

308307
private final Mono<MultiValueMap<String, Part>> multipartDataMono;
309308

309+
private final Flux<Part> parts;
310+
310311
public DelegatingServerWebExchange(
311312
ServerHttpRequest request, ServerWebExchange delegate, List<HttpMessageReader<?>> messageReaders) {
312313

313314
this.request = request;
314315
this.delegate = delegate;
315316
this.formDataMono = initFormData(request, messageReaders);
316-
this.multipartDataMono = initMultipartData(request, messageReaders);
317+
this.parts = initParts(request, messageReaders);
318+
this.multipartDataMono = initMultipartData(this.parts);
317319
}
318320

319321
@SuppressWarnings("unchecked")
@@ -339,26 +341,32 @@ private static Mono<MultiValueMap<String, String>> initFormData(ServerHttpReques
339341
}
340342

341343
@SuppressWarnings("unchecked")
342-
private static Mono<MultiValueMap<String, Part>> initMultipartData(ServerHttpRequest request,
343-
List<HttpMessageReader<?>> readers) {
344+
private static Flux<Part> initParts(ServerHttpRequest request, List<HttpMessageReader<?>> readers) {
344345

345346
try {
346347
MediaType contentType = request.getHeaders().getContentType();
347348
if (MediaType.MULTIPART_FORM_DATA.isCompatibleWith(contentType)) {
348-
return ((HttpMessageReader<MultiValueMap<String, Part>>) readers.stream()
349-
.filter(reader -> reader.canRead(MULTIPART_DATA_TYPE, MediaType.MULTIPART_FORM_DATA))
349+
return ((HttpMessageReader<Part>)readers.stream()
350+
.filter(reader -> reader.canRead(PARTS_DATA_TYPE, MediaType.MULTIPART_FORM_DATA))
350351
.findFirst()
351352
.orElseThrow(() -> new IllegalStateException("No multipart HttpMessageReader.")))
352-
.readMono(MULTIPART_DATA_TYPE, request, Hints.none())
353-
.switchIfEmpty(EMPTY_MULTIPART_DATA)
354-
.cache();
353+
.read(PARTS_DATA_TYPE, request, Hints.none());
355354
}
356355
}
357356
catch (InvalidMediaTypeException ex) {
358357
// Ignore
359358
}
360-
return EMPTY_MULTIPART_DATA;
359+
return Flux.empty();
360+
}
361+
362+
private static Mono<MultiValueMap<String, Part>> initMultipartData(Flux<Part> parts) {
363+
return parts.collect(
364+
() -> (MultiValueMap<String, Part>) new LinkedMultiValueMap<String, Part>(),
365+
(map, part) -> map.add(part.name(), part))
366+
.switchIfEmpty(EMPTY_MULTIPART_DATA)
367+
.cache();
361368
}
369+
362370
@Override
363371
public ServerHttpRequest getRequest() {
364372
return this.request;
@@ -374,6 +382,11 @@ public Mono<MultiValueMap<String, Part>> getMultipartData() {
374382
return this.multipartDataMono;
375383
}
376384

385+
@Override
386+
public Flux<Part> getParts() {
387+
return this.parts;
388+
}
389+
377390
// Delegating methods
378391

379392
@Override

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1025,6 +1025,11 @@ public Mono<MultiValueMap<String, Part>> multipartData() {
10251025
return this.request.multipartData();
10261026
}
10271027

1028+
@Override
1029+
public Flux<Part> parts() {
1030+
return this.request.parts();
1031+
}
1032+
10281033
@Override
10291034
public ServerWebExchange exchange() {
10301035
return this.request.exchange();

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2019 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -270,9 +270,23 @@ default String pathVariable(String name) {
270270
* <p><strong>Note:</strong> calling this method causes the request body to
271271
* be read and parsed in full, and the resulting {@code MultiValueMap} is
272272
* cached so that this method is safe to call more than once.
273+
* <p><strong>Note:</strong>the {@linkplain Part#content() contents} of each
274+
* part is not cached, and can only be read once.
273275
*/
274276
Mono<MultiValueMap<String, Part>> multipartData();
275277

278+
/**
279+
* Get the parts of a multipart request if the Content-Type is
280+
* {@code "multipart/form-data"} or an empty flux otherwise.
281+
* <p><strong>Note:</strong> calling this method causes the request body to
282+
* be read and parsed in full and the resulting {@code Flux} is
283+
* cached so that this method is safe to call more than once.
284+
* <p><strong>Note:</strong>the {@linkplain Part#content() contents} of each
285+
* part is not cached, and can only be read once.
286+
* @since 5.2
287+
*/
288+
Flux<Part> parts();
289+
276290
/**
277291
* Get the web exchange that this request is based on.
278292
* <p>Note: Manipulating the exchange directly (instead of using the methods provided on

spring-webflux/src/main/java/org/springframework/web/reactive/function/server/support/ServerRequestWrapper.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2019 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -208,6 +208,11 @@ public Mono<MultiValueMap<String, Part>> multipartData() {
208208
return this.delegate.multipartData();
209209
}
210210

211+
@Override
212+
public Flux<Part> parts() {
213+
return this.delegate.parts();
214+
}
215+
211216
@Override
212217
public ServerWebExchange exchange() {
213218
return this.delegate.exchange();

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

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616

1717
package org.springframework.web.reactive.function;
1818

19+
import java.io.IOException;
20+
import java.nio.file.Files;
21+
import java.nio.file.Path;
22+
import java.nio.file.Paths;
1923
import java.util.Map;
2024

2125
import org.junit.Test;
@@ -29,6 +33,7 @@
2933
import org.springframework.http.codec.multipart.FilePart;
3034
import org.springframework.http.codec.multipart.FormFieldPart;
3135
import org.springframework.http.codec.multipart.Part;
36+
import org.springframework.util.FileCopyUtils;
3237
import org.springframework.util.MultiValueMap;
3338
import org.springframework.web.reactive.function.client.ClientResponse;
3439
import org.springframework.web.reactive.function.client.WebClient;
@@ -38,7 +43,7 @@
3843
import org.springframework.web.reactive.function.server.ServerResponse;
3944

4045
import static org.assertj.core.api.Assertions.assertThat;
41-
import static org.springframework.web.reactive.function.server.RequestPredicates.POST;
46+
import static org.assertj.core.api.Assertions.fail;
4247
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
4348

4449
/**
@@ -48,6 +53,8 @@ public class MultipartIntegrationTests extends AbstractRouterFunctionIntegration
4853

4954
private final WebClient webClient = WebClient.create();
5055

56+
private ClassPathResource resource = new ClassPathResource("org/springframework/http/codec/multipart/foo.txt");
57+
5158

5259
@Test
5360
public void multipartData() {
@@ -77,54 +84,99 @@ public void parts() {
7784
.verifyComplete();
7885
}
7986

87+
@Test
88+
public void transferTo() {
89+
Mono<String> result = webClient
90+
.post()
91+
.uri("http://localhost:" + this.port + "/transferTo")
92+
.syncBody(generateBody())
93+
.retrieve()
94+
.bodyToMono(String.class);
95+
96+
StepVerifier
97+
.create(result)
98+
.consumeNextWith(location -> {
99+
try {
100+
byte[] actualBytes = Files.readAllBytes(Paths.get(location));
101+
byte[] expectedBytes = FileCopyUtils.copyToByteArray(this.resource.getInputStream());
102+
assertThat(actualBytes).isEqualTo(expectedBytes);
103+
}
104+
catch (IOException ex) {
105+
fail("IOException", ex);
106+
}
107+
})
108+
.verifyComplete();
109+
}
110+
80111
private MultiValueMap<String, HttpEntity<?>> generateBody() {
81112
MultipartBodyBuilder builder = new MultipartBodyBuilder();
82-
builder.part("fooPart", new ClassPathResource("org/springframework/http/codec/multipart/foo.txt"));
113+
builder.part("fooPart", resource);
83114
builder.part("barPart", "bar");
84115
return builder.build();
85116
}
86117

87118
@Override
88119
protected RouterFunction<ServerResponse> routerFunction() {
89120
MultipartHandler multipartHandler = new MultipartHandler();
90-
return route(POST("/multipartData"), multipartHandler::multipartData)
91-
.andRoute(POST("/parts"), multipartHandler::parts);
121+
return route()
122+
.POST("/multipartData", multipartHandler::multipartData)
123+
.POST("/parts", multipartHandler::parts)
124+
.POST("/transferTo", multipartHandler::transferTo)
125+
.build();
92126
}
93127

94128

95129
private static class MultipartHandler {
96130

97131
public Mono<ServerResponse> multipartData(ServerRequest request) {
98-
return request
99-
.body(BodyExtractors.toMultipartData())
132+
return request.multipartData()
100133
.flatMap(map -> {
101134
Map<String, Part> parts = map.toSingleValueMap();
102135
try {
103136
assertThat(parts.size()).isEqualTo(2);
104137
assertThat(((FilePart) parts.get("fooPart")).filename()).isEqualTo("foo.txt");
105138
assertThat(((FormFieldPart) parts.get("barPart")).value()).isEqualTo("bar");
139+
return ServerResponse.ok().build();
106140
}
107141
catch(Exception e) {
108142
return Mono.error(e);
109143
}
110-
return ServerResponse.ok().build();
111144
});
112145
}
113146

114147
public Mono<ServerResponse> parts(ServerRequest request) {
115-
return request.body(BodyExtractors.toParts()).collectList()
148+
return request.parts().collectList()
116149
.flatMap(parts -> {
117150
try {
118151
assertThat(parts.size()).isEqualTo(2);
119152
assertThat(((FilePart) parts.get(0)).filename()).isEqualTo("foo.txt");
120153
assertThat(((FormFieldPart) parts.get(1)).value()).isEqualTo("bar");
154+
return ServerResponse.ok().build();
121155
}
122156
catch(Exception e) {
123157
return Mono.error(e);
124158
}
125-
return ServerResponse.ok().build();
126159
});
127160
}
161+
162+
public Mono<ServerResponse> transferTo(ServerRequest request) {
163+
return request.parts()
164+
.filter(part -> part instanceof FilePart)
165+
.next()
166+
.cast(FilePart.class)
167+
.flatMap(part -> {
168+
try {
169+
Path tempFile = Files.createTempFile("MultipartIntegrationTests", null);
170+
return part.transferTo(tempFile)
171+
.then(ServerResponse.ok()
172+
.syncBody(tempFile.toString()));
173+
}
174+
catch (Exception e) {
175+
return Mono.error(e);
176+
}
177+
});
178+
}
179+
128180
}
129181

130182
}

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2019 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -247,6 +247,13 @@ public Mono<MultiValueMap<String, Part>> multipartData() {
247247
return (Mono<MultiValueMap<String, Part>>) this.body;
248248
}
249249

250+
@Override
251+
@SuppressWarnings("unchecked")
252+
public Flux<Part> parts() {
253+
Assert.state(this.body != null, "No body");
254+
return (Flux<Part>) this.body;
255+
}
256+
250257
@Override
251258
public ServerWebExchange exchange() {
252259
Assert.state(this.exchange != null, "No exchange");

0 commit comments

Comments
 (0)