parent
5af3223e76
commit
1da903dd59
|
|
@ -17,11 +17,9 @@
|
|||
package org.springframework.http.codec.multipart;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
|
@ -49,11 +47,9 @@ import org.springframework.http.ReactiveHttpOutputMessage;
|
|||
import org.springframework.http.codec.EncoderHttpMessageWriter;
|
||||
import org.springframework.http.codec.FormHttpMessageWriter;
|
||||
import org.springframework.http.codec.HttpMessageWriter;
|
||||
import org.springframework.http.codec.LoggingCodecSupport;
|
||||
import org.springframework.http.codec.ResourceHttpMessageWriter;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.MimeTypeUtils;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
/**
|
||||
|
|
@ -77,14 +73,9 @@ import org.springframework.util.MultiValueMap;
|
|||
* @since 5.0
|
||||
* @see FormHttpMessageWriter
|
||||
*/
|
||||
public class MultipartHttpMessageWriter extends LoggingCodecSupport
|
||||
public class MultipartHttpMessageWriter extends MultipartWriterSupport
|
||||
implements HttpMessageWriter<MultiValueMap<String, ?>> {
|
||||
|
||||
/**
|
||||
* THe default charset used by the writer.
|
||||
*/
|
||||
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
|
||||
|
||||
/** Suppress logging from individual part writers (full map logged at this level). */
|
||||
private static final Map<String, Object> DEFAULT_HINTS = Hints.from(Hints.SUPPRESS_LOGGING_HINT, true);
|
||||
|
||||
|
|
@ -94,10 +85,6 @@ public class MultipartHttpMessageWriter extends LoggingCodecSupport
|
|||
@Nullable
|
||||
private final HttpMessageWriter<MultiValueMap<String, String>> formWriter;
|
||||
|
||||
private Charset charset = DEFAULT_CHARSET;
|
||||
|
||||
private final List<MediaType> supportedMediaTypes;
|
||||
|
||||
|
||||
/**
|
||||
* Constructor with a default list of part writers (String and Resource).
|
||||
|
|
@ -126,9 +113,9 @@ public class MultipartHttpMessageWriter extends LoggingCodecSupport
|
|||
public MultipartHttpMessageWriter(List<HttpMessageWriter<?>> partWriters,
|
||||
@Nullable HttpMessageWriter<MultiValueMap<String, String>> formWriter) {
|
||||
|
||||
super(initMediaTypes(formWriter));
|
||||
this.partWriters = partWriters;
|
||||
this.formWriter = formWriter;
|
||||
this.supportedMediaTypes = initMediaTypes(formWriter);
|
||||
}
|
||||
|
||||
private static List<MediaType> initMediaTypes(@Nullable HttpMessageWriter<?> formWriter) {
|
||||
|
|
@ -168,25 +155,6 @@ public class MultipartHttpMessageWriter extends LoggingCodecSupport
|
|||
this.charset = charset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the configured charset for part headers.
|
||||
*/
|
||||
public Charset getCharset() {
|
||||
return this.charset;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public List<MediaType> getWritableMediaTypes() {
|
||||
return this.supportedMediaTypes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canWrite(ResolvableType elementType, @Nullable MediaType mediaType) {
|
||||
return (MultiValueMap.class.isAssignableFrom(elementType.toClass()) &&
|
||||
(mediaType == null ||
|
||||
this.supportedMediaTypes.stream().anyMatch(element -> element.isCompatibleWith(mediaType))));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> write(Publisher<? extends MultiValueMap<String, ?>> inputStream,
|
||||
|
|
@ -225,16 +193,7 @@ public class MultipartHttpMessageWriter extends LoggingCodecSupport
|
|||
|
||||
byte[] boundary = generateMultipartBoundary();
|
||||
|
||||
Map<String, String> params = new HashMap<>();
|
||||
if (mediaType != null) {
|
||||
params.putAll(mediaType.getParameters());
|
||||
}
|
||||
params.put("boundary", new String(boundary, StandardCharsets.US_ASCII));
|
||||
params.put("charset", getCharset().name());
|
||||
|
||||
mediaType = (mediaType != null ? mediaType : MediaType.MULTIPART_FORM_DATA);
|
||||
mediaType = new MediaType(mediaType, params);
|
||||
|
||||
mediaType = getMultipartMediaType(mediaType, boundary);
|
||||
outputMessage.getHeaders().setContentType(mediaType);
|
||||
|
||||
LogFormatUtils.traceDebug(logger, traceOn -> Hints.getLogPrefix(hints) + "Encoding " +
|
||||
|
|
@ -252,14 +211,6 @@ public class MultipartHttpMessageWriter extends LoggingCodecSupport
|
|||
return outputMessage.writeWith(body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a multipart boundary.
|
||||
* <p>By default delegates to {@link MimeTypeUtils#generateMultipartBoundary()}.
|
||||
*/
|
||||
protected byte[] generateMultipartBoundary() {
|
||||
return MimeTypeUtils.generateMultipartBoundary();
|
||||
}
|
||||
|
||||
private Flux<DataBuffer> encodePartValues(
|
||||
byte[] boundary, String name, List<?> values, DataBufferFactory bufferFactory) {
|
||||
|
||||
|
|
@ -268,15 +219,15 @@ public class MultipartHttpMessageWriter extends LoggingCodecSupport
|
|||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T> Flux<DataBuffer> encodePart(byte[] boundary, String name, T value, DataBufferFactory bufferFactory) {
|
||||
MultipartHttpOutputMessage outputMessage = new MultipartHttpOutputMessage(bufferFactory, getCharset());
|
||||
HttpHeaders outputHeaders = outputMessage.getHeaders();
|
||||
private <T> Flux<DataBuffer> encodePart(byte[] boundary, String name, T value, DataBufferFactory factory) {
|
||||
MultipartHttpOutputMessage message = new MultipartHttpOutputMessage(factory);
|
||||
HttpHeaders headers = message.getHeaders();
|
||||
|
||||
T body;
|
||||
ResolvableType resolvableType = null;
|
||||
if (value instanceof HttpEntity) {
|
||||
HttpEntity<T> httpEntity = (HttpEntity<T>) value;
|
||||
outputHeaders.putAll(httpEntity.getHeaders());
|
||||
headers.putAll(httpEntity.getHeaders());
|
||||
body = httpEntity.getBody();
|
||||
Assert.state(body != null, "MultipartHttpMessageWriter only supports HttpEntity with body");
|
||||
if (httpEntity instanceof ResolvableTypeProvider) {
|
||||
|
|
@ -290,20 +241,20 @@ public class MultipartHttpMessageWriter extends LoggingCodecSupport
|
|||
resolvableType = ResolvableType.forClass(body.getClass());
|
||||
}
|
||||
|
||||
if (!outputHeaders.containsKey(HttpHeaders.CONTENT_DISPOSITION)) {
|
||||
if (!headers.containsKey(HttpHeaders.CONTENT_DISPOSITION)) {
|
||||
if (body instanceof Resource) {
|
||||
outputHeaders.setContentDispositionFormData(name, ((Resource) body).getFilename());
|
||||
headers.setContentDispositionFormData(name, ((Resource) body).getFilename());
|
||||
}
|
||||
else if (resolvableType.resolve() == Resource.class) {
|
||||
body = (T) Mono.from((Publisher<?>) body).doOnNext(o -> outputHeaders
|
||||
body = (T) Mono.from((Publisher<?>) body).doOnNext(o -> headers
|
||||
.setContentDispositionFormData(name, ((Resource) o).getFilename()));
|
||||
}
|
||||
else {
|
||||
outputHeaders.setContentDispositionFormData(name, null);
|
||||
headers.setContentDispositionFormData(name, null);
|
||||
}
|
||||
}
|
||||
|
||||
MediaType contentType = outputHeaders.getContentType();
|
||||
MediaType contentType = headers.getContentType();
|
||||
|
||||
final ResolvableType finalBodyType = resolvableType;
|
||||
Optional<HttpMessageWriter<?>> writer = this.partWriters.stream()
|
||||
|
|
@ -321,62 +272,24 @@ public class MultipartHttpMessageWriter extends LoggingCodecSupport
|
|||
// but only stores the body Flux and returns Mono.empty().
|
||||
|
||||
Mono<Void> partContentReady = ((HttpMessageWriter<T>) writer.get())
|
||||
.write(bodyPublisher, resolvableType, contentType, outputMessage, DEFAULT_HINTS);
|
||||
.write(bodyPublisher, resolvableType, contentType, message, DEFAULT_HINTS);
|
||||
|
||||
// After partContentReady, we can access the part content from MultipartHttpOutputMessage
|
||||
// and use it for writing to the actual request body
|
||||
|
||||
Flux<DataBuffer> partContent = partContentReady.thenMany(Flux.defer(outputMessage::getBody));
|
||||
Flux<DataBuffer> partContent = partContentReady.thenMany(Flux.defer(message::getBody));
|
||||
|
||||
return Flux.concat(
|
||||
generateBoundaryLine(boundary, bufferFactory),
|
||||
generateBoundaryLine(boundary, factory),
|
||||
partContent,
|
||||
generateNewLine(bufferFactory));
|
||||
generateNewLine(factory));
|
||||
}
|
||||
|
||||
|
||||
private Mono<DataBuffer> generateBoundaryLine(byte[] boundary, DataBufferFactory bufferFactory) {
|
||||
return Mono.fromCallable(() -> {
|
||||
DataBuffer buffer = bufferFactory.allocateBuffer(boundary.length + 4);
|
||||
buffer.write((byte)'-');
|
||||
buffer.write((byte)'-');
|
||||
buffer.write(boundary);
|
||||
buffer.write((byte)'\r');
|
||||
buffer.write((byte)'\n');
|
||||
return buffer;
|
||||
});
|
||||
}
|
||||
|
||||
private Mono<DataBuffer> generateNewLine(DataBufferFactory bufferFactory) {
|
||||
return Mono.fromCallable(() -> {
|
||||
DataBuffer buffer = bufferFactory.allocateBuffer(2);
|
||||
buffer.write((byte)'\r');
|
||||
buffer.write((byte)'\n');
|
||||
return buffer;
|
||||
});
|
||||
}
|
||||
|
||||
private Mono<DataBuffer> generateLastLine(byte[] boundary, DataBufferFactory bufferFactory) {
|
||||
return Mono.fromCallable(() -> {
|
||||
DataBuffer buffer = bufferFactory.allocateBuffer(boundary.length + 6);
|
||||
buffer.write((byte)'-');
|
||||
buffer.write((byte)'-');
|
||||
buffer.write(boundary);
|
||||
buffer.write((byte)'-');
|
||||
buffer.write((byte)'-');
|
||||
buffer.write((byte)'\r');
|
||||
buffer.write((byte)'\n');
|
||||
return buffer;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private static class MultipartHttpOutputMessage implements ReactiveHttpOutputMessage {
|
||||
private class MultipartHttpOutputMessage implements ReactiveHttpOutputMessage {
|
||||
|
||||
private final DataBufferFactory bufferFactory;
|
||||
|
||||
private final Charset charset;
|
||||
|
||||
private final HttpHeaders headers = new HttpHeaders();
|
||||
|
||||
private final AtomicBoolean committed = new AtomicBoolean();
|
||||
|
|
@ -384,9 +297,8 @@ public class MultipartHttpMessageWriter extends LoggingCodecSupport
|
|||
@Nullable
|
||||
private Flux<DataBuffer> body;
|
||||
|
||||
public MultipartHttpOutputMessage(DataBufferFactory bufferFactory, Charset charset) {
|
||||
public MultipartHttpOutputMessage(DataBufferFactory bufferFactory) {
|
||||
this.bufferFactory = bufferFactory;
|
||||
this.charset = charset;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -414,33 +326,12 @@ public class MultipartHttpMessageWriter extends LoggingCodecSupport
|
|||
if (this.body != null) {
|
||||
return Mono.error(new IllegalStateException("Multiple calls to writeWith() not supported"));
|
||||
}
|
||||
this.body = generateHeaders().concatWith(body);
|
||||
this.body = generatePartHeaders(this.headers, this.bufferFactory).concatWith(body);
|
||||
|
||||
// We don't actually want to write (just save the body Flux)
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
private Mono<DataBuffer> generateHeaders() {
|
||||
return Mono.fromCallable(() -> {
|
||||
DataBuffer buffer = this.bufferFactory.allocateBuffer();
|
||||
for (Map.Entry<String, List<String>> entry : this.headers.entrySet()) {
|
||||
byte[] headerName = entry.getKey().getBytes(this.charset);
|
||||
for (String headerValueString : entry.getValue()) {
|
||||
byte[] headerValue = headerValueString.getBytes(this.charset);
|
||||
buffer.write(headerName);
|
||||
buffer.write((byte)':');
|
||||
buffer.write((byte)' ');
|
||||
buffer.write(headerValue);
|
||||
buffer.write((byte)'\r');
|
||||
buffer.write((byte)'\n');
|
||||
}
|
||||
}
|
||||
buffer.write((byte)'\r');
|
||||
buffer.write((byte)'\n');
|
||||
return buffer;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body) {
|
||||
return Mono.error(new UnsupportedOperationException());
|
||||
|
|
|
|||
|
|
@ -0,0 +1,168 @@
|
|||
/*
|
||||
* Copyright 2002-2020 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
|
||||
*
|
||||
* https://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.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DataBufferFactory;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.codec.LoggingCodecSupport;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.MimeTypeUtils;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
/**
|
||||
* Support class for multipart HTTP message writers.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.3
|
||||
*/
|
||||
public class MultipartWriterSupport extends LoggingCodecSupport {
|
||||
|
||||
/** THe default charset used by the writer. */
|
||||
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
|
||||
|
||||
protected final List<MediaType> supportedMediaTypes;
|
||||
|
||||
protected Charset charset = DEFAULT_CHARSET;
|
||||
|
||||
|
||||
/**
|
||||
* Constructor with the list of supported media types.
|
||||
*/
|
||||
protected MultipartWriterSupport(List<MediaType> supportedMediaTypes) {
|
||||
this.supportedMediaTypes = supportedMediaTypes;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the configured charset for part headers.
|
||||
*/
|
||||
public Charset getCharset() {
|
||||
return this.charset;
|
||||
}
|
||||
|
||||
public List<MediaType> getWritableMediaTypes() {
|
||||
return this.supportedMediaTypes;
|
||||
}
|
||||
|
||||
|
||||
public boolean canWrite(ResolvableType elementType, @Nullable MediaType mediaType) {
|
||||
if (MultiValueMap.class.isAssignableFrom(elementType.toClass())) {
|
||||
if (mediaType == null) {
|
||||
return true;
|
||||
}
|
||||
for (MediaType supportedMediaType : this.supportedMediaTypes) {
|
||||
if (supportedMediaType.isCompatibleWith(mediaType)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a multipart boundary.
|
||||
* <p>By default delegates to {@link MimeTypeUtils#generateMultipartBoundary()}.
|
||||
*/
|
||||
protected byte[] generateMultipartBoundary() {
|
||||
return MimeTypeUtils.generateMultipartBoundary();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the {@code MediaType} to use by adding "boundary" and "charset"
|
||||
* parameters to the given {@code mediaType} or "mulitpart/form-data"
|
||||
* otherwise by default.
|
||||
*/
|
||||
protected MediaType getMultipartMediaType(@Nullable MediaType mediaType, byte[] boundary) {
|
||||
Map<String, String> params = new HashMap<>();
|
||||
if (mediaType != null) {
|
||||
params.putAll(mediaType.getParameters());
|
||||
}
|
||||
params.put("boundary", new String(boundary, StandardCharsets.US_ASCII));
|
||||
params.put("charset", getCharset().name());
|
||||
|
||||
mediaType = (mediaType != null ? mediaType : MediaType.MULTIPART_FORM_DATA);
|
||||
mediaType = new MediaType(mediaType, params);
|
||||
return mediaType;
|
||||
}
|
||||
|
||||
protected Mono<DataBuffer> generateBoundaryLine(byte[] boundary, DataBufferFactory bufferFactory) {
|
||||
return Mono.fromCallable(() -> {
|
||||
DataBuffer buffer = bufferFactory.allocateBuffer(boundary.length + 4);
|
||||
buffer.write((byte)'-');
|
||||
buffer.write((byte)'-');
|
||||
buffer.write(boundary);
|
||||
buffer.write((byte)'\r');
|
||||
buffer.write((byte)'\n');
|
||||
return buffer;
|
||||
});
|
||||
}
|
||||
|
||||
protected Mono<DataBuffer> generateNewLine(DataBufferFactory bufferFactory) {
|
||||
return Mono.fromCallable(() -> {
|
||||
DataBuffer buffer = bufferFactory.allocateBuffer(2);
|
||||
buffer.write((byte)'\r');
|
||||
buffer.write((byte)'\n');
|
||||
return buffer;
|
||||
});
|
||||
}
|
||||
|
||||
protected Mono<DataBuffer> generateLastLine(byte[] boundary, DataBufferFactory bufferFactory) {
|
||||
return Mono.fromCallable(() -> {
|
||||
DataBuffer buffer = bufferFactory.allocateBuffer(boundary.length + 6);
|
||||
buffer.write((byte)'-');
|
||||
buffer.write((byte)'-');
|
||||
buffer.write(boundary);
|
||||
buffer.write((byte)'-');
|
||||
buffer.write((byte)'-');
|
||||
buffer.write((byte)'\r');
|
||||
buffer.write((byte)'\n');
|
||||
return buffer;
|
||||
});
|
||||
}
|
||||
|
||||
protected Mono<DataBuffer> generatePartHeaders(HttpHeaders headers, DataBufferFactory bufferFactory) {
|
||||
return Mono.fromCallable(() -> {
|
||||
DataBuffer buffer = bufferFactory.allocateBuffer();
|
||||
for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
|
||||
byte[] headerName = entry.getKey().getBytes(getCharset());
|
||||
for (String headerValueString : entry.getValue()) {
|
||||
byte[] headerValue = headerValueString.getBytes(getCharset());
|
||||
buffer.write(headerName);
|
||||
buffer.write((byte)':');
|
||||
buffer.write((byte)' ');
|
||||
buffer.write(headerValue);
|
||||
buffer.write((byte)'\r');
|
||||
buffer.write((byte)'\n');
|
||||
}
|
||||
}
|
||||
buffer.write((byte)'\r');
|
||||
buffer.write((byte)'\n');
|
||||
return buffer;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* Copyright 2002-2020 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
|
||||
*
|
||||
* https://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.util.Map;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.codec.Hints;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DataBufferFactory;
|
||||
import org.springframework.core.io.buffer.PooledDataBuffer;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ReactiveHttpOutputMessage;
|
||||
import org.springframework.http.codec.HttpMessageWriter;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* {@link HttpMessageWriter} for writing with {@link Part}. This can be useful
|
||||
* on the server side to write a {@code Flux<Part>} received from a client to
|
||||
* some remote service.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.3
|
||||
*/
|
||||
public class PartHttpMessageWriter extends MultipartWriterSupport implements HttpMessageWriter<Part> {
|
||||
|
||||
|
||||
public PartHttpMessageWriter() {
|
||||
super(MultipartHttpMessageReader.MIME_TYPES);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Mono<Void> write(Publisher<? extends Part> parts,
|
||||
ResolvableType elementType, @Nullable MediaType mediaType, ReactiveHttpOutputMessage outputMessage,
|
||||
Map<String, Object> hints) {
|
||||
|
||||
byte[] boundary = generateMultipartBoundary();
|
||||
|
||||
mediaType = getMultipartMediaType(mediaType, boundary);
|
||||
outputMessage.getHeaders().setContentType(mediaType);
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug(Hints.getLogPrefix(hints) + "Encoding Publisher<Part>");
|
||||
}
|
||||
|
||||
Flux<DataBuffer> body = Flux.from(parts)
|
||||
.concatMap(part -> encodePart(boundary, part, outputMessage.bufferFactory()))
|
||||
.concatWith(generateLastLine(boundary, outputMessage.bufferFactory()))
|
||||
.doOnDiscard(PooledDataBuffer.class, PooledDataBuffer::release);
|
||||
|
||||
return outputMessage.writeWith(body);
|
||||
}
|
||||
|
||||
private <T> Flux<DataBuffer> encodePart(byte[] boundary, Part part, DataBufferFactory bufferFactory) {
|
||||
HttpHeaders headers = new HttpHeaders(part.headers());
|
||||
|
||||
String name = part.name();
|
||||
if (!headers.containsKey(HttpHeaders.CONTENT_DISPOSITION)) {
|
||||
headers.setContentDispositionFormData(name,
|
||||
(part instanceof FilePart ? ((FilePart) part).filename() : null));
|
||||
}
|
||||
|
||||
return Flux.concat(
|
||||
generateBoundaryLine(boundary, bufferFactory),
|
||||
generatePartHeaders(headers, bufferFactory),
|
||||
part.content(),
|
||||
generateNewLine(bufferFactory));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2020 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.
|
||||
|
|
@ -23,6 +23,7 @@ import org.springframework.http.codec.HttpMessageWriter;
|
|||
import org.springframework.http.codec.ServerCodecConfigurer;
|
||||
import org.springframework.http.codec.ServerSentEventHttpMessageWriter;
|
||||
import org.springframework.http.codec.multipart.MultipartHttpMessageReader;
|
||||
import org.springframework.http.codec.multipart.PartHttpMessageWriter;
|
||||
import org.springframework.http.codec.multipart.SynchronossPartHttpMessageReader;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
|
|
@ -74,6 +75,11 @@ class ServerDefaultCodecsImpl extends BaseDefaultCodecs implements ServerCodecCo
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void extendTypedWriters(List<HttpMessageWriter<?>> typedWriters) {
|
||||
addCodec(typedWriters, new PartHttpMessageWriter());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void extendObjectWriters(List<HttpMessageWriter<?>> objectWriters) {
|
||||
objectWriters.add(new ServerSentEventHttpMessageWriter(getSseEncoder()));
|
||||
|
|
|
|||
|
|
@ -49,6 +49,8 @@ import static org.mockito.BDDMockito.given;
|
|||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link MultipartHttpMessageWriter}.
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
|
|
@ -118,39 +120,34 @@ public class MultipartHttpMessageWriterTests extends AbstractLeakCheckingTests {
|
|||
this.writer.write(result, null, MediaType.MULTIPART_FORM_DATA, this.response, hints)
|
||||
.block(Duration.ofSeconds(5));
|
||||
|
||||
MultiValueMap<String, Part> requestParts = parse(hints);
|
||||
MultiValueMap<String, Part> requestParts = parse(this.response, hints);
|
||||
assertThat(requestParts.size()).isEqualTo(7);
|
||||
|
||||
Part part = requestParts.getFirst("name 1");
|
||||
boolean condition4 = part instanceof FormFieldPart;
|
||||
assertThat(condition4).isTrue();
|
||||
assertThat(part instanceof FormFieldPart).isTrue();
|
||||
assertThat(part.name()).isEqualTo("name 1");
|
||||
assertThat(((FormFieldPart) part).value()).isEqualTo("value 1");
|
||||
|
||||
List<Part> parts2 = requestParts.get("name 2");
|
||||
assertThat(parts2.size()).isEqualTo(2);
|
||||
part = parts2.get(0);
|
||||
boolean condition3 = part instanceof FormFieldPart;
|
||||
assertThat(condition3).isTrue();
|
||||
assertThat(part instanceof FormFieldPart).isTrue();
|
||||
assertThat(part.name()).isEqualTo("name 2");
|
||||
assertThat(((FormFieldPart) part).value()).isEqualTo("value 2+1");
|
||||
part = parts2.get(1);
|
||||
boolean condition2 = part instanceof FormFieldPart;
|
||||
assertThat(condition2).isTrue();
|
||||
assertThat(part instanceof FormFieldPart).isTrue();
|
||||
assertThat(part.name()).isEqualTo("name 2");
|
||||
assertThat(((FormFieldPart) part).value()).isEqualTo("value 2+2");
|
||||
|
||||
part = requestParts.getFirst("logo");
|
||||
boolean condition1 = part instanceof FilePart;
|
||||
assertThat(condition1).isTrue();
|
||||
assertThat(part instanceof FilePart).isTrue();
|
||||
assertThat(part.name()).isEqualTo("logo");
|
||||
assertThat(((FilePart) part).filename()).isEqualTo("logo.jpg");
|
||||
assertThat(part.headers().getContentType()).isEqualTo(MediaType.IMAGE_JPEG);
|
||||
assertThat(part.headers().getContentLength()).isEqualTo(logo.getFile().length());
|
||||
|
||||
part = requestParts.getFirst("utf8");
|
||||
boolean condition = part instanceof FilePart;
|
||||
assertThat(condition).isTrue();
|
||||
assertThat(part instanceof FilePart).isTrue();
|
||||
assertThat(part.name()).isEqualTo("utf8");
|
||||
assertThat(((FilePart) part).filename()).isEqualTo("Hall\u00F6le.jpg");
|
||||
assertThat(part.headers().getContentType()).isEqualTo(MediaType.IMAGE_JPEG);
|
||||
|
|
@ -195,7 +192,7 @@ public class MultipartHttpMessageWriterTests extends AbstractLeakCheckingTests {
|
|||
assertThat(contentType.getParameter("boundary")).isNotEmpty();
|
||||
assertThat(contentType.getParameter("charset")).isEqualTo("UTF-8");
|
||||
|
||||
MultiValueMap<String, Part> requestParts = parse(hints);
|
||||
MultiValueMap<String, Part> requestParts = parse(this.response, hints);
|
||||
assertThat(requestParts.size()).isEqualTo(2);
|
||||
assertThat(requestParts.getFirst("name 1").name()).isEqualTo("name 1");
|
||||
assertThat(requestParts.getFirst("name 2").name()).isEqualTo("name 2");
|
||||
|
|
@ -222,13 +219,12 @@ public class MultipartHttpMessageWriterTests extends AbstractLeakCheckingTests {
|
|||
Map<String, Object> hints = Collections.emptyMap();
|
||||
this.writer.write(result, null, MediaType.MULTIPART_FORM_DATA, this.response, hints).block();
|
||||
|
||||
MultiValueMap<String, Part> requestParts = parse(hints);
|
||||
MultiValueMap<String, Part> requestParts = parse(this.response, hints);
|
||||
assertThat(requestParts.size()).isEqualTo(1);
|
||||
|
||||
Part part = requestParts.getFirst("logo");
|
||||
assertThat(part.name()).isEqualTo("logo");
|
||||
boolean condition = part instanceof FilePart;
|
||||
assertThat(condition).isTrue();
|
||||
assertThat(part instanceof FilePart).isTrue();
|
||||
assertThat(((FilePart) part).filename()).isEqualTo("logo.jpg");
|
||||
assertThat(part.headers().getContentType()).isEqualTo(MediaType.IMAGE_JPEG);
|
||||
assertThat(part.headers().getContentLength()).isEqualTo(logo.getFile().length());
|
||||
|
|
@ -273,24 +269,22 @@ public class MultipartHttpMessageWriterTests extends AbstractLeakCheckingTests {
|
|||
this.writer.write(Mono.just(multipartData), null, MediaType.MULTIPART_FORM_DATA,
|
||||
this.response, hints).block();
|
||||
|
||||
MultiValueMap<String, Part> requestParts = parse(hints);
|
||||
MultiValueMap<String, Part> requestParts = parse(this.response, hints);
|
||||
assertThat(requestParts.size()).isEqualTo(2);
|
||||
|
||||
Part part = requestParts.getFirst("resource");
|
||||
boolean condition1 = part instanceof FilePart;
|
||||
assertThat(condition1).isTrue();
|
||||
assertThat(part instanceof FilePart).isTrue();
|
||||
assertThat(((FilePart) part).filename()).isEqualTo("spring.jpg");
|
||||
assertThat(part.headers().getContentLength()).isEqualTo(logo.getFile().length());
|
||||
|
||||
part = requestParts.getFirst("buffers");
|
||||
boolean condition = part instanceof FilePart;
|
||||
assertThat(condition).isTrue();
|
||||
assertThat(part instanceof FilePart).isTrue();
|
||||
assertThat(((FilePart) part).filename()).isEqualTo("buffers.jpg");
|
||||
assertThat(part.headers().getContentLength()).isEqualTo(logo.getFile().length());
|
||||
}
|
||||
|
||||
private MultiValueMap<String, Part> parse(Map<String, Object> hints) {
|
||||
MediaType contentType = this.response.getHeaders().getContentType();
|
||||
static MultiValueMap<String, Part> parse(MockServerHttpResponse response, Map<String, Object> hints) {
|
||||
MediaType contentType = response.getHeaders().getContentType();
|
||||
assertThat(contentType.getParameter("boundary")).as("No boundary found").isNotNull();
|
||||
|
||||
// see if Synchronoss NIO Multipart can read what we wrote
|
||||
|
|
@ -299,7 +293,7 @@ public class MultipartHttpMessageWriterTests extends AbstractLeakCheckingTests {
|
|||
|
||||
MockServerHttpRequest request = MockServerHttpRequest.post("/")
|
||||
.contentType(MediaType.parseMediaType(contentType.toString()))
|
||||
.body(this.response.getBody());
|
||||
.body(response.getBody());
|
||||
|
||||
ResolvableType elementType = ResolvableType.forClassWithGenerics(
|
||||
MultiValueMap.class, String.class, Part.class);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
* Copyright 2002-2020 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
|
||||
*
|
||||
* https://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.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.codec.StringDecoder;
|
||||
import org.springframework.core.testfixture.io.buffer.AbstractLeakCheckingTests;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpResponse;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.springframework.http.codec.multipart.MultipartHttpMessageWriterTests.parse;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link PartHttpMessageWriter}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.3
|
||||
*/
|
||||
public class PartHttpMessageWriterTests extends AbstractLeakCheckingTests {
|
||||
|
||||
private final PartHttpMessageWriter writer = new PartHttpMessageWriter();
|
||||
|
||||
private final MockServerHttpResponse response = new MockServerHttpResponse(this.bufferFactory);
|
||||
|
||||
|
||||
@Test
|
||||
public void canWrite() {
|
||||
assertThat(this.writer.canWrite(
|
||||
ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, Object.class),
|
||||
MediaType.MULTIPART_FORM_DATA)).isTrue();
|
||||
assertThat(this.writer.canWrite(
|
||||
ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class),
|
||||
MediaType.MULTIPART_FORM_DATA)).isTrue();
|
||||
assertThat(this.writer.canWrite(
|
||||
ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, Object.class),
|
||||
MediaType.MULTIPART_MIXED)).isTrue();
|
||||
assertThat(this.writer.canWrite(
|
||||
ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, Object.class),
|
||||
MediaType.MULTIPART_RELATED)).isTrue();
|
||||
|
||||
assertThat(this.writer.canWrite(
|
||||
ResolvableType.forClassWithGenerics(Map.class, String.class, Object.class),
|
||||
MediaType.MULTIPART_FORM_DATA)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void write() {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.TEXT_PLAIN);
|
||||
Part textPart = mock(Part.class);
|
||||
given(textPart.name()).willReturn("text part");
|
||||
given(textPart.headers()).willReturn(headers);
|
||||
given(textPart.content()).willReturn(Flux.just(
|
||||
this.bufferFactory.wrap("text1".getBytes(StandardCharsets.UTF_8)),
|
||||
this.bufferFactory.wrap("text2".getBytes(StandardCharsets.UTF_8))));
|
||||
|
||||
FilePart filePart = mock(FilePart.class);
|
||||
given(filePart.name()).willReturn("file part");
|
||||
given(filePart.headers()).willReturn(new HttpHeaders());
|
||||
given(filePart.filename()).willReturn("file.txt");
|
||||
given(filePart.content()).willReturn(Flux.just(
|
||||
this.bufferFactory.wrap("Aa".getBytes(StandardCharsets.UTF_8)),
|
||||
this.bufferFactory.wrap("Bb".getBytes(StandardCharsets.UTF_8)),
|
||||
this.bufferFactory.wrap("Cc".getBytes(StandardCharsets.UTF_8))
|
||||
));
|
||||
|
||||
Map<String, Object> hints = Collections.emptyMap();
|
||||
this.writer.write(Flux.just(textPart, filePart), null, MediaType.MULTIPART_FORM_DATA, this.response, hints)
|
||||
.block(Duration.ofSeconds(5));
|
||||
|
||||
MultiValueMap<String, Part> requestParts = parse(this.response, hints);
|
||||
assertThat(requestParts.size()).isEqualTo(2);
|
||||
|
||||
Part part = requestParts.getFirst("text part");
|
||||
assertThat(part.name()).isEqualTo("text part");
|
||||
assertThat(part.headers().getContentType()).isEqualTo(MediaType.TEXT_PLAIN);
|
||||
String value = decodeToString(part);
|
||||
assertThat(value).isEqualTo("text1text2");
|
||||
|
||||
part = requestParts.getFirst("file part");
|
||||
assertThat(part.name()).isEqualTo("file part");
|
||||
assertThat(((FilePart) part).filename()).isEqualTo("file.txt");
|
||||
assertThat(decodeToString(part)).isEqualTo("AaBbCc");
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
private String decodeToString(Part part) {
|
||||
return StringDecoder.textPlainOnly().decodeToMono(part.content(),
|
||||
ResolvableType.forClass(String.class), MediaType.TEXT_PLAIN,
|
||||
Collections.emptyMap()).block(Duration.ZERO);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -57,6 +57,7 @@ import org.springframework.http.codec.json.Jackson2JsonEncoder;
|
|||
import org.springframework.http.codec.json.Jackson2SmileDecoder;
|
||||
import org.springframework.http.codec.json.Jackson2SmileEncoder;
|
||||
import org.springframework.http.codec.multipart.MultipartHttpMessageReader;
|
||||
import org.springframework.http.codec.multipart.PartHttpMessageWriter;
|
||||
import org.springframework.http.codec.multipart.SynchronossPartHttpMessageReader;
|
||||
import org.springframework.http.codec.protobuf.ProtobufDecoder;
|
||||
import org.springframework.http.codec.protobuf.ProtobufHttpMessageWriter;
|
||||
|
|
@ -102,7 +103,7 @@ public class ServerCodecConfigurerTests {
|
|||
@Test
|
||||
public void defaultWriters() {
|
||||
List<HttpMessageWriter<?>> writers = this.configurer.getWriters();
|
||||
assertThat(writers.size()).isEqualTo(12);
|
||||
assertThat(writers.size()).isEqualTo(13);
|
||||
assertThat(getNextEncoder(writers).getClass()).isEqualTo(ByteArrayEncoder.class);
|
||||
assertThat(getNextEncoder(writers).getClass()).isEqualTo(ByteBufferEncoder.class);
|
||||
assertThat(getNextEncoder(writers).getClass()).isEqualTo(DataBufferEncoder.class);
|
||||
|
|
@ -110,6 +111,7 @@ public class ServerCodecConfigurerTests {
|
|||
assertThat(writers.get(index.getAndIncrement()).getClass()).isEqualTo(ResourceHttpMessageWriter.class);
|
||||
assertStringEncoder(getNextEncoder(writers), true);
|
||||
assertThat(writers.get(index.getAndIncrement()).getClass()).isEqualTo(ProtobufHttpMessageWriter.class);
|
||||
assertThat(writers.get(this.index.getAndIncrement()).getClass()).isEqualTo(PartHttpMessageWriter.class);
|
||||
assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2JsonEncoder.class);
|
||||
assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2SmileEncoder.class);
|
||||
assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jaxb2XmlEncoder.class);
|
||||
|
|
|
|||
|
|
@ -207,7 +207,7 @@ public class WebFluxConfigurationSupportTests {
|
|||
assertThat(handler.getOrder()).isEqualTo(0);
|
||||
|
||||
List<HttpMessageWriter<?>> writers = handler.getMessageWriters();
|
||||
assertThat(writers.size()).isEqualTo(12);
|
||||
assertThat(writers.size()).isEqualTo(13);
|
||||
|
||||
assertHasMessageWriter(writers, forClass(byte[].class), APPLICATION_OCTET_STREAM);
|
||||
assertHasMessageWriter(writers, forClass(ByteBuffer.class), APPLICATION_OCTET_STREAM);
|
||||
|
|
@ -235,7 +235,7 @@ public class WebFluxConfigurationSupportTests {
|
|||
assertThat(handler.getOrder()).isEqualTo(100);
|
||||
|
||||
List<HttpMessageWriter<?>> writers = handler.getMessageWriters();
|
||||
assertThat(writers.size()).isEqualTo(12);
|
||||
assertThat(writers.size()).isEqualTo(13);
|
||||
|
||||
assertHasMessageWriter(writers, forClass(byte[].class), APPLICATION_OCTET_STREAM);
|
||||
assertHasMessageWriter(writers, forClass(ByteBuffer.class), APPLICATION_OCTET_STREAM);
|
||||
|
|
|
|||
Loading…
Reference in New Issue