/* * Copyright 2002-2019 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.io.IOException; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.Collections; import java.util.List; import java.util.Map; import org.junit.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.UnicastProcessor; import org.springframework.core.ResolvableType; import org.springframework.core.codec.StringDecoder; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.core.io.buffer.AbstractLeakCheckingTestCase; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.HttpEntity; import org.springframework.http.MediaType; import org.springframework.http.client.MultipartBodyBuilder; import org.springframework.http.codec.ClientCodecConfigurer; import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest; import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse; import org.springframework.util.MultiValueMap; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; /** * @author Sebastien Deleuze * @author Rossen Stoyanchev */ public class MultipartHttpMessageWriterTests extends AbstractLeakCheckingTestCase { private final MultipartHttpMessageWriter writer = new MultipartHttpMessageWriter(ClientCodecConfigurer.create().getWriters()); private final MockServerHttpResponse response = new MockServerHttpResponse(this.bufferFactory); @Test public void canWrite() { assertTrue(this.writer.canWrite( ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, Object.class), MediaType.MULTIPART_FORM_DATA)); assertTrue(this.writer.canWrite( ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class), MediaType.MULTIPART_FORM_DATA)); assertFalse(this.writer.canWrite( ResolvableType.forClassWithGenerics(Map.class, String.class, Object.class), MediaType.MULTIPART_FORM_DATA)); assertTrue(this.writer.canWrite( ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, Object.class), MediaType.APPLICATION_FORM_URLENCODED)); } @Test public void writeMultipart() throws Exception { Resource logo = new ClassPathResource("/org/springframework/http/converter/logo.jpg"); Resource utf8 = new ClassPathResource("/org/springframework/http/converter/logo.jpg") { @Override public String getFilename() { // SPR-12108 return "Hall\u00F6le.jpg"; } }; Flux bufferPublisher = 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)) ); Part mockPart = mock(Part.class); given(mockPart.content()).willReturn(bufferPublisher); MultipartBodyBuilder bodyBuilder = new MultipartBodyBuilder(); bodyBuilder.part("name 1", "value 1"); bodyBuilder.part("name 2", "value 2+1"); bodyBuilder.part("name 2", "value 2+2"); bodyBuilder.part("logo", logo); bodyBuilder.part("utf8", utf8); bodyBuilder.part("json", new Foo("bar"), MediaType.APPLICATION_JSON); bodyBuilder.asyncPart("publisher", Flux.just("foo", "bar", "baz"), String.class); bodyBuilder.asyncPart("partPublisher", Mono.just(mockPart), Part.class); Mono>> result = Mono.just(bodyBuilder.build()); Map hints = Collections.emptyMap(); this.writer.write(result, null, MediaType.MULTIPART_FORM_DATA, this.response, hints).block(Duration.ofSeconds(5)); MultiValueMap requestParts = parse(hints); assertEquals(7, requestParts.size()); Part part = requestParts.getFirst("name 1"); assertTrue(part instanceof FormFieldPart); assertEquals("name 1", part.name()); assertEquals("value 1", ((FormFieldPart) part).value()); List parts2 = requestParts.get("name 2"); assertEquals(2, parts2.size()); part = parts2.get(0); assertTrue(part instanceof FormFieldPart); assertEquals("name 2", part.name()); assertEquals("value 2+1", ((FormFieldPart) part).value()); part = parts2.get(1); assertTrue(part instanceof FormFieldPart); assertEquals("name 2", part.name()); assertEquals("value 2+2", ((FormFieldPart) part).value()); part = requestParts.getFirst("logo"); assertTrue(part instanceof FilePart); assertEquals("logo", part.name()); assertEquals("logo.jpg", ((FilePart) part).filename()); assertEquals(MediaType.IMAGE_JPEG, part.headers().getContentType()); assertEquals(logo.getFile().length(), part.headers().getContentLength()); part = requestParts.getFirst("utf8"); assertTrue(part instanceof FilePart); assertEquals("utf8", part.name()); assertEquals("Hall\u00F6le.jpg", ((FilePart) part).filename()); assertEquals(MediaType.IMAGE_JPEG, part.headers().getContentType()); assertEquals(utf8.getFile().length(), part.headers().getContentLength()); part = requestParts.getFirst("json"); assertEquals("json", part.name()); assertEquals(MediaType.APPLICATION_JSON, part.headers().getContentType()); String value = decodeToString(part); assertEquals("{\"bar\":\"bar\"}", value); part = requestParts.getFirst("publisher"); assertEquals("publisher", part.name()); value = decodeToString(part); assertEquals("foobarbaz", value); part = requestParts.getFirst("partPublisher"); assertEquals("partPublisher", part.name()); value = decodeToString(part); assertEquals("AaBbCc", value); } @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); } @Test // SPR-16402 public void singleSubscriberWithResource() throws IOException { UnicastProcessor processor = UnicastProcessor.create(); Resource logo = new ClassPathResource("/org/springframework/http/converter/logo.jpg"); Mono.just(logo).subscribe(processor); MultipartBodyBuilder bodyBuilder = new MultipartBodyBuilder(); bodyBuilder.asyncPart("logo", processor, Resource.class); Mono>> result = Mono.just(bodyBuilder.build()); Map hints = Collections.emptyMap(); this.writer.write(result, null, MediaType.MULTIPART_FORM_DATA, this.response, hints).block(); MultiValueMap requestParts = parse(hints); assertEquals(1, requestParts.size()); Part part = requestParts.getFirst("logo"); assertEquals("logo", part.name()); assertTrue(part instanceof FilePart); assertEquals("logo.jpg", ((FilePart) part).filename()); assertEquals(MediaType.IMAGE_JPEG, part.headers().getContentType()); assertEquals(logo.getFile().length(), part.headers().getContentLength()); } @Test // SPR-16402 public void singleSubscriberWithStrings() { UnicastProcessor processor = UnicastProcessor.create(); Flux.just("foo", "bar", "baz").subscribe(processor); MultipartBodyBuilder bodyBuilder = new MultipartBodyBuilder(); bodyBuilder.asyncPart("name", processor, String.class); Mono>> result = Mono.just(bodyBuilder.build()); this.writer.write(result, null, MediaType.MULTIPART_FORM_DATA, this.response, Collections.emptyMap()) .block(Duration.ofSeconds(5)); // Make sure body is consumed to avoid leak reports this.response.getBodyAsString().block(Duration.ofSeconds(5)); } @Test // SPR-16376 public void customContentDisposition() throws IOException { Resource logo = new ClassPathResource("/org/springframework/http/converter/logo.jpg"); Flux buffers = DataBufferUtils.read(logo, new DefaultDataBufferFactory(), 1024); long contentLength = logo.contentLength(); MultipartBodyBuilder bodyBuilder = new MultipartBodyBuilder(); bodyBuilder.part("resource", logo) .headers(h -> h.setContentDispositionFormData("resource", "spring.jpg")); bodyBuilder.asyncPart("buffers", buffers, DataBuffer.class) .headers(h -> { h.setContentDispositionFormData("buffers", "buffers.jpg"); h.setContentType(MediaType.IMAGE_JPEG); h.setContentLength(contentLength); }); MultiValueMap> multipartData = bodyBuilder.build(); Map hints = Collections.emptyMap(); this.writer.write(Mono.just(multipartData), null, MediaType.MULTIPART_FORM_DATA, this.response, hints).block(); MultiValueMap requestParts = parse(hints); assertEquals(2, requestParts.size()); Part part = requestParts.getFirst("resource"); assertTrue(part instanceof FilePart); assertEquals("spring.jpg", ((FilePart) part).filename()); assertEquals(logo.getFile().length(), part.headers().getContentLength()); part = requestParts.getFirst("buffers"); assertTrue(part instanceof FilePart); assertEquals("buffers.jpg", ((FilePart) part).filename()); assertEquals(logo.getFile().length(), part.headers().getContentLength()); } private MultiValueMap parse(Map hints) { MediaType contentType = this.response.getHeaders().getContentType(); assertNotNull("No boundary found", contentType.getParameter("boundary")); // see if Synchronoss NIO Multipart can read what we wrote SynchronossPartHttpMessageReader synchronossReader = new SynchronossPartHttpMessageReader(); MultipartHttpMessageReader reader = new MultipartHttpMessageReader(synchronossReader); MockServerHttpRequest request = MockServerHttpRequest.post("/") .contentType(MediaType.parseMediaType(contentType.toString())) .body(this.response.getBody()); ResolvableType elementType = ResolvableType.forClassWithGenerics( MultiValueMap.class, String.class, Part.class); MultiValueMap result = reader.readMono(elementType, request, hints) .block(Duration.ofSeconds(5)); assertNotNull(result); return result; } private class Foo { private String bar; public Foo() { } public Foo(String bar) { this.bar = bar; } public String getBar() { return this.bar; } public void setBar(String bar) { this.bar = bar; } } }