Skip to content

Commit 8964963

Browse files
committed
Add explicit support for multipart/mixed in FormHttpMessageConverter
Commit 5008423 added support for multipart/* media types in FormHttpMessageConverter, but users still had to manually register multipart/mixed as a supported media type in order to POST multipart data with that content type. This commit removes the need to manually register multipart/mixed as a supported media type by registering it automatically in FormHttpMessageConverter. In addition, this commit introduces MULTIPART_MIXED and MULTIPART_MIXED_VALUE constants in MediaType. Closes gh-23209
1 parent 75d1428 commit 8964963

File tree

5 files changed

+65
-45
lines changed

5 files changed

+65
-45
lines changed

spring-web/src/main/java/org/springframework/http/MediaType.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
* @author Rossen Stoyanchev
4545
* @author Sebastien Deleuze
4646
* @author Kazuki Shimizu
47+
* @author Sam Brannen
4748
* @since 3.0
4849
* @see <a href="https://tools.ietf.org/html/rfc7231#section-3.1.1.1">
4950
* HTTP 1.1: Semantics and Content, section 3.1.1.1</a>
@@ -288,6 +289,18 @@ public class MediaType extends MimeType implements Serializable {
288289
*/
289290
public static final String MULTIPART_FORM_DATA_VALUE = "multipart/form-data";
290291

