Remove optional javax.mail dependency from WebFlux

The MultipartHttpMessageWriter now directly encodes part header values
defaulting to UTF-8 and also specifies the charset in the
Content-Type header for the entire request.

This should work with something commonly used like Apache Commons
FileUpload which checks request.getCharacterEncoding() and uses it
for reading headers.
This commit is contained in:
Rossen Stoyanchev 2017-05-04 13:04:04 -04:00
parent a56f735edd
commit bb744574e5
4 changed files with 29 additions and 56 deletions

View File

@ -838,7 +838,6 @@ project("spring-webflux") {
testRuntime("org.python:jython-standalone:2.5.3")
testRuntime("org.webjars:underscorejs:1.8.3")
testRuntime("org.glassfish:javax.el:3.0.1-b08")
testRuntime("com.sun.mail:javax.mail:${javamailVersion}")
testRuntime("com.sun.xml.bind:jaxb-core:${jaxbVersion}")
testRuntime("com.sun.xml.bind:jaxb-impl:${jaxbVersion}")
testRuntime("org.synchronoss.cloud:nio-multipart-parser:${niomultipartVersion}")

View File

@ -16,18 +16,17 @@
package org.springframework.http.codec.multipart;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import javax.mail.internet.MimeUtility;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
@ -69,7 +68,7 @@ public class MultipartHttpMessageWriter implements HttpMessageWriter<MultiValueM
private final List<HttpMessageWriter<?>> partWriters;
private Charset filenameCharset = DEFAULT_CHARSET;
private Charset charset = DEFAULT_CHARSET;
private final DataBufferFactory bufferFactory = new DefaultDataBufferFactory();
@ -86,19 +85,20 @@ public class MultipartHttpMessageWriter implements HttpMessageWriter<MultiValueM
}
/**
* Set the character set to use for writing file names in the multipart request.
* Set the character set to use for part headers such as
* "Content-Disposition" (and its filename parameter).
* <p>By default this is set to "UTF-8".
*/
public void setFilenameCharset(Charset charset) {
public void setCharset(Charset charset) {
Assert.notNull(charset, "'charset' must not be null");
this.filenameCharset = charset;
this.charset = charset;
}
/**
* Return the configured filename charset.
* Return the configured charset for part headers.
*/
public Charset getFilenameCharset() {
return this.filenameCharset;
public Charset getCharset() {
return this.charset;
}
@ -120,8 +120,10 @@ public class MultipartHttpMessageWriter implements HttpMessageWriter<MultiValueM
byte[] boundary = generateMultipartBoundary();
outputMessage.getHeaders().setContentType(new MediaType("multipart", "form-data",
Collections.singletonMap("boundary", new String(boundary, StandardCharsets.US_ASCII))));
Map<String, String> params = new HashMap<>(2);
params.put("boundary", new String(boundary, StandardCharsets.US_ASCII));
params.put("charset", getCharset().name());
outputMessage.getHeaders().setContentType(new MediaType(MediaType.MULTIPART_FORM_DATA, params));
return Mono.from(inputStream).flatMap(map -> {
@ -149,7 +151,8 @@ public class MultipartHttpMessageWriter implements HttpMessageWriter<MultiValueM
@SuppressWarnings("unchecked")
private <T> Flux<DataBuffer> encodePart(byte[] boundary, String name, T value) {
MultipartHttpOutputMessage outputMessage = new MultipartHttpOutputMessage(this.bufferFactory);
MultipartHttpOutputMessage outputMessage =
new MultipartHttpOutputMessage(this.bufferFactory, getCharset());
T body;
if (value instanceof HttpEntity) {
@ -160,9 +163,10 @@ public class MultipartHttpMessageWriter implements HttpMessageWriter<MultiValueM
body = value;
}
ResolvableType bodyType = ResolvableType.forClass(body.getClass());
outputMessage.getHeaders().setContentDispositionFormData(name, getFilename(body));
String filename = (body instanceof Resource ? ((Resource) body).getFilename() : null);
outputMessage.getHeaders().setContentDispositionFormData(name, filename);
ResolvableType bodyType = ResolvableType.forClass(body.getClass());
MediaType contentType = outputMessage.getHeaders().getContentType();
Optional<HttpMessageWriter<?>> writer = this.partWriters.stream()
@ -189,26 +193,6 @@ public class MultipartHttpMessageWriter implements HttpMessageWriter<MultiValueM
);
}
/**
* Return the filename of the given multipart part. This value will be used
* for the {@code Content-Disposition} header.
* <p>The default implementation returns {@link Resource#getFilename()} if
* the part is a {@code Resource}, and {@code null} in other cases.
* @param part the part for which return a file name
* @return the filename or {@code null}
*/
protected String getFilename(Object part) {
if (part instanceof Resource) {
Resource resource = (Resource) part;
String filename = resource.getFilename();
filename = MimeDelegate.encode(filename, this.filenameCharset.name());
return filename;
}
else {
return null;
}
}
private DataBuffer generateBoundaryLine(byte[] boundary) {
DataBuffer buffer = this.bufferFactory.allocateBuffer(boundary.length + 4);
buffer.write((byte)'-');
@ -243,6 +227,8 @@ public class MultipartHttpMessageWriter implements HttpMessageWriter<MultiValueM
private final DataBufferFactory bufferFactory;
private final Charset charset;
private final HttpHeaders headers = new HttpHeaders();
private final AtomicBoolean commited = new AtomicBoolean();
@ -250,8 +236,9 @@ public class MultipartHttpMessageWriter implements HttpMessageWriter<MultiValueM
private Flux<DataBuffer> body;
public MultipartHttpOutputMessage(DataBufferFactory bufferFactory) {
public MultipartHttpOutputMessage(DataBufferFactory bufferFactory, Charset charset) {
this.bufferFactory = bufferFactory;
this.charset = charset;
}
@ -287,9 +274,9 @@ public class MultipartHttpMessageWriter implements HttpMessageWriter<MultiValueM
private DataBuffer generateHeaders() {
DataBuffer buffer = this.bufferFactory.allocateBuffer();
for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
byte[] headerName = entry.getKey().getBytes(StandardCharsets.US_ASCII);
byte[] headerName = entry.getKey().getBytes(this.charset);
for (String headerValueString : entry.getValue()) {
byte[] headerValue = headerValueString.getBytes(StandardCharsets.US_ASCII);
byte[] headerValue = headerValueString.getBytes(this.charset);
buffer.write(headerName);
buffer.write((byte)':');
buffer.write((byte)' ');
@ -321,19 +308,4 @@ public class MultipartHttpMessageWriter implements HttpMessageWriter<MultiValueM
}
/**
* Inner class to avoid a hard dependency on the JavaMail API.
*/
private static class MimeDelegate {
public static String encode(String value, String charset) {
try {
return MimeUtility.encodeText(value, charset, null);
}
catch (UnsupportedEncodingException ex) {
throw new IllegalStateException(ex);
}
}
}
}

View File

@ -282,7 +282,9 @@ public class SynchronossPartHttpMessageReader implements HttpMessageReader<Part>
private static class SynchronossFilePart extends DefaultSynchronossPart implements FilePart {
public SynchronossFilePart(HttpHeaders headers, StreamStorage storage, String fileName, DataBufferFactory factory) {
public SynchronossFilePart(HttpHeaders headers, StreamStorage storage,
String fileName, DataBufferFactory factory) {
super(headers, storage, factory);
}

View File

@ -101,7 +101,7 @@ public class MultipartHttpMessageWriterTests {
Map<String, Object> hints = Collections.emptyMap();
this.writer.write(Mono.just(map), null, MediaType.MULTIPART_FORM_DATA, response, hints).block();
final MediaType contentType = response.getHeaders().getContentType();
MediaType contentType = response.getHeaders().getContentType();
assertNotNull("No boundary found", contentType.getParameter("boundary"));
// see if Synchronoss NIO Multipart can read what we wrote
@ -109,7 +109,7 @@ public class MultipartHttpMessageWriterTests {
MultipartHttpMessageReader reader = new MultipartHttpMessageReader(synchronossReader);
MockServerHttpRequest request = MockServerHttpRequest.post("/foo")
.header(HttpHeaders.CONTENT_TYPE, contentType.toString())
.contentType(MediaType.parseMediaType(contentType.toString()))
.body(response.getBody());
ResolvableType elementType = ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, Part.class);