@ -1,5 +1,5 @@
@@ -1,5 +1,5 @@
/ *
* Copyright 2002 - 2013 the original author or authors .
* Copyright 2002 - 2014 the original author or authors .
*
* Licensed under the Apache License , Version 2 . 0 ( the "License" ) ;
* you may not use this file except in compliance with the License .
@ -42,42 +42,55 @@ import org.springframework.util.StreamUtils;
@@ -42,42 +42,55 @@ import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils ;
/ * *
* Implementation of { @link HttpMessageConverter } that can handle form data , including multipart form data ( i . e . file
* uploads ) .
* Implementation of { @link HttpMessageConverter } to read and write ' normal ' HTML
* forms and also to write ( but not read ) multipart data ( e . g . file uploads ) .
*
* < p > This converter can write the { @code application / x - www - form - urlencoded } and { @code multipart / form - data } media
* types , and read the { @code application / x - www - form - urlencoded } ) media type ( but not { @code multipart / form - data } ) .
* < p > In other words , this converter can read and write the
* { @code "application/x-www-form-urlencoded" } media type as
* { @link MultiValueMap MultiValueMap & lt ; String , String & gt ; } and it can also
* write ( but not read ) the { @code "multipart/form-data" } media type as
* { @link MultiValueMap MultiValueMap & lt ; String , Object & gt ; } .
*
* < p > In other words , this converter can read and write ' normal ' HTML forms ( as { @link MultiValueMap
* MultiValueMap & lt ; String , String & gt ; } ) , and it can write multipart form ( as { @link MultiValueMap
* MultiValueMap & lt ; String , Object & gt ; } . When writing multipart , this converter uses other { @link HttpMessageConverter
* HttpMessageConverters } to write the respective MIME parts . By default , basic converters are registered ( supporting
* { @code Strings } and { @code Resources } , for instance ) ; these can be overridden by setting the { @link
* # setPartConverters ( java . util . List ) partConverters } property .
* < p > When writing multipart data , this converter uses other
* { @link HttpMessageConverter HttpMessageConverters } to write the respective
* MIME parts . By default , basic converters are registered ( for { @code Strings }
* and { @code Resources } ) . These can be overridden through the
* { @link # setPartConverters partConverters } property .
*
* < p > For example , the following snippet shows how to submit an HTML form : < pre class = "code" > RestTemplate template =
* new RestTemplate ( ) ; // FormHttpMessageConverter is configured by default MultiValueMap<String, String> form =
* new LinkedMultiValueMap & lt ; String , String & gt ; ( ) ; form . add ( "field 1" , "value 1" ) ; form . add ( "field 2" , "value 2" ) ;
* form . add ( "field 2" , "value 3" ) ; template . postForLocation ( "http://example.com/myForm" , form ) ; < / pre > < p > The following
* snippet shows how to do a file upload : < pre class = "code" > MultiValueMap & lt ; String , Object & gt ; parts = new
* LinkedMultiValueMap & lt ; String , Object & gt ; ( ) ; parts . add ( "field 1" , "value 1" ) ; parts . add ( "file" , new
* ClassPathResource ( "myFile.jpg" ) ) ; template . postForLocation ( "http://example.com/myFileUpload" , parts ) ; < / pre >
* < p > For example , the following snippet shows how to submit an HTML form :
* < pre class = "code" >
* RestTemplate template = new RestTemplate ( ) ; // FormHttpMessageConverter is configured by default
* MultiValueMap & lt ; String , String & gt ; form = new LinkedMultiValueMap & lt ; String , String & gt ; ( ) ;
* form . add ( "field 1" , "value 1" ) ;
* form . add ( "field 2" , "value 2" ) ;
* form . add ( "field 2" , "value 3" ) ;
* template . postForLocation ( "http://example.com/myForm" , form ) ;
* < / pre >
*
* < p > Some methods in this class were inspired by { @code org . apache . commons . httpclient . methods . multipart . MultipartRequestEntity } .
* < p > The following snippet shows how to do a file upload :
* < pre class = "code" >
* MultiValueMap & lt ; String , Object & gt ; parts = new LinkedMultiValueMap & lt ; String , Object & gt ; ( ) ;
* parts . add ( "field 1" , "value 1" ) ;
* parts . add ( "file" , new ClassPathResource ( "myFile.jpg" ) ) ;
* template . postForLocation ( "http://example.com/myFileUpload" , parts ) ;
* < / pre >
*
* < p > Some methods in this class were inspired by
* { @code org . apache . commons . httpclient . methods . multipart . MultipartRequestEntity } .
*
* @author Arjen Poutsma
* @see MultiValueMap
* @author Rossen Stoyanchev
* @since 3 . 0
* @see MultiValueMap
* /
public class FormHttpMessageConverter implements HttpMessageConverter < MultiValueMap < String , ? > > {
private static final byte [ ] BOUNDARY_CHARS =
new byte [ ] { '-' , '_' , '1' , '2' , '3' , '4' , '5' , '6' , '7' , '8' , '9' , '0' , 'a' , 'b' , 'c' , 'd' , 'e' , 'f' , 'g' ,
new byte [ ] { '-' , '_' , '1' , '2' , '3' , '4' , '5' , '6' , '7' , '8' , '9' , '0' , 'a' , 'b' , 'c' , 'd' , 'e' , 'f' , 'g' ,
'h' , 'i' , 'j' , 'k' , 'l' , 'm' , 'n' , 'o' , 'p' , 'q' , 'r' , 's' , 't' , 'u' , 'v' , 'w' , 'x' , 'y' , 'z' , 'A' ,
'B' , 'C' , 'D' , 'E' , 'F' , 'G' , 'H' , 'I' , 'J' , 'K' , 'L' , 'M' , 'N' , 'O' , 'P' , 'Q' , 'R' , 'S' , 'T' , 'U' ,
'V' , 'W' , 'X' , 'Y' , 'Z' } ;
private final Random rnd = new Random ( ) ;
private Charset charset = Charset . forName ( "UTF-8" ) ;
@ -85,6 +98,8 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
@@ -85,6 +98,8 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
private List < HttpMessageConverter < ? > > partConverters = new ArrayList < HttpMessageConverter < ? > > ( ) ;
private final Random random = new Random ( ) ;
public FormHttpMessageConverter ( ) {
this . supportedMediaTypes . add ( MediaType . APPLICATION_FORM_URLENCODED ) ;
@ -99,28 +114,45 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
@@ -99,28 +114,45 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
/ * *
* Set the message body converters to use . These converters are used to convert objects to MIME parts .
* Set the default character set to use for reading and writing form data when
* the request or response Content - Type header does not explicitly specify it .
* < p > By default this is set to "UTF-8" .
* /
public final void setPartConverters ( List < HttpMessageConverter < ? > > partConverters ) {
Assert . notEmpty ( partConverters , "'partConverters' must not be empty" ) ;
this . partConverters = partConverters ;
public void setCharset ( Charset charset ) {
this . charset = charset ;
}
/ * *
* Add a message body converter . Such a converters is used to convert objects to MIME parts .
* Set the list of { @link MediaType } objects supported by this converter .
* /
public final void addPartConverter ( HttpMessageConverter < ? > partConverter ) {
Assert . notNull ( partConverter , "'partConverter' must not be NULL" ) ;
this . partConverters . add ( partConverter ) ;
public void setSupportedMediaTypes ( List < MediaType > supportedMediaTypes ) {
this . supportedMediaTypes = supportedMediaTypes ;
}
@Override
public List < MediaType > getSupportedMediaTypes ( ) {
return Collections . unmodifiableList ( this . supportedMediaTypes ) ;
}
/ * *
* Sets the character set used for writing form data .
* Set the message body converters to use . These converters are used to
* convert objects to MIME parts .
* /
public void setCharset ( Charset charset ) {
this . charset = charset ;
public void setPartConverters ( List < HttpMessageConverter < ? > > partConverters ) {
Assert . notEmpty ( partConverters , "'partConverters' must not be empty" ) ;
this . partConverters = partConverters ;
}
/ * *
* Add a message body converter . Such a converter is used to convert objects
* to MIME parts .
* /
public void addPartConverter ( HttpMessageConverter < ? > partConverter ) {
Assert . notNull ( partConverter , "'partConverter' must not be null" ) ;
this . partConverters . add ( partConverter ) ;
}
@Override
public boolean canRead ( Class < ? > clazz , MediaType mediaType ) {
if ( ! MultiValueMap . class . isAssignableFrom ( clazz ) ) {
@ -130,9 +162,8 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
@@ -130,9 +162,8 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
return true ;
}
for ( MediaType supportedMediaType : getSupportedMediaTypes ( ) ) {
// we can't read multipart
if ( ! supportedMediaType . equals ( MediaType . MULTIPART_FORM_DATA ) & &
supportedMediaType . includes ( mediaType ) ) {
// We can't read multipart....
if ( ! supportedMediaType . equals ( MediaType . MULTIPART_FORM_DATA ) & & supportedMediaType . includes ( mediaType ) ) {
return true ;
}
}
@ -155,30 +186,16 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
@@ -155,30 +186,16 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
return false ;
}
/ * *
* Set the list of { @link MediaType } objects supported by this converter .
* /
public void setSupportedMediaTypes ( List < MediaType > supportedMediaTypes ) {
this . supportedMediaTypes = supportedMediaTypes ;
}
@Override
public List < MediaType > getSupportedMediaTypes ( ) {
return Collections . unmodifiableList ( this . supportedMediaTypes ) ;
}
@Override
public MultiValueMap < String , String > read ( Class < ? extends MultiValueMap < String , ? > > clazz ,
HttpInputMessage inputMessage ) throws IOException , HttpMessageNotReadableException {
MediaType contentType = inputMessage . getHeaders ( ) . getContentType ( ) ;
Charset charset = contentType . getCharSet ( ) ! = null ? contentType . getCharSet ( ) : this . charset ;
Charset charset = ( contentType . getCharSet ( ) ! = null ? contentType . getCharSet ( ) : this . charset ) ;
String body = StreamUtils . copyToString ( inputMessage . getBody ( ) , charset ) ;
String [ ] pairs = StringUtils . tokenizeToStringArray ( body , "&" ) ;
MultiValueMap < String , String > result = new LinkedMultiValueMap < String , String > ( pairs . length ) ;
for ( String pair : pairs ) {
int idx = pair . indexOf ( '=' ) ;
if ( idx = = - 1 ) {
@ -197,6 +214,7 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
@@ -197,6 +214,7 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
@SuppressWarnings ( "unchecked" )
public void write ( MultiValueMap < String , ? > map , MediaType contentType , HttpOutputMessage outputMessage )
throws IOException , HttpMessageNotWritableException {
if ( ! isMultipart ( map , contentType ) ) {
writeForm ( ( MultiValueMap < String , String > ) map , contentType , outputMessage ) ;
}
@ -205,6 +223,7 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
@@ -205,6 +223,7 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
}
}
private boolean isMultipart ( MultiValueMap < String , ? > map , MediaType contentType ) {
if ( contentType ! = null ) {
return MediaType . MULTIPART_FORM_DATA . equals ( contentType ) ;
@ -221,6 +240,7 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
@@ -221,6 +240,7 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
private void writeForm ( MultiValueMap < String , String > form , MediaType contentType , HttpOutputMessage outputMessage )
throws IOException {
Charset charset ;
if ( contentType ! = null ) {
outputMessage . getHeaders ( ) . setContentType ( contentType ) ;
@ -253,16 +273,15 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
@@ -253,16 +273,15 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
StreamUtils . copy ( bytes , outputMessage . getBody ( ) ) ;
}
private void writeMultipart ( MultiValueMap < String , Object > parts , HttpOutputMessage outputMessage )
throws IOException {
private void writeMultipart ( MultiValueMap < String , Object > parts , HttpOutputMessage outputMessage ) throws IOException {
byte [ ] boundary = generateMultipartBoundary ( ) ;
Map < String , String > parameters = Collections . singletonMap ( "boundary" , new String ( boundary , "US-ASCII" ) ) ;
MediaType contentType = new MediaType ( MediaType . MULTIPART_FORM_DATA , parameters ) ;
outputMessage . getHeaders ( ) . setContentType ( contentType ) ;
writeParts ( outputMessage . getBody ( ) , parts , boundary ) ;
writeEnd ( boundary , outputMessage . getBody ( ) ) ;
writeEnd ( outputMessage . getBody ( ) , boundary ) ;
}
private void writeParts ( OutputStream os , MultiValueMap < String , Object > parts , byte [ ] boundary ) throws IOException {
@ -270,66 +289,35 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
@@ -270,66 +289,35 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
String name = entry . getKey ( ) ;
for ( Object part : entry . getValue ( ) ) {
if ( part ! = null ) {
writeBoundary ( boundary , os ) ;
HttpEntity < ? > entity = getEntity ( part ) ;
writePart ( name , entity , os ) ;
writeBoundary ( os , boundary ) ;
writePart ( name , getHttpEntity ( part ) , os ) ;
writeNewLine ( os ) ;
}
}
}
}
private void writeBoundary ( byte [ ] boundary , OutputStream os ) throws IOException {
os . write ( '-' ) ;
os . write ( '-' ) ;
os . write ( boundary ) ;
writeNewLine ( os ) ;
}
private HttpEntity < ? > getEntity ( Object part ) {
if ( part instanceof HttpEntity ) {
return ( HttpEntity < ? > ) part ;
}
else {
return new HttpEntity < Object > ( part ) ;
}
}
@SuppressWarnings ( "unchecked" )
private void writePart ( String name , HttpEntity < ? > partEntity , OutputStream os ) throws IOException {
Object partBody = partEntity . getBody ( ) ;
Class < ? > partType = partBody . getClass ( ) ;
HttpHeaders partHeaders = partEntity . getHeaders ( ) ;
MediaType partContentType = partHeaders . getContentType ( ) ;
for ( HttpMessageConverter < ? > messageConverter : partConverters ) {
for ( HttpMessageConverter < ? > messageConverter : this . partConverters ) {
if ( messageConverter . canWrite ( partType , partContentType ) ) {
HttpOutputMessage multipartOutput Message = new MultipartHttpOutputMessage ( os ) ;
multipartOutput Message . getHeaders ( ) . setContentDispositionFormData ( name , getFilename ( partBody ) ) ;
HttpOutputMessage multipartMessage = new MultipartHttpOutputMessage ( os ) ;
multipartMessage . getHeaders ( ) . setContentDispositionFormData ( name , getFilename ( partBody ) ) ;
if ( ! partHeaders . isEmpty ( ) ) {
multipartOutput Message . getHeaders ( ) . putAll ( partHeaders ) ;
multipartMessage . getHeaders ( ) . putAll ( partHeaders ) ;
}
( ( HttpMessageConverter < Object > ) messageConverter ) . write ( partBody , partContentType , multipartOutput Message ) ;
( ( HttpMessageConverter < Object > ) messageConverter ) . write ( partBody , partContentType , multipartMessage ) ;
return ;
}
}
throw new HttpMessageNotWritableException (
"Could not write request: no suitable HttpMessageConverter found for request type [" +
partType . getName ( ) + "]" ) ;
}
private void writeEnd ( byte [ ] boundary , OutputStream os ) throws IOException {
os . write ( '-' ) ;
os . write ( '-' ) ;
os . write ( boundary ) ;
os . write ( '-' ) ;
os . write ( '-' ) ;
writeNewLine ( os ) ;
throw new HttpMessageNotWritableException ( "Could not write request: no suitable HttpMessageConverter " +
"found for request type [" + partType . getName ( ) + "]" ) ;
}
private void writeNewLine ( OutputStream os ) throws IOException {
os . write ( '\r' ) ;
os . write ( '\n' ) ;
}
/ * *
* Generate a multipart boundary .
@ -337,13 +325,28 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
@@ -337,13 +325,28 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
* Can be overridden in subclasses .
* /
protected byte [ ] generateMultipartBoundary ( ) {
byte [ ] boundary = new byte [ rnd . nextInt ( 11 ) + 30 ] ;
byte [ ] boundary = new byte [ this . ra ndom . nextInt ( 11 ) + 30 ] ;
for ( int i = 0 ; i < boundary . length ; i + + ) {
boundary [ i ] = BOUNDARY_CHARS [ rnd . nextInt ( BOUNDARY_CHARS . length ) ] ;
boundary [ i ] = BOUNDARY_CHARS [ this . ra ndom . nextInt ( BOUNDARY_CHARS . length ) ] ;
}
return boundary ;
}
/ * *
* Return an { @link HttpEntity } for the given part Object .
* @param part the part to return an { @link HttpEntity } for
* @return the part Object itself it is an { @link HttpEntity } ,
* or a newly built { @link HttpEntity } wrapper for that part
* /
protected HttpEntity < ? > getHttpEntity ( Object part ) {
if ( part instanceof HttpEntity ) {
return ( HttpEntity < ? > ) part ;
}
else {
return new HttpEntity < Object > ( part ) ;
}
}
/ * *
* Return the filename of the given multipart part . This value will be used for the
* { @code Content - Disposition } header .
@ -363,30 +366,53 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
@@ -363,30 +366,53 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
}
private void writeBoundary ( OutputStream os , byte [ ] boundary ) throws IOException {
os . write ( '-' ) ;
os . write ( '-' ) ;
os . write ( boundary ) ;
writeNewLine ( os ) ;
}
private static void writeEnd ( OutputStream os , byte [ ] boundary ) throws IOException {
os . write ( '-' ) ;
os . write ( '-' ) ;
os . write ( boundary ) ;
os . write ( '-' ) ;
os . write ( '-' ) ;
writeNewLine ( os ) ;
}
private static void writeNewLine ( OutputStream os ) throws IOException {
os . write ( '\r' ) ;
os . write ( '\n' ) ;
}
/ * *
* Implementation of { @link org . springframework . http . HttpOutputMessage } used for writing multipart data .
* Implementation of { @link org . springframework . http . HttpOutputMessage } used
* to write a MIME multipart .
* /
private class MultipartHttpOutputMessage implements HttpOutputMessage {
private static class MultipartHttpOutputMessage implements HttpOutputMessage {
private final HttpHeaders headers = new HttpHeaders ( ) ;
private final OutputStream outputStream ;
private final OutputStream os ;
private final HttpHeaders headers = new HttpHeaders ( ) ;
private boolean headersWritten = false ;
public MultipartHttpOutputMessage ( OutputStream os ) {
this . os = os ;
public MultipartHttpOutputMessage ( OutputStream outputStream ) {
this . outputStream = outputStream ;
}
@Override
public HttpHeaders getHeaders ( ) {
return headersWritten ? HttpHeaders . readOnlyHttpHeaders ( headers ) : this . headers ;
return ( this . headersWritten ? HttpHeaders . readOnlyHttpHeaders ( this . headers ) : this . headers ) ;
}
@Override
public OutputStream getBody ( ) throws IOException {
writeHeaders ( ) ;
return this . os ;
return this . outputStream ;
}
private void writeHeaders ( ) throws IOException {
@ -395,24 +421,24 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
@@ -395,24 +421,24 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
byte [ ] headerName = getAsciiBytes ( entry . getKey ( ) ) ;
for ( String headerValueString : entry . getValue ( ) ) {
byte [ ] headerValue = getAsciiBytes ( headerValueString ) ;
os . write ( headerName ) ;
os . write ( ':' ) ;
os . write ( ' ' ) ;
os . write ( headerValue ) ;
writeNewLine ( os ) ;
this . outputStream . write ( headerName ) ;
this . outputStream . write ( ':' ) ;
this . outputStream . write ( ' ' ) ;
this . outputStream . write ( headerValue ) ;
writeNewLine ( this . outputStream ) ;
}
}
writeNewLine ( os ) ;
writeNewLine ( this . outputStream ) ;
this . headersWritten = true ;
}
}
protected byte [ ] getAsciiBytes ( String name ) {
private byte [ ] getAsciiBytes ( String name ) {
try {
return name . getBytes ( "US-ASCII" ) ;
}
catch ( UnsupportedEncodingException ex ) {
// should not happen, US-ASCII is always supported
// Should not happen - US-ASCII is always supported.
throw new IllegalStateException ( ex ) ;
}
}