Skip to content

Commit bb74457

Browse files
committed
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.
1 parent a56f735 commit bb74457

File tree

4 files changed

+29
-56
lines changed

4 files changed

+29
-56
lines changed

build.gradle

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -838,7 +838,6 @@ project("spring-webflux") {
838838
testRuntime("org.python:jython-standalone:2.5.3")
839839
testRuntime("org.webjars:underscorejs:1.8.3")
840840
testRuntime("org.glassfish:javax.el:3.0.1-b08")
841-
testRuntime("com.sun.mail:javax.mail:${javamailVersion}")
842841
testRuntime("com.sun.xml.bind:jaxb-core:${jaxbVersion}")
843842
testRuntime("com.sun.xml.bind:jaxb-impl:${jaxbVersion}")
844843
testRuntime("org.synchronoss.cloud:nio-multipart-parser:${niomultipartVersion}")

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

Lines changed: 24 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,17 @@
1616

1717
package org.springframework.http.codec.multipart;
1818

19-
import java.io.UnsupportedEncodingException;
2019
import java.nio.charset.Charset;
2120
import java.nio.charset.StandardCharsets;
2221
import java.util.Arrays;
2322
import java.util.Collections;
23+
import java.util.HashMap;
2424
import java.util.List;
2525
import java.util.Map;
2626
import java.util.Optional;
2727
import java.util.concurrent.atomic.AtomicBoolean;
2828
import java.util.function.Supplier;
2929
import java.util.stream.Collectors;
30-
import javax.mail.internet.MimeUtility;
3130

3231
import org.reactivestreams.Publisher;
3332
import reactor.core.publisher.Flux;
@@ -69,7 +68,7 @@ public class MultipartHttpMessageWriter implements HttpMessageWriter<MultiValueM
6968

7069
private final List<HttpMessageWriter<?>> partWriters;
7170

72-
private Charset filenameCharset = DEFAULT_CHARSET;
71+
private Charset charset = DEFAULT_CHARSET;
7372

7473
private final DataBufferFactory bufferFactory = new DefaultDataBufferFactory();
7574

@@ -86,19 +85,20 @@ public MultipartHttpMessageWriter(List<HttpMessageWriter<?>> partWriters) {
8685
}
8786

8887
/**
89-
* Set the character set to use for writing file names in the multipart request.
88+
* Set the character set to use for part headers such as
89+
* "Content-Disposition" (and its filename parameter).
9090
* <p>By default this is set to "UTF-8".
9191
*/
92-
public void setFilenameCharset(Charset charset) {
92+
public void setCharset(Charset charset) {
9393
Assert.notNull(charset, "'charset' must not be null");
94-
this.filenameCharset = charset;
94+
this.charset = charset;
9595
}
9696

9797
/**
98-
* Return the configured filename charset.
98+
* Return the configured charset for part headers.
9999
*/
100-
public Charset getFilenameCharset() {
101-
return this.filenameCharset;
100+
public Charset getCharset() {
101+
return this.charset;
102102
}
103103

104104

@@ -120,8 +120,10 @@ public Mono<Void> write(Publisher<? extends MultiValueMap<String, ?>> inputStrea
120120

121121
byte[] boundary = generateMultipartBoundary();
122122

123-
outputMessage.getHeaders().setContentType(new MediaType("multipart", "form-data",
124-
Collections.singletonMap("boundary", new String(boundary, StandardCharsets.US_ASCII))));
123+
Map<String, String> params = new HashMap<>(2);
124+
params.put("boundary", new String(boundary, StandardCharsets.US_ASCII));
125+
params.put("charset", getCharset().name());
126+
outputMessage.getHeaders().setContentType(new MediaType(MediaType.MULTIPART_FORM_DATA, params));
125127

126128
return Mono.from(inputStream).flatMap(map -> {
127129

@@ -149,7 +151,8 @@ private Flux<DataBuffer> encodePartValues(byte[] boundary, String name, List<?>
149151
@SuppressWarnings("unchecked")
150152
private <T> Flux<DataBuffer> encodePart(byte[] boundary, String name, T value) {
151153

152-
MultipartHttpOutputMessage outputMessage = new MultipartHttpOutputMessage(this.bufferFactory);
154+
MultipartHttpOutputMessage outputMessage =
155+
new MultipartHttpOutputMessage(this.bufferFactory, getCharset());
153156

154157
T body;
155158
if (value instanceof HttpEntity) {
@@ -160,9 +163,10 @@ private <T> Flux<DataBuffer> encodePart(byte[] boundary, String name, T value) {
160163
body = value;
161164
}
162165

163-
ResolvableType bodyType = ResolvableType.forClass(body.getClass());
164-
outputMessage.getHeaders().setContentDispositionFormData(name, getFilename(body));
166+
String filename = (body instanceof Resource ? ((Resource) body).getFilename() : null);
167+
outputMessage.getHeaders().setContentDispositionFormData(name, filename);
165168

169+
ResolvableType bodyType = ResolvableType.forClass(body.getClass());
166170
MediaType contentType = outputMessage.getHeaders().getContentType();
167171

168172
Optional<HttpMessageWriter<?>> writer = this.partWriters.stream()
@@ -189,26 +193,6 @@ private <T> Flux<DataBuffer> encodePart(byte[] boundary, String name, T value) {
189193
);
190194
}
191195

192-
/**
193-
* Return the filename of the given multipart part. This value will be used
194-
* for the {@code Content-Disposition} header.
195-
* <p>The default implementation returns {@link Resource#getFilename()} if
196-
* the part is a {@code Resource}, and {@code null} in other cases.
197-
* @param part the part for which return a file name
198-
* @return the filename or {@code null}
199-
*/
200-
protected String getFilename(Object part) {
201-
if (part instanceof Resource) {
202-
Resource resource = (Resource) part;
203-
String filename = resource.getFilename();
204-
filename = MimeDelegate.encode(filename, this.filenameCharset.name());
205-
return filename;
206-
}
207-
else {
208-
return null;
209-
}
210-
}
211-
212196
private DataBuffer generateBoundaryLine(byte[] boundary) {
213197
DataBuffer buffer = this.bufferFactory.allocateBuffer(boundary.length + 4);
214198
buffer.write((byte)'-');
@@ -243,15 +227,18 @@ private static class MultipartHttpOutputMessage implements ReactiveHttpOutputMes
243227

244228
private final DataBufferFactory bufferFactory;
245229

230+
private final Charset charset;
231+
246232
private final HttpHeaders headers = new HttpHeaders();
247233

248234
private final AtomicBoolean commited = new AtomicBoolean();
249235

250236
private Flux<DataBuffer> body;
251237

252238

253-
public MultipartHttpOutputMessage(DataBufferFactory bufferFactory) {
239+
public MultipartHttpOutputMessage(DataBufferFactory bufferFactory, Charset charset) {
254240
this.bufferFactory = bufferFactory;
241+
this.charset = charset;
255242
}
256243

257244

@@ -287,9 +274,9 @@ public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
287274
private DataBuffer generateHeaders() {
288275
DataBuffer buffer = this.bufferFactory.allocateBuffer();
289276
for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
290-
byte[] headerName = entry.getKey().getBytes(StandardCharsets.US_ASCII);
277+
byte[] headerName = entry.getKey().getBytes(this.charset);
291278
for (String headerValueString : entry.getValue()) {
292-
byte[] headerValue = headerValueString.getBytes(StandardCharsets.US_ASCII);
279+
byte[] headerValue = headerValueString.getBytes(this.charset);
293280
buffer.write(headerName);
294281
buffer.write((byte)':');
295282
buffer.write((byte)' ');
@@ -321,19 +308,4 @@ public Mono<Void> setComplete() {
321308

322309
}
323310

324-
/**
325-
* Inner class to avoid a hard dependency on the JavaMail API.
326-
*/
327-
private static class MimeDelegate {
328-
329-
public static String encode(String value, String charset) {
330-
try {
331-
return MimeUtility.encodeText(value, charset, null);
332-
}
333-
catch (UnsupportedEncodingException ex) {
334-
throw new IllegalStateException(ex);
335-
}
336-
}
337-
}
338-
339311
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,9 @@ protected StreamStorage getStorage() {
282282
private static class SynchronossFilePart extends DefaultSynchronossPart implements FilePart {
283283

284284

285-
public SynchronossFilePart(HttpHeaders headers, StreamStorage storage, String fileName, DataBufferFactory factory) {
285+
public SynchronossFilePart(HttpHeaders headers, StreamStorage storage,
286+
String fileName, DataBufferFactory factory) {
287+
286288
super(headers, storage, factory);
287289
}
288290

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,15 +101,15 @@ public String getFilename() {
101101
Map<String, Object> hints = Collections.emptyMap();
102102
this.writer.write(Mono.just(map), null, MediaType.MULTIPART_FORM_DATA, response, hints).block();
103103

104-
final MediaType contentType = response.getHeaders().getContentType();
104+
MediaType contentType = response.getHeaders().getContentType();
105105
assertNotNull("No boundary found", contentType.getParameter("boundary"));
106106

107107
// see if Synchronoss NIO Multipart can read what we wrote
108108
SynchronossPartHttpMessageReader synchronossReader = new SynchronossPartHttpMessageReader();
109109
MultipartHttpMessageReader reader = new MultipartHttpMessageReader(synchronossReader);
110110

111111
MockServerHttpRequest request = MockServerHttpRequest.post("/foo")
112-
.header(HttpHeaders.CONTENT_TYPE, contentType.toString())
112+
.contentType(MediaType.parseMediaType(contentType.toString()))
113113
.body(response.getBody());
114114

115115
ResolvableType elementType = ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, Part.class);

0 commit comments

Comments
 (0)