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

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

View File

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

View File

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