diff --git a/spring-web/src/main/java/org/springframework/http/client/MultipartBodyBuilder.java b/spring-web/src/main/java/org/springframework/http/client/MultipartBodyBuilder.java new file mode 100644 index 0000000000..7b46725b78 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/client/MultipartBodyBuilder.java @@ -0,0 +1,162 @@ +/* + * Copyright 2002-2017 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 + * + * http://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.client; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +/** + * A mutable builder for multipart form bodies. For example: + *
+ *
+ * MultipartBodyBuilder builder = new MultipartBodyBuilder();
+ * MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
+ * form.add("form field", "form value");
+ * builder.part("form", form).header("Foo", "Bar");
+ *
+ * Resource image = new ClassPathResource("image.jpg");
+ * builder.part("image", image).header("Baz", "Qux");
+ *
+ * MultiValueMap> multipartBody = builder.build();
+ * // use multipartBody with RestTemplate or WebClient
+ * 
+ + * @author Arjen Poutsma + * @since 5.0.2 + * @see RFC 7578 + */ +public final class MultipartBodyBuilder { + + private final LinkedMultiValueMap parts = new LinkedMultiValueMap<>(); + + + /** + * Creates a new, empty instance of the {@code MultipartBodyBuilder}. + */ + public MultipartBodyBuilder() { + } + + + /** + * Builds the multipart body. + * @return the built body + */ + public MultiValueMap> build() { + MultiValueMap> result = new LinkedMultiValueMap<>(this.parts.size()); + for (Map.Entry> entry : this.parts.entrySet()) { + for (DefaultPartBuilder builder : entry.getValue()) { + HttpEntity entity = builder.build(); + result.add(entry.getKey(), entity); + } + } + return result; + } + + /** + * Adds a part to this builder, allowing for further header customization with the returned + * {@link PartBuilder}. + * @param name the name of the part to add (may not be empty) + * @param part the part to add + * @return a builder that allows for further header customization + */ + public PartBuilder part(String name, Object part) { + return part(name, part, null); + } + + /** + * Adds a part to this builder, allowing for further header customization with the returned + * {@link PartBuilder}. + * @param name the name of the part to add (may not be empty) + * @param part the part to add + * @param contentType the {@code Content-Type} header for the part (may be {@code null}) + * @return a builder that allows for further header customization + */ + public PartBuilder part(String name, Object part, @Nullable MediaType contentType) { + Assert.hasLength(name, "'name' must not be empty"); + Assert.notNull(part, "'part' must not be null"); + + Object partBody; + HttpHeaders partHeaders = new HttpHeaders(); + + if (part instanceof HttpEntity) { + HttpEntity other = (HttpEntity) part; + partBody = other.getBody(); + partHeaders.addAll(other.getHeaders()); + } + else { + partBody = part; + } + + if (contentType != null) { + partHeaders.setContentType(contentType); + } + DefaultPartBuilder builder = new DefaultPartBuilder(partBody, partHeaders); + this.parts.add(name, builder); + return builder; + } + + + /** + * Builder interface that allows for customization of part headers. + */ + public interface PartBuilder { + + /** + * Add the given part-specific header values under the given name. + * @param headerName the part header name + * @param headerValues the part header value(s) + * @return this builder + * @see HttpHeaders#add(String, String) + */ + PartBuilder header(String headerName, String... headerValues); + } + + + private static class DefaultPartBuilder implements PartBuilder { + + @Nullable + private final Object body; + + private final HttpHeaders headers; + + + public DefaultPartBuilder(@Nullable Object body, HttpHeaders headers) { + this.body = body; + this.headers = headers; + } + + @Override + public PartBuilder header(String headerName, String... headerValues) { + this.headers.addAll(headerName, Arrays.asList(headerValues)); + return this; + } + + public HttpEntity build() { + return new HttpEntity<>(this.body, this.headers); + } + } + +} diff --git a/spring-web/src/test/java/org/springframework/http/client/MultipartBodyBuilderTests.java b/spring-web/src/test/java/org/springframework/http/client/MultipartBodyBuilderTests.java new file mode 100644 index 0000000000..9293d02905 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/client/MultipartBodyBuilderTests.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2017 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 + * + * http://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.client; + +import org.junit.Test; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import static org.junit.Assert.*; + +/** + * @author Arjen Poutsma + */ +public class MultipartBodyBuilderTests { + + @Test + public void builder() throws Exception { + MultiValueMap form = new LinkedMultiValueMap<>(); + form.add("form field", "form value"); + Resource logo = new ClassPathResource("/org/springframework/http/converter/logo.jpg"); + HttpHeaders entityHeaders = new HttpHeaders(); + entityHeaders.add("foo", "bar"); + HttpEntity entity = new HttpEntity<>("body", entityHeaders); + + MultipartBodyBuilder builder = new MultipartBodyBuilder(); + builder.part("key", form).header("foo", "bar"); + builder.part("logo", logo).header("baz", "qux"); + builder.part("entity", entity).header("baz", "qux"); + + MultiValueMap> result = builder.build(); + + assertEquals(3, result.size()); + assertNotNull(result.getFirst("key")); + assertEquals(form, result.getFirst("key").getBody()); + assertEquals("bar", result.getFirst("key").getHeaders().getFirst("foo")); + + assertNotNull(result.getFirst("logo")); + assertEquals(logo, result.getFirst("logo").getBody()); + assertEquals("qux", result.getFirst("logo").getHeaders().getFirst("baz")); + + assertNotNull(result.getFirst("entity")); + assertEquals("body", result.getFirst("entity").getBody()); + assertEquals("bar", result.getFirst("entity").getHeaders().getFirst("foo")); + assertEquals("qux", result.getFirst("entity").getHeaders().getFirst("baz")); + } + + +} \ No newline at end of file