18
18
19
19
import java .nio .charset .Charset ;
20
20
import java .nio .charset .StandardCharsets ;
21
+ import java .util .ArrayList ;
21
22
import java .util .Arrays ;
22
23
import java .util .Collections ;
23
24
import java .util .HashMap ;
44
45
import org .springframework .http .MediaType ;
45
46
import org .springframework .http .ReactiveHttpOutputMessage ;
46
47
import org .springframework .http .codec .EncoderHttpMessageWriter ;
48
+ import org .springframework .http .codec .FormHttpMessageWriter ;
47
49
import org .springframework .http .codec .HttpMessageWriter ;
48
50
import org .springframework .http .codec .ResourceHttpMessageWriter ;
49
51
import org .springframework .lang .Nullable ;
52
54
import org .springframework .util .MultiValueMap ;
53
55
54
56
/**
55
- * {@code HttpMessageWriter} for {@code "multipart/form-data"} requests.
57
+ * {@link HttpMessageWriter} for writing a {@code MultiValueMap<String, ?>}
58
+ * as multipart form data, i.e. {@code "multipart/form-data"}, to the body
59
+ * of a request.
56
60
*
57
- * <p>This writer delegates to other message writers to write the respective
58
- * parts. By default basic writers are registered for {@code String}, and
59
- * {@code Resources}. These can be overridden through the provided constructors.
61
+ * <p>The serialization of individual parts is delegated to other writers.
62
+ * By default only {@link String} and {@link Resource} parts are supported but
63
+ * you can configure others through a constructor argument.
64
+ *
65
+ * <p>This writer can be configured with a {@link FormHttpMessageWriter} to
66
+ * delegate to. It is the preferred way of supporting both form data and
67
+ * multipart data (as opposed to registering each writer separately) so that
68
+ * when the {@link MediaType} is not specified and generics are not present on
69
+ * the target element type, we can inspect the values in the actual map and
70
+ * decide whether to write plain form data (String values only) or otherwise.
60
71
*
61
72
* @author Sebastien Deleuze
62
73
* @author Rossen Stoyanchev
63
74
* @since 5.0
75
+ * @see FormHttpMessageWriter
64
76
*/
65
77
public class MultipartHttpMessageWriter implements HttpMessageWriter <MultiValueMap <String , ?>> {
66
78
67
79
public static final Charset DEFAULT_CHARSET = StandardCharsets .UTF_8 ;
68
80
69
81
70
- private final DataBufferFactory bufferFactory = new DefaultDataBufferFactory ();
71
-
72
82
private final List <HttpMessageWriter <?>> partWriters ;
73
83
84
+ @ Nullable
85
+ private final HttpMessageWriter <MultiValueMap <String , String >> formWriter ;
86
+
74
87
private Charset charset = DEFAULT_CHARSET ;
75
88
89
+ private final List <MediaType > supportedMediaTypes ;
76
90
91
+ private final DataBufferFactory bufferFactory = new DefaultDataBufferFactory ();
92
+
93
+
94
+ /**
95
+ * Constructor with a default list of part writers (String and Resource).
96
+ */
77
97
public MultipartHttpMessageWriter () {
78
- this . partWriters = Arrays .asList (
98
+ this ( Arrays .asList (
79
99
new EncoderHttpMessageWriter <>(CharSequenceEncoder .textPlainOnly ()),
80
100
new ResourceHttpMessageWriter ()
81
- );
101
+ )) ;
82
102
}
83
103
104
+ /**
105
+ * Constructor with explicit list of writers for serializing parts.
106
+ */
84
107
public MultipartHttpMessageWriter (List <HttpMessageWriter <?>> partWriters ) {
108
+ this (partWriters , new FormHttpMessageWriter ());
109
+ }
110
+
111
+ /**
112
+ * Constructor with explicit list of writers for serializing parts and a
113
+ * writer for plain form data to fall back when no media type is specified
114
+ * and the actual map consists of String values only.
115
+ * @param partWriters the writers for serializing parts
116
+ * @param formWriter the fallback writer for form data, {@code null} by default
117
+ */
118
+ public MultipartHttpMessageWriter (List <HttpMessageWriter <?>> partWriters ,
119
+ @ Nullable HttpMessageWriter <MultiValueMap <String , String >> formWriter ) {
120
+
85
121
this .partWriters = partWriters ;
122
+ this .formWriter = formWriter ;
123
+ this .supportedMediaTypes = initMediaTypes (formWriter );
124
+ }
125
+
126
+ private static List <MediaType > initMediaTypes (@ Nullable HttpMessageWriter <?> formWriter ) {
127
+ List <MediaType > result = new ArrayList <>();
128
+ result .add (MediaType .MULTIPART_FORM_DATA );
129
+ if (formWriter != null ) {
130
+ result .addAll (formWriter .getWritableMediaTypes ());
131
+ }
132
+ return Collections .unmodifiableList (result );
86
133
}
87
134
88
135
@@ -106,34 +153,63 @@ public Charset getCharset() {
106
153
107
154
@ Override
108
155
public List <MediaType > getWritableMediaTypes () {
109
- return Collections . singletonList ( MediaType . MULTIPART_FORM_DATA ) ;
156
+ return this . supportedMediaTypes ;
110
157
}
111
158
112
159
@ Override
113
160
public boolean canWrite (ResolvableType elementType , @ Nullable MediaType mediaType ) {
114
161
Class <?> rawClass = elementType .getRawClass ();
115
- return (rawClass != null && MultiValueMap .class .isAssignableFrom (rawClass ) &&
116
- (mediaType == null || MediaType .MULTIPART_FORM_DATA .isCompatibleWith (mediaType )));
162
+ return rawClass != null && MultiValueMap .class .isAssignableFrom (rawClass ) &&
163
+ (mediaType == null ||
164
+ this .supportedMediaTypes .stream ().anyMatch (m -> m .isCompatibleWith (mediaType )));
117
165
}
118
166
119
167
@ Override
120
168
public Mono <Void > write (Publisher <? extends MultiValueMap <String , ?>> inputStream ,
121
169
ResolvableType elementType , @ Nullable MediaType mediaType , ReactiveHttpOutputMessage outputMessage ,
122
170
Map <String , Object > hints ) {
123
171
172
+ return Mono .from (inputStream ).flatMap (map -> {
173
+ if (this .formWriter == null || isMultipart (map , mediaType )) {
174
+ return writeMultipart (map , outputMessage );
175
+ }
176
+ else {
177
+ @ SuppressWarnings ("unchecked" )
178
+ MultiValueMap <String , String > formData = (MultiValueMap <String , String >) map ;
179
+ return this .formWriter .write (Mono .just (formData ), elementType , mediaType , outputMessage , hints );
180
+ }
181
+
182
+ });
183
+ }
184
+
185
+ private boolean isMultipart (MultiValueMap <String , ?> map , @ Nullable MediaType contentType ) {
186
+ if (contentType != null ) {
187
+ return MediaType .MULTIPART_FORM_DATA .includes (contentType );
188
+ }
189
+ for (String name : map .keySet ()) {
190
+ for (Object value : map .get (name )) {
191
+ if (value != null && !(value instanceof String )) {
192
+ return true ;
193
+ }
194
+ }
195
+ }
196
+ return false ;
197
+ }
198
+
199
+ private Mono <Void > writeMultipart (MultiValueMap <String , ?> map , ReactiveHttpOutputMessage outputMessage ) {
124
200
byte [] boundary = generateMultipartBoundary ();
125
201
126
202
Map <String , String > params = new HashMap <>(2 );
127
203
params .put ("boundary" , new String (boundary , StandardCharsets .US_ASCII ));
128
204
params .put ("charset" , getCharset ().name ());
205
+
129
206
outputMessage .getHeaders ().setContentType (new MediaType (MediaType .MULTIPART_FORM_DATA , params ));
130
207
131
- return Mono .from (inputStream ).flatMap (map -> {
132
- Flux <DataBuffer > body = Flux .fromIterable (map .entrySet ())
133
- .concatMap (entry -> encodePartValues (boundary , entry .getKey (), entry .getValue ()))
134
- .concatWith (Mono .just (generateLastLine (boundary )));
135
- return outputMessage .writeWith (body );
136
- });
208
+ Flux <DataBuffer > body = Flux .fromIterable (map .entrySet ())
209
+ .concatMap (entry -> encodePartValues (boundary , entry .getKey (), entry .getValue ()))
210
+ .concatWith (Mono .just (generateLastLine (boundary )));
211
+
212
+ return outputMessage .writeWith (body );
137
213
}
138
214
139
215
/**
0 commit comments