55
55
* write (but not read) the {@code "multipart/form-data"} media type as
56
56
* {@link MultiValueMap MultiValueMap<String, Object>}.
57
57
*
58
+ * <h3>Multipart Data</h3>
59
+ *
60
+ * <p>By default, {@code "multipart/form-data"} is used as the content type when
61
+ * {@linkplain #write writing} multipart data. As of Spring Framework 5.2 it is
62
+ * also possible to write multipart data using other multipart subtypes such as
63
+ * {@code "multipart/mixed"} and {@code "multipart/related"}, as long as the
64
+ * multipart subtype is registered as a {@linkplain #getSupportedMediaTypes
65
+ * 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
+ *
58
68
* <p>When writing multipart data, this converter uses other
59
69
* {@link HttpMessageConverter HttpMessageConverters} to write the respective
60
- * MIME parts. By default, basic converters are registered (e.g., for {@code String}
61
- * and {@code Resource}). These can be overridden through the
62
- * {@link #setPartConverters partConverters} property.
70
+ * MIME parts. By default, basic converters are registered for byte array,
71
+ * {@code String}, and {@code Resource}. These can be overridden via
72
+ * {@link #setPartConverters} or augmented via {@link #addPartConverter}.
73
+ *
74
+ * <h3>Examples</h3>
75
+ *
76
+ * <p>The following snippet shows how to submit an HTML form using the
77
+ * {@code "multipart/form-data"} content type.
63
78
*
64
- * <p>For example, the following snippet shows how to submit an HTML form:
65
79
* <pre class="code">
66
- * RestTemplate template = new RestTemplate();
80
+ * RestTemplate restTemplate = new RestTemplate();
67
81
* // AllEncompassingFormHttpMessageConverter is configured by default
68
82
*
69
83
* MultiValueMap<String, Object> form = new LinkedMultiValueMap<>();
70
84
* form.add("field 1", "value 1");
71
85
* form.add("field 2", "value 2");
72
86
* form.add("field 2", "value 3");
73
87
* form.add("field 3", 4); // non-String form values supported as of 5.1.4
74
- * template .postForLocation("https://example.com/myForm", form);
88
+ * restTemplate .postForLocation("https://example.com/myForm", form);
75
89
* </pre>
76
90
*
77
- * <p>The following snippet shows how to do a file upload:
91
+ * <p>The following snippet shows how to do a file upload using the
92
+ * {@code "multipart/form-data"} content type.
93
+ *
78
94
* <pre class="code">
79
95
* MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
80
96
* parts.add("field 1", "value 1");
81
97
* parts.add("file", new ClassPathResource("myFile.jpg"));
82
- * template .postForLocation("https://example.com/myFileUpload", parts);
98
+ * restTemplate .postForLocation("https://example.com/myFileUpload", parts);
83
99
* </pre>
84
100
*
101
+ * <p>The following snippet shows how to do a file upload using the
102
+ * {@code "multipart/mixed"} content type.
103
+ *
104
+ * <pre class="code">
105
+ * MediaType multipartMixed = new MediaType("multipart", "mixed");
106
+ *
107
+ * restTemplate.getMessageConverters().stream()
108
+ * .filter(FormHttpMessageConverter.class::isInstance)
109
+ * .map(FormHttpMessageConverter.class::cast)
110
+ * .findFirst()
111
+ * .orElseThrow(() -> new IllegalStateException("Failed to find FormHttpMessageConverter"))
112
+ * .addSupportedMediaTypes(multipartMixed);
113
+ *
114
+ * MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
115
+ * parts.add("field 1", "value 1");
116
+ * parts.add("file", new ClassPathResource("myFile.jpg"));
117
+ *
118
+ * HttpHeaders requestHeaders = new HttpHeaders();
119
+ * requestHeaders.setContentType(multipartMixed);
120
+ * HttpEntity<MultiValueMap<String, Object>> requestEntity =
121
+ * new HttpEntity<>(parts, requestHeaders);
122
+ *
123
+ * restTemplate.postForLocation("https://example.com/myFileUpload", requestEntity);
124
+ * </pre>
125
+ *
126
+ * <h3>Miscellaneous</h3>
127
+ *
85
128
* <p>Some methods in this class were inspired by
86
129
* {@code org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity}.
87
130
*
95
138
*/
96
139
public class FormHttpMessageConverter implements HttpMessageConverter <MultiValueMap <String , ?>> {
97
140
141
+ private static final MediaType MULTIPART_ALL = new MediaType ("multipart" , "*" );
142
+
98
143
/**
99
144
* The default charset used by the converter.
100
145
*/
@@ -154,6 +199,12 @@ public void addSupportedMediaTypes(MediaType... supportedMediaTypes) {
154
199
}
155
200
}
156
201
202
+ /**
203
+ * {@inheritDoc}
204
+ *
205
+ * @see #setSupportedMediaTypes(List)
206
+ * @see #addSupportedMediaTypes(MediaType...)
207
+ */
157
208
@ Override
158
209
public List <MediaType > getSupportedMediaTypes () {
159
210
return Collections .unmodifiableList (this .supportedMediaTypes );
@@ -236,8 +287,11 @@ public boolean canRead(Class<?> clazz, @Nullable MediaType mediaType) {
236
287
return true ;
237
288
}
238
289
for (MediaType supportedMediaType : getSupportedMediaTypes ()) {
239
- // We can't read multipart....
240
- if (!supportedMediaType .equals (MediaType .MULTIPART_FORM_DATA ) && supportedMediaType .includes (mediaType )) {
290
+ if (MULTIPART_ALL .includes (supportedMediaType )) {
291
+ // We can't read multipart, so skip this supported media type.
292
+ continue ;
293
+ }
294
+ if (supportedMediaType .includes (mediaType )) {
241
295
return true ;
242
296
}
243
297
}
@@ -291,7 +345,7 @@ public void write(MultiValueMap<String, ?> map, @Nullable MediaType contentType,
291
345
throws IOException , HttpMessageNotWritableException {
292
346
293
347
if (isMultipart (map , contentType )) {
294
- writeMultipart ((MultiValueMap <String , Object >) map , outputMessage );
348
+ writeMultipart ((MultiValueMap <String , Object >) map , contentType , outputMessage );
295
349
}
296
350
else {
297
351
writeForm ((MultiValueMap <String , Object >) map , contentType , outputMessage );
@@ -301,7 +355,7 @@ public void write(MultiValueMap<String, ?> map, @Nullable MediaType contentType,
301
355
302
356
private boolean isMultipart (MultiValueMap <String , ?> map , @ Nullable MediaType contentType ) {
303
357
if (contentType != null ) {
304
- return MediaType . MULTIPART_FORM_DATA .includes (contentType );
358
+ return MULTIPART_ALL .includes (contentType );
305
359
}
306
360
for (List <?> values : map .values ()) {
307
361
for (Object value : values ) {
@@ -368,19 +422,26 @@ protected String serializeForm(MultiValueMap<String, Object> formData, Charset c
368
422
return builder .toString ();
369
423
}
370
424
371
- private void writeMultipart (final MultiValueMap <String , Object > parts , HttpOutputMessage outputMessage )
425
+ private void writeMultipart (final MultiValueMap <String , Object > parts , MediaType contentType , HttpOutputMessage outputMessage )
372
426
throws IOException {
373
427
428
+ // If the supplied content type is null, fall back to multipart/form-data.
429
+ // Otherwise rely on the fact that isMultipart() already verified the
430
+ // supplied content type is multipart/*.
431
+ if (contentType == null ) {
432
+ contentType = MediaType .MULTIPART_FORM_DATA ;
433
+ }
434
+
374
435
final byte [] boundary = generateMultipartBoundary ();
375
436
Map <String , String > parameters = new LinkedHashMap <>(2 );
376
437
if (!isFilenameCharsetSet ()) {
377
438
parameters .put ("charset" , this .charset .name ());
378
439
}
379
440
parameters .put ("boundary" , new String (boundary , StandardCharsets .US_ASCII ));
380
441
381
- MediaType contentType = new MediaType ( MediaType . MULTIPART_FORM_DATA , parameters );
382
- HttpHeaders headers = outputMessage . getHeaders ( );
383
- headers .setContentType (contentType );
442
+ // Add parameters to output content type
443
+ contentType = new MediaType ( contentType , parameters );
444
+ outputMessage . getHeaders () .setContentType (contentType );
384
445
385
446
if (outputMessage instanceof StreamingHttpOutputMessage ) {
386
447
StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage ) outputMessage ;
0 commit comments