1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.springframework.http.converter;
18
19 import java.io.IOException;
20 import java.io.OutputStream;
21 import java.io.UnsupportedEncodingException;
22 import java.net.URLDecoder;
23 import java.net.URLEncoder;
24 import java.nio.charset.Charset;
25 import java.util.ArrayList;
26 import java.util.Collections;
27 import java.util.Iterator;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.Random;
31 import javax.mail.internet.MimeUtility;
32
33 import org.springframework.core.io.Resource;
34 import org.springframework.http.HttpEntity;
35 import org.springframework.http.HttpHeaders;
36 import org.springframework.http.HttpInputMessage;
37 import org.springframework.http.HttpOutputMessage;
38 import org.springframework.http.MediaType;
39 import org.springframework.util.Assert;
40 import org.springframework.util.LinkedMultiValueMap;
41 import org.springframework.util.MultiValueMap;
42 import org.springframework.util.StreamUtils;
43 import org.springframework.util.StringUtils;
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87 public class FormHttpMessageConverter implements HttpMessageConverter<MultiValueMap<String, ?>> {
88
89 public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
90
91 private static final byte[] BOUNDARY_CHARS =
92 new byte[] {'-', '_', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'a', 'b', 'c', 'd', 'e', 'f', 'g',
93 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A',
94 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U',
95 'V', 'W', 'X', 'Y', 'Z'};
96
97
98 private Charset charset = DEFAULT_CHARSET;
99
100 private Charset multipartCharset;
101
102 private List<MediaType> supportedMediaTypes = new ArrayList<MediaType>();
103
104 private List<HttpMessageConverter<?>> partConverters = new ArrayList<HttpMessageConverter<?>>();
105
106 private final Random random = new Random();
107
108
109 public FormHttpMessageConverter() {
110 this.supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
111 this.supportedMediaTypes.add(MediaType.MULTIPART_FORM_DATA);
112
113 this.partConverters.add(new ByteArrayHttpMessageConverter());
114 StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
115 stringHttpMessageConverter.setWriteAcceptCharset(false);
116 this.partConverters.add(stringHttpMessageConverter);
117 this.partConverters.add(new ResourceHttpMessageConverter());
118 }
119
120
121
122
123
124
125
126 public void setCharset(Charset charset) {
127 this.charset = charset;
128 }
129
130
131
132
133
134
135
136
137
138
139 public void setMultipartCharset(Charset multipartCharset) {
140 this.multipartCharset = multipartCharset;
141 }
142
143
144
145
146 public void setSupportedMediaTypes(List<MediaType> supportedMediaTypes) {
147 this.supportedMediaTypes = supportedMediaTypes;
148 }
149
150 @Override
151 public List<MediaType> getSupportedMediaTypes() {
152 return Collections.unmodifiableList(this.supportedMediaTypes);
153 }
154
155
156
157
158
159 public void setPartConverters(List<HttpMessageConverter<?>> partConverters) {
160 Assert.notEmpty(partConverters, "'partConverters' must not be empty");
161 this.partConverters = partConverters;
162 }
163
164
165
166
167
168 public void addPartConverter(HttpMessageConverter<?> partConverter) {
169 Assert.notNull(partConverter, "'partConverter' must not be null");
170 this.partConverters.add(partConverter);
171 }
172
173
174 @Override
175 public boolean canRead(Class<?> clazz, MediaType mediaType) {
176 if (!MultiValueMap.class.isAssignableFrom(clazz)) {
177 return false;
178 }
179 if (mediaType == null) {
180 return true;
181 }
182 for (MediaType supportedMediaType : getSupportedMediaTypes()) {
183
184 if (!supportedMediaType.equals(MediaType.MULTIPART_FORM_DATA) && supportedMediaType.includes(mediaType)) {
185 return true;
186 }
187 }
188 return false;
189 }
190
191 @Override
192 public boolean canWrite(Class<?> clazz, MediaType mediaType) {
193 if (!MultiValueMap.class.isAssignableFrom(clazz)) {
194 return false;
195 }
196 if (mediaType == null || MediaType.ALL.equals(mediaType)) {
197 return true;
198 }
199 for (MediaType supportedMediaType : getSupportedMediaTypes()) {
200 if (supportedMediaType.isCompatibleWith(mediaType)) {
201 return true;
202 }
203 }
204 return false;
205 }
206
207 @Override
208 public MultiValueMap<String, String> read(Class<? extends MultiValueMap<String, ?>> clazz,
209 HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
210
211 MediaType contentType = inputMessage.getHeaders().getContentType();
212 Charset charset = (contentType.getCharSet() != null ? contentType.getCharSet() : this.charset);
213 String body = StreamUtils.copyToString(inputMessage.getBody(), charset);
214
215 String[] pairs = StringUtils.tokenizeToStringArray(body, "&");
216 MultiValueMap<String, String> result = new LinkedMultiValueMap<String, String>(pairs.length);
217 for (String pair : pairs) {
218 int idx = pair.indexOf('=');
219 if (idx == -1) {
220 result.add(URLDecoder.decode(pair, charset.name()), null);
221 }
222 else {
223 String name = URLDecoder.decode(pair.substring(0, idx), charset.name());
224 String value = URLDecoder.decode(pair.substring(idx + 1), charset.name());
225 result.add(name, value);
226 }
227 }
228 return result;
229 }
230
231 @Override
232 @SuppressWarnings("unchecked")
233 public void write(MultiValueMap<String, ?> map, MediaType contentType, HttpOutputMessage outputMessage)
234 throws IOException, HttpMessageNotWritableException {
235
236 if (!isMultipart(map, contentType)) {
237 writeForm((MultiValueMap<String, String>) map, contentType, outputMessage);
238 }
239 else {
240 writeMultipart((MultiValueMap<String, Object>) map, outputMessage);
241 }
242 }
243
244
245 private boolean isMultipart(MultiValueMap<String, ?> map, MediaType contentType) {
246 if (contentType != null) {
247 return MediaType.MULTIPART_FORM_DATA.includes(contentType);
248 }
249 for (String name : map.keySet()) {
250 for (Object value : map.get(name)) {
251 if (value != null && !(value instanceof String)) {
252 return true;
253 }
254 }
255 }
256 return false;
257 }
258
259 private void writeForm(MultiValueMap<String, String> form, MediaType contentType, HttpOutputMessage outputMessage)
260 throws IOException {
261
262 Charset charset;
263 if (contentType != null) {
264 outputMessage.getHeaders().setContentType(contentType);
265 charset = contentType.getCharSet() != null ? contentType.getCharSet() : this.charset;
266 }
267 else {
268 outputMessage.getHeaders().setContentType(MediaType.APPLICATION_FORM_URLENCODED);
269 charset = this.charset;
270 }
271 StringBuilder builder = new StringBuilder();
272 for (Iterator<String> nameIterator = form.keySet().iterator(); nameIterator.hasNext();) {
273 String name = nameIterator.next();
274 for (Iterator<String> valueIterator = form.get(name).iterator(); valueIterator.hasNext();) {
275 String value = valueIterator.next();
276 builder.append(URLEncoder.encode(name, charset.name()));
277 if (value != null) {
278 builder.append('=');
279 builder.append(URLEncoder.encode(value, charset.name()));
280 if (valueIterator.hasNext()) {
281 builder.append('&');
282 }
283 }
284 }
285 if (nameIterator.hasNext()) {
286 builder.append('&');
287 }
288 }
289 byte[] bytes = builder.toString().getBytes(charset.name());
290 outputMessage.getHeaders().setContentLength(bytes.length);
291 StreamUtils.copy(bytes, outputMessage.getBody());
292 }
293
294 private void writeMultipart(MultiValueMap<String, Object> parts, HttpOutputMessage outputMessage) throws IOException {
295 byte[] boundary = generateMultipartBoundary();
296 Map<String, String> parameters = Collections.singletonMap("boundary", new String(boundary, "US-ASCII"));
297
298 MediaType contentType = new MediaType(MediaType.MULTIPART_FORM_DATA, parameters);
299 outputMessage.getHeaders().setContentType(contentType);
300
301 writeParts(outputMessage.getBody(), parts, boundary);
302 writeEnd(outputMessage.getBody(), boundary);
303 }
304
305 private void writeParts(OutputStream os, MultiValueMap<String, Object> parts, byte[] boundary) throws IOException {
306 for (Map.Entry<String, List<Object>> entry : parts.entrySet()) {
307 String name = entry.getKey();
308 for (Object part : entry.getValue()) {
309 if (part != null) {
310 writeBoundary(os, boundary);
311 writePart(name, getHttpEntity(part), os);
312 writeNewLine(os);
313 }
314 }
315 }
316 }
317
318 @SuppressWarnings("unchecked")
319 private void writePart(String name, HttpEntity<?> partEntity, OutputStream os) throws IOException {
320 Object partBody = partEntity.getBody();
321 Class<?> partType = partBody.getClass();
322 HttpHeaders partHeaders = partEntity.getHeaders();
323 MediaType partContentType = partHeaders.getContentType();
324 for (HttpMessageConverter<?> messageConverter : this.partConverters) {
325 if (messageConverter.canWrite(partType, partContentType)) {
326 HttpOutputMessage multipartMessage = new MultipartHttpOutputMessage(os);
327 multipartMessage.getHeaders().setContentDispositionFormData(name, getFilename(partBody));
328 if (!partHeaders.isEmpty()) {
329 multipartMessage.getHeaders().putAll(partHeaders);
330 }
331 ((HttpMessageConverter<Object>) messageConverter).write(partBody, partContentType, multipartMessage);
332 return;
333 }
334 }
335 throw new HttpMessageNotWritableException("Could not write request: no suitable HttpMessageConverter " +
336 "found for request type [" + partType.getName() + "]");
337 }
338
339
340
341
342
343
344
345 protected byte[] generateMultipartBoundary() {
346 byte[] boundary = new byte[this.random.nextInt(11) + 30];
347 for (int i = 0; i < boundary.length; i++) {
348 boundary[i] = BOUNDARY_CHARS[this.random.nextInt(BOUNDARY_CHARS.length)];
349 }
350 return boundary;
351 }
352
353
354
355
356
357
358
359 protected HttpEntity<?> getHttpEntity(Object part) {
360 if (part instanceof HttpEntity) {
361 return (HttpEntity<?>) part;
362 }
363 else {
364 return new HttpEntity<Object>(part);
365 }
366 }
367
368
369
370
371
372
373
374
375
376 protected String getFilename(Object part) {
377 if (part instanceof Resource) {
378 Resource resource = (Resource) part;
379 String filename = resource.getFilename();
380 if (this.multipartCharset != null) {
381 filename = MimeDelegate.encode(filename, this.multipartCharset.name());
382 }
383 return filename;
384 }
385 else {
386 return null;
387 }
388 }
389
390
391 private void writeBoundary(OutputStream os, byte[] boundary) throws IOException {
392 os.write('-');
393 os.write('-');
394 os.write(boundary);
395 writeNewLine(os);
396 }
397
398 private static void writeEnd(OutputStream os, byte[] boundary) throws IOException {
399 os.write('-');
400 os.write('-');
401 os.write(boundary);
402 os.write('-');
403 os.write('-');
404 writeNewLine(os);
405 }
406
407 private static void writeNewLine(OutputStream os) throws IOException {
408 os.write('\r');
409 os.write('\n');
410 }
411
412
413
414
415
416
417 private static class MultipartHttpOutputMessage implements HttpOutputMessage {
418
419 private final OutputStream outputStream;
420
421 private final HttpHeaders headers = new HttpHeaders();
422
423 private boolean headersWritten = false;
424
425 public MultipartHttpOutputMessage(OutputStream outputStream) {
426 this.outputStream = outputStream;
427 }
428
429 @Override
430 public HttpHeaders getHeaders() {
431 return (this.headersWritten ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers);
432 }
433
434 @Override
435 public OutputStream getBody() throws IOException {
436 writeHeaders();
437 return this.outputStream;
438 }
439
440 private void writeHeaders() throws IOException {
441 if (!this.headersWritten) {
442 for (Map.Entry<String, List<String>> entry : this.headers.entrySet()) {
443 byte[] headerName = getAsciiBytes(entry.getKey());
444 for (String headerValueString : entry.getValue()) {
445 byte[] headerValue = getAsciiBytes(headerValueString);
446 this.outputStream.write(headerName);
447 this.outputStream.write(':');
448 this.outputStream.write(' ');
449 this.outputStream.write(headerValue);
450 writeNewLine(this.outputStream);
451 }
452 }
453 writeNewLine(this.outputStream);
454 this.headersWritten = true;
455 }
456 }
457
458 private byte[] getAsciiBytes(String name) {
459 try {
460 return name.getBytes("US-ASCII");
461 }
462 catch (UnsupportedEncodingException ex) {
463
464 throw new IllegalStateException(ex);
465 }
466 }
467 }
468
469
470
471
472
473 private static class MimeDelegate {
474
475 public static String encode(String value, String charset) {
476 try {
477 return MimeUtility.encodeText(value, charset, null);
478 }
479 catch (UnsupportedEncodingException ex) {
480 throw new IllegalStateException(ex);
481 }
482 }
483 }
484
485 }