Skip to content

Commit fc7bede

Browse files
committed
Support data binding for multipart requests in WebFlux
Issue: SPR-14546
1 parent b5089ac commit fc7bede

File tree

12 files changed

+377
-154
lines changed

12 files changed

+377
-154
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright 2002-2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.http.codec.multipart;
18+
19+
import java.io.File;
20+
21+
import reactor.core.publisher.Mono;
22+
23+
/**
24+
* Specialization of {@link Part} for a file upload.
25+
*
26+
* @author Rossen Stoyanchev
27+
* @since 5.0
28+
*/
29+
public interface FilePart extends Part {
30+
31+
/**
32+
* Return the name of the file selected by the user in a browser form.
33+
*/
34+
String getFilename();
35+
36+
37+
/**
38+
* Transfer the file in this part to the given file destination.
39+
* @param dest the target file
40+
* @return completion {@code Mono} with the result of the file transfer,
41+
* possibly {@link IllegalStateException} if the part isn't a file
42+
*/
43+
Mono<Void> transferTo(File dest);
44+
45+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2002-2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.http.codec.multipart;
18+
19+
/**
20+
* Specialization of {@link Part} for a form field.
21+
*
22+
* @author Rossen Stoyanchev
23+
* @since 5.0
24+
*/
25+
public interface FormFieldPart extends Part {
26+
27+
/**
28+
* Return the form field value.
29+
*/
30+
String getValue();
31+
32+
}

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

Lines changed: 4 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,7 @@
1616

1717
package org.springframework.http.codec.multipart;
1818

19-
import java.io.File;
20-
import java.util.Optional;
21-
2219
import reactor.core.publisher.Flux;
23-
import reactor.core.publisher.Mono;
2420

2521
import org.springframework.core.io.buffer.DataBuffer;
2622
import org.springframework.http.HttpHeaders;
@@ -29,9 +25,10 @@
2925
* Representation for a part in a "multipart/form-data" request.
3026
*
3127
* <p>The origin of a multipart request may a browser form in which case each
32-
* part represents a text-based form field or a file upload. Multipart requests
33-
* may also be used outside of browsers to transfer data with any content type
34-
* such as JSON, PDF, etc.
28+
* part is either a {@link FormFieldPart} or a {@link FilePart}.
29+
*
30+
* <p>Multipart requests may also be used outside of a browser for data of any
31+
* content type (e.g. JSON, PDF, etc).
3532
*
3633
* @author Sebastien Deleuze
3734
* @author Rossen Stoyanchev
@@ -53,30 +50,9 @@ public interface Part {
5350
*/
5451
HttpHeaders getHeaders();
5552

56-
/**
57-
*
58-
* Return the name of the file selected by the user in a browser form.
59-
* @return the filename if defined and available
60-
*/
61-
Optional<String> getFilename();
62-
63-
/**
64-
* Return the part content converted to a String with the charset from the
65-
* {@code Content-Type} header or {@code UTF-8} by default.
66-
*/
67-
Mono<String> getContentAsString();
68-
6953
/**
7054
* Return the part raw content as a stream of DataBuffer's.
7155
*/
7256
Flux<DataBuffer> getContent();
7357

74-
/**
75-
* Transfer the file in this part to the given file destination.
76-
* @param destination the target file
77-
* @return completion {@code Mono} with the result of the file transfer,
78-
* possibly {@link IllegalStateException} if the part isn't a file
79-
*/
80-
Mono<Void> transferTo(File destination);
81-
8258
}

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

Lines changed: 84 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,6 @@
5252
import org.springframework.http.ReactiveHttpInputMessage;
5353
import org.springframework.http.codec.HttpMessageReader;
5454
import org.springframework.util.Assert;
55-
import org.springframework.util.MimeType;
56-
import org.springframework.util.StreamUtils;
5755

5856
/**
5957
* {@code HttpMessageReader} for parsing {@code "multipart/form-data"} requests
@@ -71,6 +69,8 @@
7169
*/
7270
public class SynchronossPartHttpMessageReader implements HttpMessageReader<Part> {
7371

72+
private final DataBufferFactory bufferFactory = new DefaultDataBufferFactory();
73+
7474

7575
@Override
7676
public List<MediaType> getReadableMediaTypes() {
@@ -88,7 +88,7 @@ public boolean canRead(ResolvableType elementType, MediaType mediaType) {
8888
public Flux<Part> read(ResolvableType elementType, ReactiveHttpInputMessage message,
8989
Map<String, Object> hints) {
9090

91-
return Flux.create(new SynchronossPartGenerator(message));
91+
return Flux.create(new SynchronossPartGenerator(message, this.bufferFactory));
9292
}
9393

9494

@@ -109,17 +109,20 @@ private static class SynchronossPartGenerator implements Consumer<FluxSink<Part>
109109

110110
private final ReactiveHttpInputMessage inputMessage;
111111

112+
private final DataBufferFactory bufferFactory;
113+
112114

113-
SynchronossPartGenerator(ReactiveHttpInputMessage inputMessage) {
115+
SynchronossPartGenerator(ReactiveHttpInputMessage inputMessage, DataBufferFactory factory) {
114116
this.inputMessage = inputMessage;
117+
this.bufferFactory = factory;
115118
}
116119

117120

118121
@Override
119122
public void accept(FluxSink<Part> emitter) {
120123

121124
MultipartContext context = createMultipartContext();
122-
NioMultipartParserListener listener = new FluxSinkAdapterListener(emitter);
125+
NioMultipartParserListener listener = new FluxSinkAdapterListener(emitter, this.bufferFactory);
123126
NioMultipartParser parser = Multipart.multipart(context).forNIO(listener);
124127

125128
this.inputMessage.getBody().subscribe(buffer -> {
@@ -167,26 +170,32 @@ private static class FluxSinkAdapterListener implements NioMultipartParserListen
167170

168171
private final FluxSink<Part> sink;
169172

173+
private final DataBufferFactory bufferFactory;
174+
170175
private final AtomicInteger terminated = new AtomicInteger(0);
171176

172177

173-
FluxSinkAdapterListener(FluxSink<Part> sink) {
178+
FluxSinkAdapterListener(FluxSink<Part> sink, DataBufferFactory bufferFactory) {
174179
this.sink = sink;
180+
this.bufferFactory = bufferFactory;
175181
}
176182

177183

178184
@Override
179185
public void onPartFinished(StreamStorage storage, Map<String, List<String>> headers) {
180186
HttpHeaders httpHeaders = new HttpHeaders();
181187
httpHeaders.putAll(headers);
182-
this.sink.next(new SynchronossPart(httpHeaders, storage));
188+
Part part = MultipartUtils.getFileName(httpHeaders) != null ?
189+
new SynchronossFilePart(httpHeaders, storage, this.bufferFactory) :
190+
new DefaultSynchronossPart(httpHeaders, storage, this.bufferFactory);
191+
this.sink.next(part);
183192
}
184193

185194
@Override
186195
public void onFormFieldPartFinished(String name, String value, Map<String, List<String>> headers) {
187196
HttpHeaders httpHeaders = new HttpHeaders();
188197
httpHeaders.putAll(headers);
189-
this.sink.next(new SynchronossPart(httpHeaders, value));
198+
this.sink.next(new SynchronossFormFieldPart(httpHeaders, this.bufferFactory, value));
190199
}
191200

192201
@Override
@@ -213,31 +222,18 @@ public void onNestedPartFinished() {
213222
}
214223

215224

216-
private static class SynchronossPart implements Part {
225+
private static abstract class AbstractSynchronossPart implements Part {
217226

218227
private final HttpHeaders headers;
219228

220-
private final StreamStorage storage;
221-
222-
private final String content;
223-
224-
private final DataBufferFactory bufferFactory = new DefaultDataBufferFactory();
225-
229+
private final DataBufferFactory bufferFactory;
226230

227-
SynchronossPart(HttpHeaders headers, StreamStorage storage) {
228-
Assert.notNull(headers, "HttpHeaders is required");
229-
Assert.notNull(storage, "'storage' is required");
230-
this.headers = headers;
231-
this.storage = storage;
232-
this.content = null;
233-
}
234231

235-
SynchronossPart(HttpHeaders headers, String content) {
232+
AbstractSynchronossPart(HttpHeaders headers, DataBufferFactory bufferFactory) {
236233
Assert.notNull(headers, "HttpHeaders is required");
237-
Assert.notNull(content, "'content' is required");
234+
Assert.notNull(bufferFactory, "'bufferFactory' is required");
238235
this.headers = headers;
239-
this.storage = null;
240-
this.content = content;
236+
this.bufferFactory = bufferFactory;
241237
}
242238

243239

@@ -251,52 +247,53 @@ public HttpHeaders getHeaders() {
251247
return this.headers;
252248
}
253249

254-
@Override
255-
public Optional<String> getFilename() {
256-
return Optional.ofNullable(MultipartUtils.getFileName(this.headers));
250+
protected DataBufferFactory getBufferFactory() {
251+
return this.bufferFactory;
257252
}
253+
}
258254

259-
@Override
260-
public Mono<String> getContentAsString() {
261-
if (this.content != null) {
262-
return Mono.just(this.content);
263-
}
264-
try {
265-
InputStream inputStream = this.storage.getInputStream();
266-
Charset charset = getCharset();
267-
return Mono.just(StreamUtils.copyToString(inputStream, charset));
268-
}
269-
catch (IOException e) {
270-
return Mono.error(new IllegalStateException(
271-
"Error while reading part content as a string", e));
272-
}
273-
}
255+
private static class DefaultSynchronossPart extends AbstractSynchronossPart {
274256

275-
private Charset getCharset() {
276-
return Optional.ofNullable(this.headers.getContentType())
277-
.map(MimeType::getCharset).orElse(StandardCharsets.UTF_8);
257+
private final StreamStorage storage;
258+
259+
260+
DefaultSynchronossPart(HttpHeaders headers, StreamStorage storage, DataBufferFactory factory) {
261+
super(headers, factory);
262+
Assert.notNull(storage, "'storage' is required");
263+
this.storage = storage;
278264
}
279265

266+
280267
@Override
281268
public Flux<DataBuffer> getContent() {
282-
if (this.content != null) {
283-
DataBuffer buffer = this.bufferFactory.allocateBuffer(this.content.length());
284-
buffer.write(this.content.getBytes());
285-
return Flux.just(buffer);
286-
}
287269
InputStream inputStream = this.storage.getInputStream();
288-
return DataBufferUtils.read(inputStream, this.bufferFactory, 4096);
270+
return DataBufferUtils.read(inputStream, getBufferFactory(), 4096);
271+
}
272+
273+
protected StreamStorage getStorage() {
274+
return this.storage;
275+
}
276+
}
277+
278+
private static class SynchronossFilePart extends DefaultSynchronossPart implements FilePart {
279+
280+
281+
public SynchronossFilePart(HttpHeaders headers, StreamStorage storage, DataBufferFactory factory) {
282+
super(headers, storage, factory);
283+
}
284+
285+
286+
@Override
287+
public String getFilename() {
288+
return MultipartUtils.getFileName(getHeaders());
289289
}
290290

291291
@Override
292292
public Mono<Void> transferTo(File destination) {
293-
if (this.storage == null || !getFilename().isPresent()) {
294-
return Mono.error(new IllegalStateException("The part does not represent a file."));
295-
}
296293
ReadableByteChannel input = null;
297294
FileChannel output = null;
298295
try {
299-
input = Channels.newChannel(this.storage.getInputStream());
296+
input = Channels.newChannel(getStorage().getInputStream());
300297
output = new FileOutputStream(destination).getChannel();
301298

302299
long size = (input instanceof FileChannel ? ((FileChannel) input).size() : Long.MAX_VALUE);
@@ -332,4 +329,34 @@ public Mono<Void> transferTo(File destination) {
332329
}
333330
}
334331

332+
private static class SynchronossFormFieldPart extends AbstractSynchronossPart implements FormFieldPart {
333+
334+
private final String content;
335+
336+
337+
SynchronossFormFieldPart(HttpHeaders headers, DataBufferFactory bufferFactory, String content) {
338+
super(headers, bufferFactory);
339+
this.content = content;
340+
}
341+
342+
343+
@Override
344+
public String getValue() {
345+
return this.content;
346+
}
347+
348+
@Override
349+
public Flux<DataBuffer> getContent() {
350+
byte[] bytes = this.content.getBytes(getCharset());
351+
DataBuffer buffer = getBufferFactory().allocateBuffer(bytes.length);
352+
buffer.write(bytes);
353+
return Flux.just(buffer);
354+
}
355+
356+
private Charset getCharset() {
357+
return Optional.ofNullable(MultipartUtils.getCharEncoding(getHeaders()))
358+
.map(Charset::forName).orElse(StandardCharsets.UTF_8);
359+
}
360+
}
361+
335362
}

0 commit comments

Comments
 (0)