Support data binding for multipart requests in WebFlux
Issue: SPR-14546
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2002-2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.http.codec.multipart;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* Specialization of {@link Part} for a file upload.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public interface FilePart extends Part {
|
||||
|
||||
/**
|
||||
* Return the name of the file selected by the user in a browser form.
|
||||
*/
|
||||
String getFilename();
|
||||
|
||||
|
||||
/**
|
||||
* Transfer the file in this part to the given file destination.
|
||||
* @param dest the target file
|
||||
* @return completion {@code Mono} with the result of the file transfer,
|
||||
* possibly {@link IllegalStateException} if the part isn't a file
|
||||
*/
|
||||
Mono<Void> transferTo(File dest);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2002-2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.http.codec.multipart;
|
||||
|
||||
/**
|
||||
* Specialization of {@link Part} for a form field.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public interface FormFieldPart extends Part {
|
||||
|
||||
/**
|
||||
* Return the form field value.
|
||||
*/
|
||||
String getValue();
|
||||
|
||||
}
|
||||
@@ -16,11 +16,7 @@
|
||||
|
||||
package org.springframework.http.codec.multipart;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Optional;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
@@ -29,9 +25,10 @@ import org.springframework.http.HttpHeaders;
|
||||
* Representation for a part in a "multipart/form-data" request.
|
||||
*
|
||||
* <p>The origin of a multipart request may a browser form in which case each
|
||||
* part represents a text-based form field or a file upload. Multipart requests
|
||||
* may also be used outside of browsers to transfer data with any content type
|
||||
* such as JSON, PDF, etc.
|
||||
* part is either a {@link FormFieldPart} or a {@link FilePart}.
|
||||
*
|
||||
* <p>Multipart requests may also be used outside of a browser for data of any
|
||||
* content type (e.g. JSON, PDF, etc).
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @author Rossen Stoyanchev
|
||||
@@ -53,30 +50,9 @@ public interface Part {
|
||||
*/
|
||||
HttpHeaders getHeaders();
|
||||
|
||||
/**
|
||||
*
|
||||
* Return the name of the file selected by the user in a browser form.
|
||||
* @return the filename if defined and available
|
||||
*/
|
||||
Optional<String> getFilename();
|
||||
|
||||
/**
|
||||
* Return the part content converted to a String with the charset from the
|
||||
* {@code Content-Type} header or {@code UTF-8} by default.
|
||||
*/
|
||||
Mono<String> getContentAsString();
|
||||
|
||||
/**
|
||||
* Return the part raw content as a stream of DataBuffer's.
|
||||
*/
|
||||
Flux<DataBuffer> getContent();
|
||||
|
||||
/**
|
||||
* Transfer the file in this part to the given file destination.
|
||||
* @param destination the target file
|
||||
* @return completion {@code Mono} with the result of the file transfer,
|
||||
* possibly {@link IllegalStateException} if the part isn't a file
|
||||
*/
|
||||
Mono<Void> transferTo(File destination);
|
||||
|
||||
}
|
||||
|
||||
@@ -52,8 +52,6 @@ import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ReactiveHttpInputMessage;
|
||||
import org.springframework.http.codec.HttpMessageReader;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.MimeType;
|
||||
import org.springframework.util.StreamUtils;
|
||||
|
||||
/**
|
||||
* {@code HttpMessageReader} for parsing {@code "multipart/form-data"} requests
|
||||
@@ -71,6 +69,8 @@ import org.springframework.util.StreamUtils;
|
||||
*/
|
||||
public class SynchronossPartHttpMessageReader implements HttpMessageReader<Part> {
|
||||
|
||||
private final DataBufferFactory bufferFactory = new DefaultDataBufferFactory();
|
||||
|
||||
|
||||
@Override
|
||||
public List<MediaType> getReadableMediaTypes() {
|
||||
@@ -88,7 +88,7 @@ public class SynchronossPartHttpMessageReader implements HttpMessageReader<Part>
|
||||
public Flux<Part> read(ResolvableType elementType, ReactiveHttpInputMessage message,
|
||||
Map<String, Object> hints) {
|
||||
|
||||
return Flux.create(new SynchronossPartGenerator(message));
|
||||
return Flux.create(new SynchronossPartGenerator(message, this.bufferFactory));
|
||||
}
|
||||
|
||||
|
||||
@@ -109,9 +109,12 @@ public class SynchronossPartHttpMessageReader implements HttpMessageReader<Part>
|
||||
|
||||
private final ReactiveHttpInputMessage inputMessage;
|
||||
|
||||
private final DataBufferFactory bufferFactory;
|
||||
|
||||
SynchronossPartGenerator(ReactiveHttpInputMessage inputMessage) {
|
||||
|
||||
SynchronossPartGenerator(ReactiveHttpInputMessage inputMessage, DataBufferFactory factory) {
|
||||
this.inputMessage = inputMessage;
|
||||
this.bufferFactory = factory;
|
||||
}
|
||||
|
||||
|
||||
@@ -119,7 +122,7 @@ public class SynchronossPartHttpMessageReader implements HttpMessageReader<Part>
|
||||
public void accept(FluxSink<Part> emitter) {
|
||||
|
||||
MultipartContext context = createMultipartContext();
|
||||
NioMultipartParserListener listener = new FluxSinkAdapterListener(emitter);
|
||||
NioMultipartParserListener listener = new FluxSinkAdapterListener(emitter, this.bufferFactory);
|
||||
NioMultipartParser parser = Multipart.multipart(context).forNIO(listener);
|
||||
|
||||
this.inputMessage.getBody().subscribe(buffer -> {
|
||||
@@ -167,11 +170,14 @@ public class SynchronossPartHttpMessageReader implements HttpMessageReader<Part>
|
||||
|
||||
private final FluxSink<Part> sink;
|
||||
|
||||
private final DataBufferFactory bufferFactory;
|
||||
|
||||
private final AtomicInteger terminated = new AtomicInteger(0);
|
||||
|
||||
|
||||
FluxSinkAdapterListener(FluxSink<Part> sink) {
|
||||
FluxSinkAdapterListener(FluxSink<Part> sink, DataBufferFactory bufferFactory) {
|
||||
this.sink = sink;
|
||||
this.bufferFactory = bufferFactory;
|
||||
}
|
||||
|
||||
|
||||
@@ -179,14 +185,17 @@ public class SynchronossPartHttpMessageReader implements HttpMessageReader<Part>
|
||||
public void onPartFinished(StreamStorage storage, Map<String, List<String>> headers) {
|
||||
HttpHeaders httpHeaders = new HttpHeaders();
|
||||
httpHeaders.putAll(headers);
|
||||
this.sink.next(new SynchronossPart(httpHeaders, storage));
|
||||
Part part = MultipartUtils.getFileName(httpHeaders) != null ?
|
||||
new SynchronossFilePart(httpHeaders, storage, this.bufferFactory) :
|
||||
new DefaultSynchronossPart(httpHeaders, storage, this.bufferFactory);
|
||||
this.sink.next(part);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFormFieldPartFinished(String name, String value, Map<String, List<String>> headers) {
|
||||
HttpHeaders httpHeaders = new HttpHeaders();
|
||||
httpHeaders.putAll(headers);
|
||||
this.sink.next(new SynchronossPart(httpHeaders, value));
|
||||
this.sink.next(new SynchronossFormFieldPart(httpHeaders, this.bufferFactory, value));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -213,31 +222,18 @@ public class SynchronossPartHttpMessageReader implements HttpMessageReader<Part>
|
||||
}
|
||||
|
||||
|
||||
private static class SynchronossPart implements Part {
|
||||
private static abstract class AbstractSynchronossPart implements Part {
|
||||
|
||||
private final HttpHeaders headers;
|
||||
|
||||
private final StreamStorage storage;
|
||||
|
||||
private final String content;
|
||||
|
||||
private final DataBufferFactory bufferFactory = new DefaultDataBufferFactory();
|
||||
private final DataBufferFactory bufferFactory;
|
||||
|
||||
|
||||
SynchronossPart(HttpHeaders headers, StreamStorage storage) {
|
||||
AbstractSynchronossPart(HttpHeaders headers, DataBufferFactory bufferFactory) {
|
||||
Assert.notNull(headers, "HttpHeaders is required");
|
||||
Assert.notNull(storage, "'storage' is required");
|
||||
Assert.notNull(bufferFactory, "'bufferFactory' is required");
|
||||
this.headers = headers;
|
||||
this.storage = storage;
|
||||
this.content = null;
|
||||
}
|
||||
|
||||
SynchronossPart(HttpHeaders headers, String content) {
|
||||
Assert.notNull(headers, "HttpHeaders is required");
|
||||
Assert.notNull(content, "'content' is required");
|
||||
this.headers = headers;
|
||||
this.storage = null;
|
||||
this.content = content;
|
||||
this.bufferFactory = bufferFactory;
|
||||
}
|
||||
|
||||
|
||||
@@ -251,52 +247,53 @@ public class SynchronossPartHttpMessageReader implements HttpMessageReader<Part>
|
||||
return this.headers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> getFilename() {
|
||||
return Optional.ofNullable(MultipartUtils.getFileName(this.headers));
|
||||
protected DataBufferFactory getBufferFactory() {
|
||||
return this.bufferFactory;
|
||||
}
|
||||
}
|
||||
|
||||
private static class DefaultSynchronossPart extends AbstractSynchronossPart {
|
||||
|
||||
private final StreamStorage storage;
|
||||
|
||||
|
||||
DefaultSynchronossPart(HttpHeaders headers, StreamStorage storage, DataBufferFactory factory) {
|
||||
super(headers, factory);
|
||||
Assert.notNull(storage, "'storage' is required");
|
||||
this.storage = storage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<String> getContentAsString() {
|
||||
if (this.content != null) {
|
||||
return Mono.just(this.content);
|
||||
}
|
||||
try {
|
||||
InputStream inputStream = this.storage.getInputStream();
|
||||
Charset charset = getCharset();
|
||||
return Mono.just(StreamUtils.copyToString(inputStream, charset));
|
||||
}
|
||||
catch (IOException e) {
|
||||
return Mono.error(new IllegalStateException(
|
||||
"Error while reading part content as a string", e));
|
||||
}
|
||||
}
|
||||
|
||||
private Charset getCharset() {
|
||||
return Optional.ofNullable(this.headers.getContentType())
|
||||
.map(MimeType::getCharset).orElse(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<DataBuffer> getContent() {
|
||||
if (this.content != null) {
|
||||
DataBuffer buffer = this.bufferFactory.allocateBuffer(this.content.length());
|
||||
buffer.write(this.content.getBytes());
|
||||
return Flux.just(buffer);
|
||||
}
|
||||
InputStream inputStream = this.storage.getInputStream();
|
||||
return DataBufferUtils.read(inputStream, this.bufferFactory, 4096);
|
||||
return DataBufferUtils.read(inputStream, getBufferFactory(), 4096);
|
||||
}
|
||||
|
||||
protected StreamStorage getStorage() {
|
||||
return this.storage;
|
||||
}
|
||||
}
|
||||
|
||||
private static class SynchronossFilePart extends DefaultSynchronossPart implements FilePart {
|
||||
|
||||
|
||||
public SynchronossFilePart(HttpHeaders headers, StreamStorage storage, DataBufferFactory factory) {
|
||||
super(headers, storage, factory);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return MultipartUtils.getFileName(getHeaders());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> transferTo(File destination) {
|
||||
if (this.storage == null || !getFilename().isPresent()) {
|
||||
return Mono.error(new IllegalStateException("The part does not represent a file."));
|
||||
}
|
||||
ReadableByteChannel input = null;
|
||||
FileChannel output = null;
|
||||
try {
|
||||
input = Channels.newChannel(this.storage.getInputStream());
|
||||
input = Channels.newChannel(getStorage().getInputStream());
|
||||
output = new FileOutputStream(destination).getChannel();
|
||||
|
||||
long size = (input instanceof FileChannel ? ((FileChannel) input).size() : Long.MAX_VALUE);
|
||||
@@ -332,4 +329,34 @@ public class SynchronossPartHttpMessageReader implements HttpMessageReader<Part>
|
||||
}
|
||||
}
|
||||
|
||||
private static class SynchronossFormFieldPart extends AbstractSynchronossPart implements FormFieldPart {
|
||||
|
||||
private final String content;
|
||||
|
||||
|
||||
SynchronossFormFieldPart(HttpHeaders headers, DataBufferFactory bufferFactory, String content) {
|
||||
super(headers, bufferFactory);
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getValue() {
|
||||
return this.content;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<DataBuffer> getContent() {
|
||||
byte[] bytes = this.content.getBytes(getCharset());
|
||||
DataBuffer buffer = getBufferFactory().allocateBuffer(bytes.length);
|
||||
buffer.write(bytes);
|
||||
return Flux.just(buffer);
|
||||
}
|
||||
|
||||
private Charset getCharset() {
|
||||
return Optional.ofNullable(MultipartUtils.getCharEncoding(getHeaders()))
|
||||
.map(Charset::forName).orElse(StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,10 +19,12 @@ package org.springframework.web.bind.support;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.beans.MutablePropertyValues;
|
||||
import org.springframework.http.codec.multipart.FormFieldPart;
|
||||
import org.springframework.http.codec.multipart.Part;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
@@ -105,6 +107,9 @@ public class WebExchangeDataBinder extends WebDataBinder {
|
||||
|
||||
private static void addBindValue(Map<String, Object> params, String key, List<?> values) {
|
||||
if (!CollectionUtils.isEmpty(values)) {
|
||||
values = values.stream()
|
||||
.map(value -> value instanceof FormFieldPart ? ((FormFieldPart) value).getValue() : value)
|
||||
.collect(Collectors.toList());
|
||||
params.put(key, values.size() == 1 ? values.get(0) : values);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user