View Javadoc
1   /*
2    * Copyright 2002-2014 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
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   * Implementation of {@link HttpMessageConverter} to read and write 'normal' HTML
47   * forms and also to write (but not read) multipart data (e.g. file uploads).
48   *
49   * <p>In other words, this converter can read and write the
50   * {@code "application/x-www-form-urlencoded"} media type as
51   * {@link MultiValueMap MultiValueMap&lt;String, String&gt;} and it can also
52   * write (but not read) the {@code "multipart/form-data"} media type as
53   * {@link MultiValueMap MultiValueMap&lt;String, Object&gt;}.
54   *
55   * <p>When writing multipart data, this converter uses other
56   * {@link HttpMessageConverter HttpMessageConverters} to write the respective
57   * MIME parts. By default, basic converters are registered (for {@code Strings}
58   * and {@code Resources}). These can be overridden through the
59   * {@link #setPartConverters partConverters} property.
60   *
61   * <p>For example, the following snippet shows how to submit an HTML form:
62   * <pre class="code">
63   * RestTemplate template = new RestTemplate();  // FormHttpMessageConverter is configured by default
64   * MultiValueMap&lt;String, String&gt; form = new LinkedMultiValueMap&lt;String, String&gt;();
65   * form.add("field 1", "value 1");
66   * form.add("field 2", "value 2");
67   * form.add("field 2", "value 3");
68   * template.postForLocation("http://example.com/myForm", form);
69   * </pre>
70   *
71   * <p>The following snippet shows how to do a file upload:
72   * <pre class="code">
73   * MultiValueMap&lt;String, Object&gt; parts = new LinkedMultiValueMap&lt;String, Object&gt;();
74   * parts.add("field 1", "value 1");
75   * parts.add("file", new ClassPathResource("myFile.jpg"));
76   * template.postForLocation("http://example.com/myFileUpload", parts);
77   * </pre>
78   *
79   * <p>Some methods in this class were inspired by
80   * {@code org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity}.
81   *
82   * @author Arjen Poutsma
83   * @author Rossen Stoyanchev
84   * @since 3.0
85   * @see MultiValueMap
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 	 * Set the default character set to use for reading and writing form data when
123 	 * the request or response Content-Type header does not explicitly specify it.
124 	 * <p>By default this is set to "UTF-8".
125 	 */
126 	public void setCharset(Charset charset) {
127 		this.charset = charset;
128 	}
129 
130 	/**
131 	 * Set the character set to use when writing multipart data to encode file
132 	 * names. Encoding is based on the encoded-word syntax defined in RFC 2047
133 	 * and relies on {@code MimeUtility} from "javax.mail".
134 	 * <p>If not set file names will be encoded as US-ASCII.
135 	 * @param multipartCharset the charset to use
136 	 * @since 4.1.1
137 	 * @see <a href="http://en.wikipedia.org/wiki/MIME#Encoded-Word">Encoded-Word</a>
138 	 */
139 	public void setMultipartCharset(Charset multipartCharset) {
140 		this.multipartCharset = multipartCharset;
141 	}
142 
143 	/**
144 	 * Set the list of {@link MediaType} objects supported by this converter.
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 	 * Set the message body converters to use. These converters are used to
157 	 * convert objects to MIME parts.
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 	 * Add a message body converter. Such a converter is used to convert objects
166 	 * to MIME parts.
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 			// We can't read multipart....
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 	 * Generate a multipart boundary.
342 	 * <p>The default implementation returns a random boundary.
343 	 * Can be overridden in subclasses.
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 	 * Return an {@link HttpEntity} for the given part Object.
355 	 * @param part the part to return an {@link HttpEntity} for
356 	 * @return the part Object itself it is an {@link HttpEntity},
357 	 * or a newly built {@link HttpEntity} wrapper for that part
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 	 * Return the filename of the given multipart part. This value will be used for the
370 	 * {@code Content-Disposition} header.
371 	 * <p>The default implementation returns {@link Resource#getFilename()} if the part is a
372 	 * {@code Resource}, and {@code null} in other cases. Can be overridden in subclasses.
373 	 * @param part the part to determine the file name for
374 	 * @return the filename, or {@code null} if not known
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 	 * Implementation of {@link org.springframework.http.HttpOutputMessage} used
415 	 * to write a MIME multipart.
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 				// Should not happen - US-ASCII is always supported.
464 				throw new IllegalStateException(ex);
465 			}
466 		}
467 	}
468 
469 
470 	/**
471 	 * Inner class to avoid a hard dependency on the JavaMail API.
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 }