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<String, String>}), and it can write multipart form (as {@link MultiValueMap 54 * MultiValueMap<String, Object>}. 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<String, String> form = 61 * new LinkedMultiValueMap<String, String>(); 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<String, Object> parts = new 64 * LinkedMultiValueMap<String, Object>(); 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