1 /*
2  * Copyright 2002-2011 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.InputStreamReader;
21 import java.io.OutputStream;
22 import java.io.UnsupportedEncodingException;
23 import java.net.URLDecoder;
24 import java.net.URLEncoder;
25 import java.nio.charset.Charset;
26 import java.util.ArrayList;
27 import java.util.Collections;
28 import java.util.Iterator;
29 import java.util.List;
30 import java.util.Map;
31 import java.util.Random;
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.FileCopyUtils;
41 import org.springframework.util.LinkedMultiValueMap;
42 import org.springframework.util.MultiValueMap;
43 import org.springframework.util.StringUtils;
44 
45 /**
46  * Implementation of {@link HttpMessageConverter} that can handle form data, including multipart form data (i.e. file
47  * uploads).
48  *
49  * <p>This converter can write the {@code application/x-www-form-urlencoded} and {@code multipart/form-data} media
50  * types, and read the {@code application/x-www-form-urlencoded}) media type (but not {@code multipart/form-data}).
51  *
52  * <p>In other words, this converter can read and write 'normal' HTML forms (as {@link MultiValueMap
53  * MultiValueMap&lt;String, String&gt;}), and it can write multipart form (as {@link MultiValueMap
54  * MultiValueMap&lt;String, Object&gt;}. When writing multipart, this converter uses other {@link HttpMessageConverter
55  * HttpMessageConverters} to write the respective MIME parts. By default, basic converters are registered (supporting
56  * {@code Strings} and {@code Resources}, for instance); these can be overridden by setting the {@link
57  * #setPartConverters(java.util.List) partConverters} property.
58  *
59  * <p>For example, the following snippet shows how to submit an HTML form: <pre class="code"> RestTemplate template =
60  * new RestTemplate(); // FormHttpMessageConverter is configured by default MultiValueMap&lt;String, String&gt; form =
61  * new LinkedMultiValueMap&lt;String, String&gt;(); form.add("field 1", "value 1"); form.add("field 2", "value 2");
62  * form.add("field 2", "value 3"); template.postForLocation("http://example.com/myForm", form); </pre> <p>The following
63  * snippet shows how to do a file upload: <pre class="code"> MultiValueMap&lt;String, Object&gt; parts = new
64  * LinkedMultiValueMap&lt;String, Object&gt;(); parts.add("field 1", "value 1"); parts.add("file", new
65  * ClassPathResource("myFile.jpg")); template.postForLocation("http://example.com/myFileUpload", parts); </pre>
66  *
67  * <p>Some methods in this class were inspired by {@link org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity}.
68  *
69  * @author Arjen Poutsma
70  * @see MultiValueMap
71  * @since 3.0
72  */
73 public class FormHttpMessageConverter implements HttpMessageConverter<MultiValueMap<String, ?>> {
74 
75 	private static final byte[] BOUNDARY_CHARS =
76 			new byte[]{'-', '_', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'a', 'b', 'c', 'd', 'e', 'f', 'g',
77 					'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A',
78 					'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U',
79 					'V', 'W', 'X', 'Y', 'Z'};
80 
81 	private final Random rnd = new Random();
82 
83 	private Charset charset = Charset.forName("UTF-8");
84 
85 	private List<MediaType> supportedMediaTypes = new ArrayList<MediaType>();
86 
87 	private List<HttpMessageConverter<?>> partConverters = new ArrayList<HttpMessageConverter<?>>();
88 
89 
FormHttpMessageConverter()90 	public FormHttpMessageConverter() {
91 		this.supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
92 		this.supportedMediaTypes.add(MediaType.MULTIPART_FORM_DATA);
93 
94 		this.partConverters.add(new ByteArrayHttpMessageConverter());
95 		StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
96 		stringHttpMessageConverter.setWriteAcceptCharset(false);
97 		this.partConverters.add(stringHttpMessageConverter);
98 		this.partConverters.add(new ResourceHttpMessageConverter());
99 	}
100 
101 
102 	/**
103 	 * Set the message body converters to use. These converters are used to convert objects to MIME parts.
104 	 */
setPartConverters(List<HttpMessageConverter<?>> partConverters)105 	public final void setPartConverters(List<HttpMessageConverter<?>> partConverters) {
106 		Assert.notEmpty(partConverters, "'partConverters' must not be empty");
107 		this.partConverters = partConverters;
108 	}
109 
110 	/**
111 	 * Add a message body converter. Such a converters is used to convert objects to MIME parts.
112 	 */
addPartConverter(HttpMessageConverter<?> partConverter)113 	public final void addPartConverter(HttpMessageConverter<?> partConverter) {
114 		Assert.notNull(partConverter, "'partConverter' must not be NULL");
115 		this.partConverters.add(partConverter);
116 	}
117 
118 	/**
119 	 * Sets the character set used for writing form data.
120 	 */
setCharset(Charset charset)121 	public void setCharset(Charset charset) {
122 		this.charset = charset;
123 	}
124 
canRead(Class<?> clazz, MediaType mediaType)125 	public boolean canRead(Class<?> clazz, MediaType mediaType) {
126 		if (!MultiValueMap.class.isAssignableFrom(clazz)) {
127 			return false;
128 		}
129 		if (mediaType == null) {
130 			return true;
131 		}
132 		for (MediaType supportedMediaType : getSupportedMediaTypes()) {
133 			// we can't read multipart
134 			if (!supportedMediaType.equals(MediaType.MULTIPART_FORM_DATA) &&
135 				supportedMediaType.includes(mediaType)) {
136 				return true;
137 			}
138 		}
139 		return false;
140 	}
141 
canWrite(Class<?> clazz, MediaType mediaType)142 	public boolean canWrite(Class<?> clazz, MediaType mediaType) {
143 		if (!MultiValueMap.class.isAssignableFrom(clazz)) {
144 			return false;
145 		}
146 		if (mediaType == null || MediaType.ALL.equals(mediaType)) {
147 			return true;
148 		}
149 		for (MediaType supportedMediaType : getSupportedMediaTypes()) {
150 			if (supportedMediaType.isCompatibleWith(mediaType)) {
151 				return true;
152 			}
153 		}
154 		return false;
155 	}
156 
157 	/**
158 	 * Set the list of {@link MediaType} objects supported by this converter.
159 	 */
setSupportedMediaTypes(List<MediaType> supportedMediaTypes)160 	public void setSupportedMediaTypes(List<MediaType> supportedMediaTypes) {
161 		this.supportedMediaTypes = supportedMediaTypes;
162 	}
163 
getSupportedMediaTypes()164 	public List<MediaType> getSupportedMediaTypes() {
165 		return Collections.unmodifiableList(this.supportedMediaTypes);
166 	}
167 
read(Class<? extends MultiValueMap<String, ?>> clazz, HttpInputMessage inputMessage)168 	public MultiValueMap<String, String> read(Class<? extends MultiValueMap<String, ?>> clazz,
169 			HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
170 
171 		MediaType contentType = inputMessage.getHeaders().getContentType();
172 		Charset charset = contentType.getCharSet() != null ? contentType.getCharSet() : this.charset;
173 		String body = FileCopyUtils.copyToString(new InputStreamReader(inputMessage.getBody(), charset));
174 
175 		String[] pairs = StringUtils.tokenizeToStringArray(body, "&");
176 
177 		MultiValueMap<String, String> result = new LinkedMultiValueMap<String, String>(pairs.length);
178 
179 		for (String pair : pairs) {
180 			int idx = pair.indexOf('=');
181 			if (idx == -1) {
182 				result.add(URLDecoder.decode(pair, charset.name()), null);
183 			}
184 			else {
185 				String name = URLDecoder.decode(pair.substring(0, idx), charset.name());
186 				String value = URLDecoder.decode(pair.substring(idx + 1), charset.name());
187 				result.add(name, value);
188 			}
189 		}
190 		return result;
191 	}
192 
193 	@SuppressWarnings("unchecked")
write(MultiValueMap<String, ?> map, MediaType contentType, HttpOutputMessage outputMessage)194 	public void write(MultiValueMap<String, ?> map, MediaType contentType, HttpOutputMessage outputMessage)
195 			throws IOException, HttpMessageNotWritableException {
196 		if (!isMultipart(map, contentType)) {
197 			writeForm((MultiValueMap<String, String>) map, contentType, outputMessage);
198 		}
199 		else {
200 			writeMultipart((MultiValueMap<String, Object>) map, outputMessage);
201 		}
202 	}
203 
isMultipart(MultiValueMap<String, ?> map, MediaType contentType)204 	private boolean isMultipart(MultiValueMap<String, ?> map, MediaType contentType) {
205 		if (contentType != null) {
206 			return MediaType.MULTIPART_FORM_DATA.equals(contentType);
207 		}
208 		for (String name : map.keySet()) {
209 			for (Object value : map.get(name)) {
210 				if (value != null && !(value instanceof String)) {
211 					return true;
212 				}
213 			}
214 		}
215 		return false;
216 	}
217 
writeForm(MultiValueMap<String, String> form, MediaType contentType, HttpOutputMessage outputMessage)218 	private void writeForm(MultiValueMap<String, String> form, MediaType contentType, HttpOutputMessage outputMessage)
219 			throws IOException {
220 		Charset charset;
221 		if (contentType != null) {
222 			outputMessage.getHeaders().setContentType(contentType);
223 			charset = contentType.getCharSet() != null ? contentType.getCharSet() : this.charset;
224 		}
225 		else {
226 			outputMessage.getHeaders().setContentType(MediaType.APPLICATION_FORM_URLENCODED);
227 			charset = this.charset;
228 		}
229 		StringBuilder builder = new StringBuilder();
230 		for (Iterator<String> nameIterator = form.keySet().iterator(); nameIterator.hasNext();) {
231 			String name = nameIterator.next();
232 			for (Iterator<String> valueIterator = form.get(name).iterator(); valueIterator.hasNext();) {
233 				String value = valueIterator.next();
234 				builder.append(URLEncoder.encode(name, charset.name()));
235 				if (value != null) {
236 					builder.append('=');
237 					builder.append(URLEncoder.encode(value, charset.name()));
238 					if (valueIterator.hasNext()) {
239 						builder.append('&');
240 					}
241 				}
242 			}
243 			if (nameIterator.hasNext()) {
244 				builder.append('&');
245 			}
246 		}
247 		byte[] bytes = builder.toString().getBytes(charset.name());
248 		outputMessage.getHeaders().setContentLength(bytes.length);
249 		FileCopyUtils.copy(bytes, outputMessage.getBody());
250 	}
251 
writeMultipart(MultiValueMap<String, Object> parts, HttpOutputMessage outputMessage)252 	private void writeMultipart(MultiValueMap<String, Object> parts, HttpOutputMessage outputMessage)
253 			throws IOException {
254 		byte[] boundary = generateMultipartBoundary();
255 
256 		Map<String, String> parameters = Collections.singletonMap("boundary", new String(boundary, "US-ASCII"));
257 		MediaType contentType = new MediaType(MediaType.MULTIPART_FORM_DATA, parameters);
258 		outputMessage.getHeaders().setContentType(contentType);
259 
260 		writeParts(outputMessage.getBody(), parts, boundary);
261 		writeEnd(boundary, outputMessage.getBody());
262 	}
263 
writeParts(OutputStream os, MultiValueMap<String, Object> parts, byte[] boundary)264 	private void writeParts(OutputStream os, MultiValueMap<String, Object> parts, byte[] boundary) throws IOException {
265 		for (Map.Entry<String, List<Object>> entry : parts.entrySet()) {
266 			String name = entry.getKey();
267 			for (Object part : entry.getValue()) {
268 				if (part != null) {
269 					writeBoundary(boundary, os);
270 					HttpEntity entity = getEntity(part);
271 					writePart(name, entity, os);
272 					writeNewLine(os);
273 				}
274 			}
275 		}
276 	}
277 
writeBoundary(byte[] boundary, OutputStream os)278 	private void writeBoundary(byte[] boundary, OutputStream os) throws IOException {
279 		os.write('-');
280 		os.write('-');
281 		os.write(boundary);
282 		writeNewLine(os);
283 	}
284 
285 	@SuppressWarnings("unchecked")
getEntity(Object part)286 	private HttpEntity getEntity(Object part) {
287 		if (part instanceof HttpEntity) {
288 			return (HttpEntity) part;
289 		}
290 		else {
291 			return new HttpEntity(part);
292 		}
293 	}
294 
295 	@SuppressWarnings("unchecked")
writePart(String name, HttpEntity partEntity, OutputStream os)296 	private void writePart(String name, HttpEntity partEntity, OutputStream os) throws IOException {
297 		Object partBody = partEntity.getBody();
298 		Class<?> partType = partBody.getClass();
299 		HttpHeaders partHeaders = partEntity.getHeaders();
300 		MediaType partContentType = partHeaders.getContentType();
301 		for (HttpMessageConverter messageConverter : partConverters) {
302 			if (messageConverter.canWrite(partType, partContentType)) {
303 				HttpOutputMessage multipartOutputMessage = new MultipartHttpOutputMessage(os);
304 				multipartOutputMessage.getHeaders().setContentDispositionFormData(name, getFilename(partBody));
305 				if (!partHeaders.isEmpty()) {
306 					multipartOutputMessage.getHeaders().putAll(partHeaders);
307 				}
308 				messageConverter.write(partBody, partContentType, multipartOutputMessage);
309 				return;
310 			}
311 		}
312 		throw new HttpMessageNotWritableException(
313 				"Could not write request: no suitable HttpMessageConverter found for request type [" +
314 						partType.getName() + "]");
315 	}
316 
writeEnd(byte[] boundary, OutputStream os)317 	private void writeEnd(byte[] boundary, OutputStream os) throws IOException {
318 		os.write('-');
319 		os.write('-');
320 		os.write(boundary);
321 		os.write('-');
322 		os.write('-');
323 		writeNewLine(os);
324 	}
325 
writeNewLine(OutputStream os)326 	private void writeNewLine(OutputStream os) throws IOException {
327 		os.write('\r');
328 		os.write('\n');
329 	}
330 
331 	/**
332 	 * Generate a multipart boundary.
333 	 * <p>The default implementation returns a random boundary.
334 	 * Can be overridden in subclasses.
335 	 */
generateMultipartBoundary()336 	protected byte[] generateMultipartBoundary() {
337 		byte[] boundary = new byte[rnd.nextInt(11) + 30];
338 		for (int i = 0; i < boundary.length; i++) {
339 			boundary[i] = BOUNDARY_CHARS[rnd.nextInt(BOUNDARY_CHARS.length)];
340 		}
341 		return boundary;
342 	}
343 
344 	/**
345 	 * Return the filename of the given multipart part. This value will be used for the
346 	 * {@code Content-Disposition} header.
347 	 * <p>The default implementation returns {@link Resource#getFilename()} if the part is a
348 	 * {@code Resource}, and {@code null} in other cases. Can be overridden in subclasses.
349 	 * @param part the part to determine the file name for
350 	 * @return the filename, or {@code null} if not known
351 	 */
getFilename(Object part)352 	protected String getFilename(Object part) {
353 		if (part instanceof Resource) {
354 			Resource resource = (Resource) part;
355 			return resource.getFilename();
356 		}
357 		else {
358 			return null;
359 		}
360 	}
361 
362 
363 	/**
364 	 * Implementation of {@link org.springframework.http.HttpOutputMessage} used for writing multipart data.
365 	 */
366 	private class MultipartHttpOutputMessage implements HttpOutputMessage {
367 
368 		private final HttpHeaders headers = new HttpHeaders();
369 
370 		private final OutputStream os;
371 
372 		private boolean headersWritten = false;
373 
MultipartHttpOutputMessage(OutputStream os)374 		public MultipartHttpOutputMessage(OutputStream os) {
375 			this.os = os;
376 		}
377 
getHeaders()378 		public HttpHeaders getHeaders() {
379 			return headersWritten ? HttpHeaders.readOnlyHttpHeaders(headers) : this.headers;
380 		}
381 
getBody()382 		public OutputStream getBody() throws IOException {
383 			writeHeaders();
384 			return this.os;
385 		}
386 
writeHeaders()387 		private void writeHeaders() throws IOException {
388 			if (!this.headersWritten) {
389 				for (Map.Entry<String, List<String>> entry : this.headers.entrySet()) {
390 					byte[] headerName = getAsciiBytes(entry.getKey());
391 					for (String headerValueString : entry.getValue()) {
392 						byte[] headerValue = getAsciiBytes(headerValueString);
393 						os.write(headerName);
394 						os.write(':');
395 						os.write(' ');
396 						os.write(headerValue);
397 						writeNewLine(os);
398 					}
399 				}
400 				writeNewLine(os);
401 				this.headersWritten = true;
402 			}
403 		}
404 
getAsciiBytes(String name)405 		protected byte[] getAsciiBytes(String name) {
406 			try {
407 				return name.getBytes("US-ASCII");
408 			}
409 			catch (UnsupportedEncodingException ex) {
410 				// should not happen, US-ASCII is always supported
411 				throw new IllegalStateException(ex);
412 			}
413 		}
414 	}
415 
416 }
417