28
28
import java .util .LinkedHashMap ;
29
29
import java .util .List ;
30
30
import java .util .Map ;
31
+ import java .util .function .BiConsumer ;
31
32
32
33
import org .springframework .core .io .Resource ;
33
34
import org .springframework .http .ContentDisposition ;
52
53
*
53
54
* <p>In other words, this converter can read and write the
54
55
* {@code "application/x-www-form-urlencoded"} media type as
56
+ * {@code Map<String, String>} or as
55
57
* {@link MultiValueMap MultiValueMap<String, String>}, and it can also
56
58
* write (but not read) the {@code "multipart/form-data"} and
57
- * {@code "multipart/mixed"} media types as
59
+ * {@code "multipart/mixed"} media types as {@code Map<String, Object>} or as
58
60
* {@link MultiValueMap MultiValueMap<String, Object>}.
59
61
*
60
62
* <h3>Multipart Data</h3>
81
83
* {@code "multipart/form-data"} content type.
82
84
*
83
85
* <pre class="code">
84
- * RestTemplate restTemplate = new RestTemplate ();
86
+ * RestClient restClient = RestClient.create ();
85
87
* // AllEncompassingFormHttpMessageConverter is configured by default
86
88
*
87
89
* MultiValueMap<String, Object> form = new LinkedMultiValueMap<>();
90
92
* form.add("field 2", "value 3");
91
93
* form.add("field 3", 4); // non-String form values supported as of 5.1.4
92
94
*
93
- * restTemplate.postForLocation("https://example.com/myForm", form);</pre>
95
+ * ResponseEntity<Void> response = restClient.post()
96
+ * .uri("https://example.com/myForm")
97
+ * .contentType(MULTIPART_FORM_DATA)
98
+ * .body(form)
99
+ * .retrieve()
100
+ * .toBodilessEntity();</pre>
94
101
*
95
102
* <p>The following snippet shows how to do a file upload using the
96
103
* {@code "multipart/form-data"} content type.
100
107
* parts.add("field 1", "value 1");
101
108
* parts.add("file", new ClassPathResource("myFile.jpg"));
102
109
*
103
- * restTemplate.postForLocation("https://example.com/myFileUpload", parts);</pre>
110
+ * ResponseEntity<Void> response = restClient.post()
111
+ * .uri("https://example.com/myForm")
112
+ * .contentType(MULTIPART_FORM_DATA)
113
+ * .body(parts)
114
+ * .retrieve()
115
+ * .toBodilessEntity();</pre>
104
116
*
105
117
* <p>The following snippet shows how to do a file upload using the
106
118
* {@code "multipart/mixed"} content type.
110
122
* parts.add("field 1", "value 1");
111
123
* parts.add("file", new ClassPathResource("myFile.jpg"));
112
124
*
113
- * HttpHeaders requestHeaders = new HttpHeaders();
114
- * requestHeaders.setContentType(MediaType.MULTIPART_MIXED);
115
- *
116
- * restTemplate.postForLocation("https://example.com/myFileUpload",
117
- * new HttpEntity<>(parts, requestHeaders));</pre>
125
+ * ResponseEntity<Void> response = restClient.post()
126
+ * .uri("https://example.com/myForm")
127
+ * .contentType(MULTIPART_MIXED)
128
+ * .body(form)
129
+ * .retrieve()
130
+ * .toBodilessEntity();</pre>
118
131
*
119
132
* <p>The following snippet shows how to do a file upload using the
120
133
* {@code "multipart/related"} content type.
121
134
*
122
135
* <pre class="code">
123
- * MediaType multipartRelated = new MediaType("multipart", "related");
124
- *
125
- * restTemplate.getMessageConverters().stream()
126
- * .filter(FormHttpMessageConverter.class::isInstance)
136
+ * restClient = restClient.mutate()
137
+ * .messageConverters(l -> l.stream()
138
+ * .filter(FormHttpMessageConverter.class::isInstance)
127
139
* .map(FormHttpMessageConverter.class::cast)
128
140
* .findFirst()
129
141
* .orElseThrow(() -> new IllegalStateException("Failed to find FormHttpMessageConverter"))
130
- * .addSupportedMediaTypes(multipartRelated );
142
+ * .addSupportedMediaTypes(MULTIPART_RELATED );
131
143
*
132
144
* MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
133
145
* parts.add("field 1", "value 1");
134
146
* parts.add("file", new ClassPathResource("myFile.jpg"));
135
147
*
136
- * HttpHeaders requestHeaders = new HttpHeaders();
137
- * requestHeaders.setContentType(multipartRelated);
138
- *
139
- * restTemplate.postForLocation("https://example.com/myFileUpload",
140
- * new HttpEntity<>(parts, requestHeaders));</pre>
148
+ * ResponseEntity<Void> response = restClient.post()
149
+ * .uri("https://example.com/myForm")
150
+ * .contentType(MULTIPART_RELATED)
151
+ * .body(form)
152
+ * .retrieve()
153
+ * .toBodilessEntity();</pre>
141
154
*
142
155
* <h3>Miscellaneous</h3>
143
156
*
144
157
* <p>Some methods in this class were inspired by
145
158
* {@code org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity}.
146
159
*
160
+ * <p>As of 6.2, the {@code FormHttpMessageConverter} is parameterized over
161
+ * {@code Map<String, ?>}, whereas before it was {@code MultiValueMap<String, ?>},
162
+ * in order to support single-value maps.
163
+ *
147
164
* @author Arjen Poutsma
148
165
* @author Rossen Stoyanchev
149
166
* @author Juergen Hoeller
152
169
* @see org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter
153
170
* @see org.springframework.util.MultiValueMap
154
171
*/
155
- public class FormHttpMessageConverter implements HttpMessageConverter <MultiValueMap <String , ?>> {
172
+ public class FormHttpMessageConverter implements HttpMessageConverter <Map <String , ?>> {
156
173
157
174
/** The default charset used by the converter. */
158
175
public static final Charset DEFAULT_CHARSET = StandardCharsets .UTF_8 ;
@@ -295,7 +312,7 @@ public void setMultipartCharset(Charset charset) {
295
312
296
313
@ Override
297
314
public boolean canRead (Class <?> clazz , @ Nullable MediaType mediaType ) {
298
- if (!MultiValueMap .class .isAssignableFrom (clazz )) {
315
+ if (!Map .class .isAssignableFrom (clazz )) {
299
316
return false ;
300
317
}
301
318
if (mediaType == null ) {
@@ -315,7 +332,7 @@ public boolean canRead(Class<?> clazz, @Nullable MediaType mediaType) {
315
332
316
333
@ Override
317
334
public boolean canWrite (Class <?> clazz , @ Nullable MediaType mediaType ) {
318
- if (!MultiValueMap .class .isAssignableFrom (clazz )) {
335
+ if (!Map .class .isAssignableFrom (clazz )) {
319
336
return false ;
320
337
}
321
338
if (mediaType == null || MediaType .ALL .equals (mediaType )) {
@@ -330,59 +347,75 @@ public boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) {
330
347
}
331
348
332
349
@ Override
333
- public MultiValueMap <String , String > read (@ Nullable Class <? extends MultiValueMap <String , ?>> clazz ,
350
+ public Map <String , ? > read (@ Nullable Class <? extends Map <String , ?>> clazz ,
334
351
HttpInputMessage inputMessage ) throws IOException , HttpMessageNotReadableException {
335
352
336
353
MediaType contentType = inputMessage .getHeaders ().getContentType ();
337
354
Charset charset = (contentType != null && contentType .getCharset () != null ?
338
355
contentType .getCharset () : this .charset );
339
356
String body = StreamUtils .copyToString (inputMessage .getBody (), charset );
340
-
341
357
String [] pairs = StringUtils .tokenizeToStringArray (body , "&" );
342
- MultiValueMap <String , String > result = new LinkedMultiValueMap <>(pairs .length );
358
+
359
+ if (clazz == null || MultiValueMap .class .isAssignableFrom (clazz )) {
360
+ MultiValueMap <String , String > result = new LinkedMultiValueMap <>(pairs .length );
361
+ readToMap (pairs , charset , result ::add );
362
+ return result ;
363
+ }
364
+ else {
365
+ Map <String , String > result = CollectionUtils .newLinkedHashMap (pairs .length );
366
+ readToMap (pairs , charset , result ::putIfAbsent );
367
+ return result ;
368
+ }
369
+ }
370
+
371
+ private static void readToMap (String [] pairs , Charset charset , BiConsumer <String , String > addFunction ) {
343
372
for (String pair : pairs ) {
344
373
int idx = pair .indexOf ('=' );
345
374
if (idx == -1 ) {
346
- result . add (URLDecoder .decode (pair , charset ), null );
375
+ addFunction . accept (URLDecoder .decode (pair , charset ), null );
347
376
}
348
377
else {
349
378
String name = URLDecoder .decode (pair .substring (0 , idx ), charset );
350
379
String value = URLDecoder .decode (pair .substring (idx + 1 ), charset );
351
- result . add (name , value );
380
+ addFunction . accept (name , value );
352
381
}
353
382
}
354
- return result ;
355
383
}
356
384
357
385
@ Override
358
386
@ SuppressWarnings ("unchecked" )
359
- public void write (MultiValueMap <String , ?> map , @ Nullable MediaType contentType , HttpOutputMessage outputMessage )
387
+ public void write (Map <String , ?> map , @ Nullable MediaType contentType , HttpOutputMessage outputMessage )
360
388
throws IOException , HttpMessageNotWritableException {
361
389
362
390
if (isMultipart (map , contentType )) {
363
- writeMultipart ((MultiValueMap <String , Object >) map , contentType , outputMessage );
391
+ writeMultipart ((Map <String , Object >) map , contentType , outputMessage );
364
392
}
365
393
else {
366
- writeForm ((MultiValueMap <String , Object >) map , contentType , outputMessage );
394
+ writeForm ((Map <String , Object >) map , contentType , outputMessage );
367
395
}
368
396
}
369
397
370
398
371
- private boolean isMultipart (MultiValueMap <String , ?> map , @ Nullable MediaType contentType ) {
399
+ private boolean isMultipart (Map <String , ?> map , @ Nullable MediaType contentType ) {
372
400
if (contentType != null ) {
373
401
return contentType .getType ().equalsIgnoreCase ("multipart" );
374
402
}
375
- for (List <?> values : map .values ()) {
376
- for (Object value : values ) {
377
- if (value != null && !(value instanceof String )) {
378
- return true ;
403
+ for (Object value : map .values ()) {
404
+ if (value instanceof List <?> values ) {
405
+ for (Object v : values ) {
406
+ if (v != null && !(v instanceof String )) {
407
+ return true ;
408
+ }
379
409
}
380
410
}
411
+ else if (value != null && !(value instanceof String )) {
412
+ return true ;
413
+ }
381
414
}
382
415
return false ;
383
416
}
384
417
385
- private void writeForm (MultiValueMap <String , Object > formData , @ Nullable MediaType mediaType ,
418
+ private void writeForm (Map <String , Object > formData , @ Nullable MediaType mediaType ,
386
419
HttpOutputMessage outputMessage ) throws IOException {
387
420
388
421
mediaType = getFormContentType (mediaType );
@@ -430,30 +463,36 @@ protected MediaType getFormContentType(@Nullable MediaType contentType) {
430
463
return contentType ;
431
464
}
432
465
433
- protected String serializeForm (MultiValueMap <String , Object > formData , Charset charset ) {
466
+ protected String serializeForm (Map <String , Object > formData , Charset charset ) {
434
467
StringBuilder builder = new StringBuilder ();
435
- formData .forEach ((name , values ) -> {
468
+ formData .forEach ((name , value ) -> {
469
+ if (value instanceof List <?> values ) {
436
470
if (name == null ) {
437
471
Assert .isTrue (CollectionUtils .isEmpty (values ), () -> "Null name in form data: " + formData );
438
472
return ;
439
473
}
440
- values .forEach (value -> {
441
- if (builder .length () != 0 ) {
442
- builder .append ('&' );
443
- }
444
- builder .append (URLEncoder .encode (name , charset ));
445
- if (value != null ) {
446
- builder .append ('=' );
447
- builder .append (URLEncoder .encode (String .valueOf (value ), charset ));
448
- }
449
- });
474
+ values .forEach (v -> appendFormValue (builder , name , v , charset ));
475
+ }
476
+ else {
477
+ appendFormValue (builder , name , value , charset );
478
+ }
450
479
});
451
-
452
480
return builder .toString ();
453
481
}
454
482
483
+ private static void appendFormValue (StringBuilder builder , String name , @ Nullable Object value , Charset charset ) {
484
+ if (!builder .isEmpty ()) {
485
+ builder .append ('&' );
486
+ }
487
+ builder .append (URLEncoder .encode (name , charset ));
488
+ if (value != null ) {
489
+ builder .append ('=' );
490
+ builder .append (URLEncoder .encode (String .valueOf (value ), charset ));
491
+ }
492
+ }
493
+
455
494
private void writeMultipart (
456
- MultiValueMap <String , Object > parts , @ Nullable MediaType contentType , HttpOutputMessage outputMessage )
495
+ Map <String , Object > parts , @ Nullable MediaType contentType , HttpOutputMessage outputMessage )
457
496
throws IOException {
458
497
459
498
// If the supplied content type is null, fall back to multipart/form-data.
@@ -500,16 +539,24 @@ private boolean isFilenameCharsetSet() {
500
539
return (this .multipartCharset != null );
501
540
}
502
541
503
- private void writeParts (OutputStream os , MultiValueMap <String , Object > parts , byte [] boundary ) throws IOException {
504
- for (Map .Entry <String , List < Object > > entry : parts .entrySet ()) {
542
+ private void writeParts (OutputStream os , Map <String , Object > parts , byte [] boundary ) throws IOException {
543
+ for (Map .Entry <String , Object > entry : parts .entrySet ()) {
505
544
String name = entry .getKey ();
506
- for (Object part : entry .getValue ()) {
507
- if (part != null ) {
508
- writeBoundary (os , boundary );
509
- writePart (name , getHttpEntity (part ), os );
510
- writeNewLine (os );
545
+ Object value = entry .getValue ();
546
+ if (value instanceof List <?> values ) {
547
+ for (Object part : values ) {
548
+ if (part != null ) {
549
+ writeBoundary (os , boundary );
550
+ writePart (name , getHttpEntity (part ), os );
551
+ writeNewLine (os );
552
+ }
511
553
}
512
554
}
555
+ else if (value != null ) {
556
+ writeBoundary (os , boundary );
557
+ writePart (name , getHttpEntity (value ), os );
558
+ writeNewLine (os );
559
+ }
513
560
}
514
561
}
515
562
0 commit comments