Browse Source

Add PartHttpMessageWriter

Closes gh-24535
pull/25040/head
Rossen Stoyanchev 5 years ago
parent
commit
1da903dd59
  1. 147
      spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartHttpMessageWriter.java
  2. 168
      spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartWriterSupport.java
  3. 90
      spring-web/src/main/java/org/springframework/http/codec/multipart/PartHttpMessageWriter.java
  4. 8
      spring-web/src/main/java/org/springframework/http/codec/support/ServerDefaultCodecsImpl.java
  5. 40
      spring-web/src/test/java/org/springframework/http/codec/multipart/MultipartHttpMessageWriterTests.java
  6. 119
      spring-web/src/test/java/org/springframework/http/codec/multipart/PartHttpMessageWriterTests.java
  7. 4
      spring-web/src/test/java/org/springframework/http/codec/support/ServerCodecConfigurerTests.java
  8. 4
      spring-webflux/src/test/java/org/springframework/web/reactive/config/WebFluxConfigurationSupportTests.java

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

@ -17,11 +17,9 @@ @@ -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; @@ -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; @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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));
}
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;
});
generateNewLine(factory));
}
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 @@ -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 @@ -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());

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

@ -0,0 +1,168 @@ @@ -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;
});
}
}

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

@ -0,0 +1,90 @@ @@ -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));
}
}

8
spring-web/src/main/java/org/springframework/http/codec/support/ServerDefaultCodecsImpl.java

@ -1,5 +1,5 @@ @@ -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; @@ -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 @@ -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()));

40
spring-web/src/test/java/org/springframework/http/codec/multipart/MultipartHttpMessageWriterTests.java

@ -49,6 +49,8 @@ import static org.mockito.BDDMockito.given; @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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);

119
spring-web/src/test/java/org/springframework/http/codec/multipart/PartHttpMessageWriterTests.java

@ -0,0 +1,119 @@ @@ -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);
}
}

4
spring-web/src/test/java/org/springframework/http/codec/support/ServerCodecConfigurerTests.java

@ -57,6 +57,7 @@ import org.springframework.http.codec.json.Jackson2JsonEncoder; @@ -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 { @@ -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 { @@ -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);

4
spring-webflux/src/test/java/org/springframework/web/reactive/config/WebFluxConfigurationSupportTests.java

@ -207,7 +207,7 @@ public class WebFluxConfigurationSupportTests { @@ -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 { @@ -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…
Cancel
Save