Skip to content

Commit 2d1f875

Browse files
committed
Add MultipartBodyBuilder
This commit introduces the MultipartBodyBuilder, a builder for multipart form bodies. Issue: SPR-16134
1 parent 9b7af8b commit 2d1f875

File tree

2 files changed

+229
-0
lines changed

2 files changed

+229
-0
lines changed
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/*
2+
* Copyright 2002-2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.http.client;
18+
19+
import java.util.Arrays;
20+
import java.util.List;
21+
import java.util.Map;
22+
23+
import org.springframework.http.HttpEntity;
24+
import org.springframework.http.HttpHeaders;
25+
import org.springframework.http.MediaType;
26+
import org.springframework.lang.Nullable;
27+
import org.springframework.util.Assert;
28+
import org.springframework.util.LinkedMultiValueMap;
29+
import org.springframework.util.MultiValueMap;
30+
31+
/**
32+
* A mutable builder for multipart form bodies. For example:
33+
* <pre class="code">
34+
*
35+
* MultipartBodyBuilder builder = new MultipartBodyBuilder();
36+
* MultiValueMap&lt;String, String&gt; form = new LinkedMultiValueMap&lt;&gt;();
37+
* form.add("form field", "form value");
38+
* builder.part("form", form).header("Foo", "Bar");
39+
*
40+
* Resource image = new ClassPathResource("image.jpg");
41+
* builder.part("image", image).header("Baz", "Qux");
42+
*
43+
* MultiValueMap<String, HttpEntity<?>> multipartBody = builder.build();
44+
* // use multipartBody with RestTemplate or WebClient
45+
* </pre>
46+
47+
* @author Arjen Poutsma
48+
* @since 5.0.2
49+
* @see <a href="https://tools.ietf.org/html/rfc7578">RFC 7578</a>
50+
*/
51+
public final class MultipartBodyBuilder {
52+
53+
private final LinkedMultiValueMap<String, DefaultPartBuilder> parts = new LinkedMultiValueMap<>();
54+
55+
56+
/**
57+
* Creates a new, empty instance of the {@code MultipartBodyBuilder}.
58+
*/
59+
public MultipartBodyBuilder() {
60+
}
61+
62+
63+
/**
64+
* Builds the multipart body.
65+
* @return the built body
66+
*/
67+
public MultiValueMap<String, HttpEntity<?>> build() {
68+
MultiValueMap<String, HttpEntity<?>> result = new LinkedMultiValueMap<>(this.parts.size());
69+
for (Map.Entry<String, List<DefaultPartBuilder>> entry : this.parts.entrySet()) {
70+
for (DefaultPartBuilder builder : entry.getValue()) {
71+
HttpEntity<?> entity = builder.build();
72+
result.add(entry.getKey(), entity);
73+
}
74+
}
75+
return result;
76+
}
77+
78+
/**
79+
* Adds a part to this builder, allowing for further header customization with the returned
80+
* {@link PartBuilder}.
81+
* @param name the name of the part to add (may not be empty)
82+
* @param part the part to add
83+
* @return a builder that allows for further header customization
84+
*/
85+
public PartBuilder part(String name, Object part) {
86+
return part(name, part, null);
87+
}
88+
89+
/**
90+
* Adds a part to this builder, allowing for further header customization with the returned
91+
* {@link PartBuilder}.
92+
* @param name the name of the part to add (may not be empty)
93+
* @param part the part to add
94+
* @param contentType the {@code Content-Type} header for the part (may be {@code null})
95+
* @return a builder that allows for further header customization
96+
*/
97+
public PartBuilder part(String name, Object part, @Nullable MediaType contentType) {
98+
Assert.hasLength(name, "'name' must not be empty");
99+
Assert.notNull(part, "'part' must not be null");
100+
101+
Object partBody;
102+
HttpHeaders partHeaders = new HttpHeaders();
103+
104+
if (part instanceof HttpEntity) {
105+
HttpEntity<?> other = (HttpEntity<?>) part;
106+
partBody = other.getBody();
107+
partHeaders.addAll(other.getHeaders());
108+
}
109+
else {
110+
partBody = part;
111+
}
112+
113+
if (contentType != null) {
114+
partHeaders.setContentType(contentType);
115+
}
116+
DefaultPartBuilder builder = new DefaultPartBuilder(partBody, partHeaders);
117+
this.parts.add(name, builder);
118+
return builder;
119+
}
120+
121+
122+
/**
123+
* Builder interface that allows for customization of part headers.
124+
*/
125+
public interface PartBuilder {
126+
127+
/**
128+
* Add the given part-specific header values under the given name.
129+
* @param headerName the part header name
130+
* @param headerValues the part header value(s)
131+
* @return this builder
132+
* @see HttpHeaders#add(String, String)
133+
*/
134+
PartBuilder header(String headerName, String... headerValues);
135+
}
136+
137+
138+
private static class DefaultPartBuilder implements PartBuilder {
139+
140+
@Nullable
141+
private final Object body;
142+
143+
private final HttpHeaders headers;
144+
145+
146+
public DefaultPartBuilder(@Nullable Object body, HttpHeaders headers) {
147+
this.body = body;
148+
this.headers = headers;
149+
}
150+
151+
@Override
152+
public PartBuilder header(String headerName, String... headerValues) {
153+
this.headers.addAll(headerName, Arrays.asList(headerValues));
154+
return this;
155+
}
156+
157+
public HttpEntity<?> build() {
158+
return new HttpEntity<>(this.body, this.headers);
159+
}
160+
}
161+
162+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Copyright 2002-2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.http.client;
18+
19+
import org.junit.Test;
20+
21+
import org.springframework.core.io.ClassPathResource;
22+
import org.springframework.core.io.Resource;
23+
import org.springframework.http.HttpEntity;
24+
import org.springframework.http.HttpHeaders;
25+
import org.springframework.util.LinkedMultiValueMap;
26+
import org.springframework.util.MultiValueMap;
27+
28+
import static org.junit.Assert.*;
29+
30+
/**
31+
* @author Arjen Poutsma
32+
*/
33+
public class MultipartBodyBuilderTests {
34+
35+
@Test
36+
public void builder() throws Exception {
37+
MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
38+
form.add("form field", "form value");
39+
Resource logo = new ClassPathResource("/org/springframework/http/converter/logo.jpg");
40+
HttpHeaders entityHeaders = new HttpHeaders();
41+
entityHeaders.add("foo", "bar");
42+
HttpEntity<String> entity = new HttpEntity<>("body", entityHeaders);
43+
44+
MultipartBodyBuilder builder = new MultipartBodyBuilder();
45+
builder.part("key", form).header("foo", "bar");
46+
builder.part("logo", logo).header("baz", "qux");
47+
builder.part("entity", entity).header("baz", "qux");
48+
49+
MultiValueMap<String, HttpEntity<?>> result = builder.build();
50+
51+
assertEquals(3, result.size());
52+
assertNotNull(result.getFirst("key"));
53+
assertEquals(form, result.getFirst("key").getBody());
54+
assertEquals("bar", result.getFirst("key").getHeaders().getFirst("foo"));
55+
56+
assertNotNull(result.getFirst("logo"));
57+
assertEquals(logo, result.getFirst("logo").getBody());
58+
assertEquals("qux", result.getFirst("logo").getHeaders().getFirst("baz"));
59+
60+
assertNotNull(result.getFirst("entity"));
61+
assertEquals("body", result.getFirst("entity").getBody());
62+
assertEquals("bar", result.getFirst("entity").getHeaders().getFirst("foo"));
63+
assertEquals("qux", result.getFirst("entity").getHeaders().getFirst("baz"));
64+
}
65+
66+
67+
}

0 commit comments

Comments
 (0)