292+
/**
293+
* Public constant media type for {@code multipart/mixed}.
294+
* @since 5.2
295+
*/
296+
public static final MediaType MULTIPART_MIXED;
297+
298+
/**
299+
* A String equivalent of {@link MediaType#MULTIPART_MIXED}.
300+
* @since 5.2
301+
*/
302+
public static final String MULTIPART_MIXED_VALUE = "multipart/mixed";
303+
291304
/**
292305
* Public constant media type for {@code text/event-stream}.
293306
* @since 4.3.6
@@ -367,6 +380,7 @@ public class MediaType extends MimeType implements Serializable {
367380
IMAGE_JPEG = new MediaType("image", "jpeg");
368381
IMAGE_PNG = new MediaType("image", "png");
369382
MULTIPART_FORM_DATA = new MediaType("multipart", "form-data");
383+
MULTIPART_MIXED = new MediaType("multipart", "mixed");
370384
TEXT_EVENT_STREAM = new MediaType("text", "event-stream");
371385
TEXT_HTML = new MediaType("text", "html");
372386
TEXT_MARKDOWN = new MediaType("text", "markdown");

spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@
5252
* <p>In other words, this converter can read and write the
5353
* {@code "application/x-www-form-urlencoded"} media type as
5454
* {@link MultiValueMap MultiValueMap&lt;String, String&gt;}, and it can also
55-
* write (but not read) the {@code "multipart/form-data"} media type as
55+
* write (but not read) the {@code "multipart/form-data"} and
56+
* {@code "multipart/mixed"} media types as
5657
* {@link MultiValueMap MultiValueMap&lt;String, Object&gt;}.
5758
*
5859
* <h3>Multipart Data</h3>
@@ -63,7 +64,9 @@
6364
* {@code "multipart/mixed"} and {@code "multipart/related"}, as long as the
6465
* multipart subtype is registered as a {@linkplain #getSupportedMediaTypes
6566
* supported media type} <em>and</em> the desired multipart subtype is specified
66-
* as the content type when {@linkplain #write writing} the multipart data.
67+
* as the content type when {@linkplain #write writing} the multipart data. Note
68+
* that {@code "multipart/mixed"} is registered as a supported media type by
69+
* default.
6770
*
6871
* <p>When writing multipart data, this converter uses other
6972
* {@link HttpMessageConverter HttpMessageConverters} to write the respective
@@ -85,8 +88,8 @@
8588
* form.add("field 2", "value 2");
8689
* form.add("field 2", "value 3");
8790
* form.add("field 3", 4); // non-String form values supported as of 5.1.4
88-
* restTemplate.postForLocation("https://example.com/myForm", form);
89-
* </pre>
91+
*
92+
* restTemplate.postForLocation("https://example.com/myForm", form);</pre>
9093
*
9194
* <p>The following snippet shows how to do a file upload using the
9295
* {@code "multipart/form-data"} content type.
@@ -95,33 +98,45 @@
9598
* MultiValueMap&lt;String, Object&gt; parts = new LinkedMultiValueMap&lt;&gt;();
9699
* parts.add("field 1", "value 1");
97100
* parts.add("file", new ClassPathResource("myFile.jpg"));
98-
* restTemplate.postForLocation("https://example.com/myFileUpload", parts);
99-
* </pre>
101+
*
102+
* restTemplate.postForLocation("https://example.com/myFileUpload", parts);</pre>
100103
*
101104
* <p>The following snippet shows how to do a file upload using the
102105
* {@code "multipart/mixed"} content type.
103106
*
104107
* <pre class="code">
105-
* MediaType multipartMixed = new MediaType("multipart", "mixed");
108+
* MultiValueMap&lt;String, Object&gt; parts = new LinkedMultiValueMap&lt;&gt;();
109+
* parts.add("field 1", "value 1");
110+
* parts.add("file", new ClassPathResource("myFile.jpg"));
111+
*
112+
* HttpHeaders requestHeaders = new HttpHeaders();
113+
* requestHeaders.setContentType(MediaType.MULTIPART_MIXED);
114+
*
115+
* restTemplate.postForLocation("https://example.com/myFileUpload",
116+
* new HttpEntity&lt;&gt;(parts, requestHeaders));</pre>
117+
*
118+
* <p>The following snippet shows how to do a file upload using the
119+
* {@code "multipart/related"} content type.
120+
*
121+
* <pre class="code">
122+
* MediaType multipartRelated = new MediaType("multipart", "related");
106123
*
107124
* restTemplate.getMessageConverters().stream()
108125
* .filter(FormHttpMessageConverter.class::isInstance)
109126
* .map(FormHttpMessageConverter.class::cast)
110127
* .findFirst()
111128
* .orElseThrow(() -&gt; new IllegalStateException("Failed to find FormHttpMessageConverter"))
112-
* .addSupportedMediaTypes(multipartMixed);
129+
* .addSupportedMediaTypes(multipartRelated);
113130
*
114131
* MultiValueMap&lt;String, Object&gt; parts = new LinkedMultiValueMap&lt;&gt;();
115132
* parts.add("field 1", "value 1");
116133
* parts.add("file", new ClassPathResource("myFile.jpg"));
117134
*
118135
* HttpHeaders requestHeaders = new HttpHeaders();
119-
* requestHeaders.setContentType(multipartMixed);
120-
* HttpEntity&lt;MultiValueMap&lt;String, Object&gt;&gt; requestEntity =
121-
* new HttpEntity&lt;&gt;(parts, requestHeaders);
136+
* requestHeaders.setContentType(multipartRelated);
122137
*
123-
* restTemplate.postForLocation("https://example.com/myFileUpload", requestEntity);
124-
* </pre>
138+
* restTemplate.postForLocation("https://example.com/myFileUpload",
139+
* new HttpEntity&lt;&gt;(parts, requestHeaders));</pre>
125140
*
126141
* <h3>Miscellaneous</h3>
127142
*
@@ -138,13 +153,13 @@
138153
*/
139154
public class FormHttpMessageConverter implements HttpMessageConverter<MultiValueMap<String, ?>> {
140155

141-
private static final MediaType MULTIPART_ALL = new MediaType("multipart", "*");
142-
143156
/**
144157
* The default charset used by the converter.
145158
*/
146159
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
147160

161+
static final MediaType MULTIPART_ALL = new MediaType("multipart", "*");
162+
148163
private static final MediaType DEFAULT_FORM_DATA_MEDIA_TYPE =
149164
new MediaType(MediaType.APPLICATION_FORM_URLENCODED, DEFAULT_CHARSET);
150165

@@ -162,6 +177,7 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
162177
public FormHttpMessageConverter() {
163178
this.supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
164179
this.supportedMediaTypes.add(MediaType.MULTIPART_FORM_DATA);
180+
this.supportedMediaTypes.add(MediaType.MULTIPART_MIXED);
165181

166182
this.partConverters.add(new ByteArrayHttpMessageConverter());
167183
this.partConverters.add(new StringHttpMessageConverter());

spring-web/src/test/java/org/springframework/http/converter/FormHttpMessageConverterTests.java

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,9 @@
4949
import static org.mockito.Mockito.verify;
5050
import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED;
5151
import static org.springframework.http.MediaType.MULTIPART_FORM_DATA;
52+
import static org.springframework.http.MediaType.MULTIPART_MIXED;
5253
import static org.springframework.http.MediaType.TEXT_XML;
54+
import static org.springframework.http.converter.FormHttpMessageConverter.MULTIPART_ALL;
5355

5456
/**
5557
* Unit tests for {@link FormHttpMessageConverter} and
@@ -61,8 +63,6 @@
6163
*/
6264
public class FormHttpMessageConverterTests {
6365

64-
private static final MediaType MULTIPART_ALL = new MediaType("multipart", "*");
65-
private static final MediaType MULTIPART_MIXED = new MediaType("multipart", "mixed");
6666
private static final MediaType MULTIPART_RELATED = new MediaType("multipart", "related");
6767

6868
private final FormHttpMessageConverter converter = new AllEncompassingFormHttpMessageConverter();
@@ -80,51 +80,41 @@ public void canRead() {
8080
@Test
8181
public void cannotReadMultipart() {
8282
// Without custom multipart types supported
83-
assertCannotRead(MULTIPART_ALL);
84-
assertCannotRead(MULTIPART_FORM_DATA);
85-
assertCannotRead(MULTIPART_MIXED);
86-
assertCannotRead(MULTIPART_RELATED);
83+
asssertCannotReadMultipart();
8784

88-
this.converter.addSupportedMediaTypes(MULTIPART_MIXED, MULTIPART_RELATED);
85+
this.converter.addSupportedMediaTypes(MULTIPART_RELATED);
8986

90-
// With custom multipart types supported
91-
assertCannotRead(MULTIPART_ALL);
92-
assertCannotRead(MULTIPART_FORM_DATA);
93-
assertCannotRead(MULTIPART_MIXED);
94-
assertCannotRead(MULTIPART_RELATED);
87+
// Should still be the case with custom multipart types supported
88+
asssertCannotReadMultipart();
9589
}
9690

9791
@Test
9892
public void canWrite() {
9993
assertCanWrite(APPLICATION_FORM_URLENCODED);
10094
assertCanWrite(MULTIPART_FORM_DATA);
95+
assertCanWrite(MULTIPART_MIXED);
10196
assertCanWrite(new MediaType("multipart", "form-data", StandardCharsets.UTF_8));
10297
assertCanWrite(MediaType.ALL);
10398
assertCanWrite(null);
10499
}
105100

106101
@Test
107102
public void setSupportedMediaTypes() {
108-
assertCannotWrite(MULTIPART_MIXED);
109103
assertCannotWrite(MULTIPART_RELATED);
110104

111105
List<MediaType> supportedMediaTypes = new ArrayList<>(this.converter.getSupportedMediaTypes());
112-
supportedMediaTypes.add(MULTIPART_MIXED);
113106
supportedMediaTypes.add(MULTIPART_RELATED);
114107
this.converter.setSupportedMediaTypes(supportedMediaTypes);
115108

116-
assertCanWrite(MULTIPART_MIXED);
117109
assertCanWrite(MULTIPART_RELATED);
118110
}
119111

120112
@Test
121113
public void addSupportedMediaTypes() {
122-
assertCannotWrite(MULTIPART_MIXED);
123114
assertCannotWrite(MULTIPART_RELATED);
124115

125-
this.converter.addSupportedMediaTypes(MULTIPART_MIXED, MULTIPART_RELATED);
116+
this.converter.addSupportedMediaTypes(MULTIPART_RELATED);
126117

127-
assertCanWrite(MULTIPART_MIXED);
128118
assertCanWrite(MULTIPART_RELATED);
129119
}
130120

@@ -286,6 +276,13 @@ private void assertCanRead(Class<?> clazz, MediaType mediaType) {
286276
assertThat(this.converter.canRead(clazz, mediaType)).as(clazz.getSimpleName() + " : " + mediaType).isTrue();
287277
}
288278

279+
private void asssertCannotReadMultipart() {
280+
assertCannotRead(MULTIPART_ALL);
281+
assertCannotRead(MULTIPART_FORM_DATA);
282+
assertCannotRead(MULTIPART_MIXED);
283+
assertCannotRead(MULTIPART_RELATED);
284+
}
285+
289286
private void assertCannotRead(MediaType mediaType) {
290287
assertCannotRead(MultiValueMap.class, mediaType);
291288
}

spring-web/src/test/java/org/springframework/web/client/AbstractMockWebServerTestCase.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,14 @@
3636
import static org.springframework.http.HttpHeaders.CONTENT_TYPE;
3737
import static org.springframework.http.HttpHeaders.LOCATION;
3838
import static org.springframework.http.MediaType.MULTIPART_FORM_DATA;
39+
import static org.springframework.http.MediaType.MULTIPART_MIXED;
3940

4041
/**
4142
* @author Brian Clozel
4243
* @author Sam Brannen
4344
*/
4445
public class AbstractMockWebServerTestCase {
4546

46-
protected static final MediaType MULTIPART_MIXED = new MediaType("multipart", "mixed");
4747
protected static final MediaType MULTIPART_RELATED = new MediaType("multipart", "related");
4848

4949
protected static final MediaType textContentType =

spring-web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
6464
import static org.junit.Assume.assumeFalse;
6565
import static org.springframework.http.HttpMethod.POST;
66+
import static org.springframework.http.MediaType.MULTIPART_MIXED;
6667

6768
/**
6869
* Integration tests for {@link RestTemplate}.
@@ -274,15 +275,10 @@ public void multipartFormData() {
274275
}
275276

276277
@Test
277-
public void multipartMixed() {
278-
addSupportedMediaTypeToFormHttpMessageConverter(MULTIPART_MIXED);
279-
278+
public void multipartMixed() throws Exception {
280279
HttpHeaders requestHeaders = new HttpHeaders();
281280
requestHeaders.setContentType(MULTIPART_MIXED);
282-
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(createMultipartParts(),
283-
requestHeaders);
284-
285-
template.postForLocation(baseUrl + "/multipartMixed", requestEntity);
281+
template.postForLocation(baseUrl + "/multipartMixed", new HttpEntity<>(createMultipartParts(), requestHeaders));
286282
}
287283

288284
@Test
@@ -291,10 +287,7 @@ public void multipartRelated() {
291287

292288
HttpHeaders requestHeaders = new HttpHeaders();
293289
requestHeaders.setContentType(MULTIPART_RELATED);
294-
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(createMultipartParts(),
295-
requestHeaders);
296-
297-
template.postForLocation(baseUrl + "/multipartRelated", requestEntity);
290+
template.postForLocation(baseUrl + "/multipartRelated", new HttpEntity<>(createMultipartParts(), requestHeaders));
298291
}
299292

300293
private MultiValueMap<String, Object> createMultipartParts() {

0 commit comments

Comments
 (0)