Browse Source

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.
pull/1418/head
Rossen Stoyanchev 9 years ago
parent
commit
bb744574e5
  1. 1
      build.gradle
  2. 76
      spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartHttpMessageWriter.java
  3. 4
      spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java
  4. 4
      spring-web/src/test/java/org/springframework/http/codec/multipart/MultipartHttpMessageWriterTests.java

1
build.gradle

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

76
spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartHttpMessageWriter.java

@ -16,18 +16,17 @@
package org.springframework.http.codec.multipart; package org.springframework.http.codec.multipart;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.mail.internet.MimeUtility;
import org.reactivestreams.Publisher; import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
@ -69,7 +68,7 @@ public class MultipartHttpMessageWriter implements HttpMessageWriter<MultiValueM
private final List<HttpMessageWriter<?>> partWriters; private final List<HttpMessageWriter<?>> partWriters;
private Charset filenameCharset = DEFAULT_CHARSET; private Charset charset = DEFAULT_CHARSET;
private final DataBufferFactory bufferFactory = new DefaultDataBufferFactory(); 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". * <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"); 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() { public Charset getCharset() {
return this.filenameCharset; return this.charset;
} }
@ -120,8 +120,10 @@ public class MultipartHttpMessageWriter implements HttpMessageWriter<MultiValueM
byte[] boundary = generateMultipartBoundary(); byte[] boundary = generateMultipartBoundary();
outputMessage.getHeaders().setContentType(new MediaType("multipart", "form-data", Map<String, String> params = new HashMap<>(2);
Collections.singletonMap("boundary", new String(boundary, StandardCharsets.US_ASCII)))); 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 -> { return Mono.from(inputStream).flatMap(map -> {
@ -149,7 +151,8 @@ public class MultipartHttpMessageWriter implements HttpMessageWriter<MultiValueM
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private <T> Flux<DataBuffer> encodePart(byte[] boundary, String name, T value) { 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; T body;
if (value instanceof HttpEntity) { if (value instanceof HttpEntity) {
@ -160,9 +163,10 @@ public class MultipartHttpMessageWriter implements HttpMessageWriter<MultiValueM
body = value; body = value;
} }
ResolvableType bodyType = ResolvableType.forClass(body.getClass()); String filename = (body instanceof Resource ? ((Resource) body).getFilename() : null);
outputMessage.getHeaders().setContentDispositionFormData(name, getFilename(body)); outputMessage.getHeaders().setContentDispositionFormData(name, filename);
ResolvableType bodyType = ResolvableType.forClass(body.getClass());
MediaType contentType = outputMessage.getHeaders().getContentType(); MediaType contentType = outputMessage.getHeaders().getContentType();
Optional<HttpMessageWriter<?>> writer = this.partWriters.stream() 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) { private DataBuffer generateBoundaryLine(byte[] boundary) {
DataBuffer buffer = this.bufferFactory.allocateBuffer(boundary.length + 4); DataBuffer buffer = this.bufferFactory.allocateBuffer(boundary.length + 4);
buffer.write((byte)'-'); buffer.write((byte)'-');
@ -243,6 +227,8 @@ public class MultipartHttpMessageWriter implements HttpMessageWriter<MultiValueM
private final DataBufferFactory bufferFactory; private final DataBufferFactory bufferFactory;
private final Charset charset;
private final HttpHeaders headers = new HttpHeaders(); private final HttpHeaders headers = new HttpHeaders();
private final AtomicBoolean commited = new AtomicBoolean(); private final AtomicBoolean commited = new AtomicBoolean();
@ -250,8 +236,9 @@ public class MultipartHttpMessageWriter implements HttpMessageWriter<MultiValueM
private Flux<DataBuffer> body; private Flux<DataBuffer> body;
public MultipartHttpOutputMessage(DataBufferFactory bufferFactory) { public MultipartHttpOutputMessage(DataBufferFactory bufferFactory, Charset charset) {
this.bufferFactory = bufferFactory; this.bufferFactory = bufferFactory;
this.charset = charset;
} }
@ -287,9 +274,9 @@ public class MultipartHttpMessageWriter implements HttpMessageWriter<MultiValueM
private DataBuffer generateHeaders() { private DataBuffer generateHeaders() {
DataBuffer buffer = this.bufferFactory.allocateBuffer(); DataBuffer buffer = this.bufferFactory.allocateBuffer();
for (Map.Entry<String, List<String>> entry : headers.entrySet()) { 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()) { for (String headerValueString : entry.getValue()) {
byte[] headerValue = headerValueString.getBytes(StandardCharsets.US_ASCII); byte[] headerValue = headerValueString.getBytes(this.charset);
buffer.write(headerName); buffer.write(headerName);
buffer.write((byte)':'); buffer.write((byte)':');
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);
}
}
}
} }

4
spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java

@ -282,7 +282,9 @@ public class SynchronossPartHttpMessageReader implements HttpMessageReader<Part>
private static class SynchronossFilePart extends DefaultSynchronossPart implements FilePart { 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); super(headers, storage, factory);
} }

4
spring-web/src/test/java/org/springframework/http/codec/multipart/MultipartHttpMessageWriterTests.java

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

Loading…
Cancel
Save