Use the configured charset for part headers
This comment extends the use of the charset property in FormHttpMessageConverter to also include multipart headers with a default of UTF-8. We now also set the charset parameter of the "Content-Type" header to indicate to the server side how to decode correctly. Issue: SPR-15205
This commit is contained in:
parent
bda2723933
commit
75117f42b8
|
|
@ -25,6 +25,7 @@ import java.nio.charset.Charset;
|
|||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
|
@ -148,10 +149,17 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
|
|||
/**
|
||||
* 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". As of 4.3, it will also be used as
|
||||
* the default charset for the conversion of text bodies in a multipart request.
|
||||
* In contrast to this, {@link #setMultipartCharset} only affects the encoding of
|
||||
* <i>file names</i> in a multipart request according to the encoded-word syntax.
|
||||
*
|
||||
* <p>As of 4.3, this is also used as the default charset for the conversion
|
||||
* of text bodies in a multipart request.
|
||||
*
|
||||
* <p>As of 5.0 this is also used for part headers including
|
||||
* "Content-Disposition" (and its filename parameter) unless (the mutually
|
||||
* exclusive) {@link #setMultipartCharset} is also set, in which case part
|
||||
* headers are encoded as ASCII and <i>filename</i> is encoded with the
|
||||
* "encoded-word" syntax from RFC 2047.
|
||||
*
|
||||
* <p>By default this is set to "UTF-8".
|
||||
*/
|
||||
public void setCharset(Charset charset) {
|
||||
if (charset != this.charset) {
|
||||
|
|
@ -177,9 +185,13 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
|
|||
|
||||
/**
|
||||
* Set the character set to use when writing multipart data to encode file
|
||||
* names. Encoding is based on the encoded-word syntax defined in RFC 2047
|
||||
* names. Encoding is based on the "encoded-word" syntax defined in RFC 2047
|
||||
* and relies on {@code MimeUtility} from "javax.mail".
|
||||
* <p>If not set file names will be encoded as US-ASCII.
|
||||
*
|
||||
* <p>As of 5.0 by default part headers, including Content-Disposition (and
|
||||
* its filename parameter) will be encoded based on the setting of
|
||||
* {@link #setCharset(Charset)} or {@code UTF-8} by default.
|
||||
*
|
||||
* @since 4.1.1
|
||||
* @see <a href="http://en.wikipedia.org/wiki/MIME#Encoded-Word">Encoded-Word</a>
|
||||
*/
|
||||
|
|
@ -322,7 +334,11 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
|
|||
|
||||
private void writeMultipart(final MultiValueMap<String, Object> parts, HttpOutputMessage outputMessage) throws IOException {
|
||||
final byte[] boundary = generateMultipartBoundary();
|
||||
Map<String, String> parameters = Collections.singletonMap("boundary", new String(boundary, "US-ASCII"));
|
||||
Map<String, String> parameters = new HashMap<>(2);
|
||||
parameters.put("boundary", new String(boundary, "US-ASCII"));
|
||||
if (!isFilenameCharsetSet()) {
|
||||
parameters.put("charset", this.charset.name());
|
||||
}
|
||||
|
||||
MediaType contentType = new MediaType(MediaType.MULTIPART_FORM_DATA, parameters);
|
||||
HttpHeaders headers = outputMessage.getHeaders();
|
||||
|
|
@ -344,6 +360,15 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When {@link #setMultipartCharset(Charset)} is configured (i.e. RFC 2047,
|
||||
* "encoded-word" syntax) we need to use ASCII for part headers or otherwise
|
||||
* we encode directly using the configured {@link #setCharset(Charset)}.
|
||||
*/
|
||||
private boolean isFilenameCharsetSet() {
|
||||
return this.multipartCharset != null;
|
||||
}
|
||||
|
||||
private void writeParts(OutputStream os, MultiValueMap<String, Object> parts, byte[] boundary) throws IOException {
|
||||
for (Map.Entry<String, List<Object>> entry : parts.entrySet()) {
|
||||
String name = entry.getKey();
|
||||
|
|
@ -365,7 +390,8 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
|
|||
MediaType partContentType = partHeaders.getContentType();
|
||||
for (HttpMessageConverter<?> messageConverter : this.partConverters) {
|
||||
if (messageConverter.canWrite(partType, partContentType)) {
|
||||
HttpOutputMessage multipartMessage = new MultipartHttpOutputMessage(os);
|
||||
Charset charset = isFilenameCharsetSet() ? StandardCharsets.US_ASCII : this.charset;
|
||||
HttpOutputMessage multipartMessage = new MultipartHttpOutputMessage(os, charset);
|
||||
multipartMessage.getHeaders().setContentDispositionFormData(name, getFilename(partBody));
|
||||
if (!partHeaders.isEmpty()) {
|
||||
multipartMessage.getHeaders().putAll(partHeaders);
|
||||
|
|
@ -378,7 +404,6 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
|
|||
"found for request type [" + partType.getName() + "]");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generate a multipart boundary.
|
||||
* <p>This implementation delegates to
|
||||
|
|
@ -451,12 +476,15 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
|
|||
|
||||
private final OutputStream outputStream;
|
||||
|
||||
private final Charset charset;
|
||||
|
||||
private final HttpHeaders headers = new HttpHeaders();
|
||||
|
||||
private boolean headersWritten = false;
|
||||
|
||||
public MultipartHttpOutputMessage(OutputStream outputStream) {
|
||||
public MultipartHttpOutputMessage(OutputStream outputStream, Charset charset) {
|
||||
this.outputStream = outputStream;
|
||||
this.charset = charset;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -473,9 +501,9 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
|
|||
private void writeHeaders() throws IOException {
|
||||
if (!this.headersWritten) {
|
||||
for (Map.Entry<String, List<String>> entry : this.headers.entrySet()) {
|
||||
byte[] headerName = getAsciiBytes(entry.getKey());
|
||||
byte[] headerName = getBytes(entry.getKey());
|
||||
for (String headerValueString : entry.getValue()) {
|
||||
byte[] headerValue = getAsciiBytes(headerValueString);
|
||||
byte[] headerValue = getBytes(headerValueString);
|
||||
this.outputStream.write(headerName);
|
||||
this.outputStream.write(':');
|
||||
this.outputStream.write(' ');
|
||||
|
|
@ -488,8 +516,8 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
|
|||
}
|
||||
}
|
||||
|
||||
private byte[] getAsciiBytes(String name) {
|
||||
return name.getBytes(StandardCharsets.US_ASCII);
|
||||
private byte[] getBytes(String name) {
|
||||
return name.getBytes(this.charset);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -43,11 +43,17 @@ import org.springframework.http.converter.support.AllEncompassingFormHttpMessage
|
|||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.*;
|
||||
import static org.hamcrest.CoreMatchers.allOf;
|
||||
import static org.hamcrest.CoreMatchers.endsWith;
|
||||
import static org.hamcrest.CoreMatchers.startsWith;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.BDDMockito.*;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.BDDMockito.never;
|
||||
import static org.mockito.BDDMockito.verify;
|
||||
|
||||
/**
|
||||
* @author Arjen Poutsma
|
||||
|
|
@ -138,7 +144,6 @@ public class FormHttpMessageConverterTests {
|
|||
parts.add("xml", entity);
|
||||
|
||||
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
|
||||
this.converter.setMultipartCharset(StandardCharsets.UTF_8);
|
||||
this.converter.write(parts, new MediaType("multipart", "form-data", StandardCharsets.UTF_8), outputMessage);
|
||||
|
||||
final MediaType contentType = outputMessage.getHeaders().getContentType();
|
||||
|
|
|
|||
Loading…
Reference in New Issue