Add PartHttpMessageWriter

Closes gh-24535
This commit is contained in:
Rossen Stoyanchev
2020-05-07 16:28:33 +01:00
parent 5af3223e76
commit 1da903dd59
8 changed files with 425 additions and 155 deletions

View File

@@ -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());

View File

@@ -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;
});
}
}

View File

@@ -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));
}
}

View File

@@ -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()));