Skip to content

Commit 1a9e42b

Browse files
committed
Support multipart filename with charset
StandardMultipartHttpServletRequest now supports filenames with charset information. Issue: SPR-13319
1 parent 4314da9 commit 1a9e42b

File tree

2 files changed

+94
-12
lines changed

2 files changed

+94
-12
lines changed

spring-web/src/main/java/org/springframework/web/multipart/support/StandardMultipartHttpServletRequest.java

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.io.IOException;
2121
import java.io.InputStream;
2222
import java.io.Serializable;
23+
import java.nio.charset.Charset;
2324
import java.util.ArrayList;
2425
import java.util.Collection;
2526
import java.util.Collections;
@@ -44,6 +45,7 @@
4445
* methods - without any custom processing on our side.
4546
*
4647
* @author Juergen Hoeller
48+
* @author Rossen Stoyanchev
4749
* @since 3.1
4850
*/
4951
public class StandardMultipartHttpServletRequest extends AbstractMultipartHttpServletRequest {
@@ -52,6 +54,11 @@ public class StandardMultipartHttpServletRequest extends AbstractMultipartHttpSe
5254

5355
private static final String FILENAME_KEY = "filename=";
5456

57+
private static final String FILENAME_WITH_CHARSET_KEY = "filename*=";
58+
59+
private static final Charset US_ASCII = Charset.forName("us-ascii");
60+
61+
5562
private Set<String> multipartParameterNames;
5663

5764

@@ -86,7 +93,11 @@ private void parseRequest(HttpServletRequest request) {
8693
this.multipartParameterNames = new LinkedHashSet<String>(parts.size());
8794
MultiValueMap<String, MultipartFile> files = new LinkedMultiValueMap<String, MultipartFile>(parts.size());
8895
for (Part part : parts) {
89-
String filename = extractFilename(part.getHeader(CONTENT_DISPOSITION));
96+
String disposition = part.getHeader(CONTENT_DISPOSITION);
97+
String filename = extractFilename(disposition);
98+
if (filename == null) {
99+
filename = extractFilenameWithCharset(disposition);
100+
}
90101
if (filename != null) {
91102
files.add(part.getName(), new StandardMultipartFile(part, filename));
92103
}
@@ -102,15 +113,18 @@ private void parseRequest(HttpServletRequest request) {
102113
}
103114

104115
private String extractFilename(String contentDisposition) {
116+
return extractFilename(contentDisposition, FILENAME_KEY);
117+
}
118+
119+
private String extractFilename(String contentDisposition, String key) {
105120
if (contentDisposition == null) {
106121
return null;
107122
}
108-
// TODO: can only handle the typical case at the moment
109-
int startIndex = contentDisposition.indexOf(FILENAME_KEY);
123+
int startIndex = contentDisposition.indexOf(key);
110124
if (startIndex == -1) {
111125
return null;
112126
}
113-
String filename = contentDisposition.substring(startIndex + FILENAME_KEY.length());
127+
String filename = contentDisposition.substring(startIndex + key.length());
114128
if (filename.startsWith("\"")) {
115129
int endIndex = filename.indexOf("\"", 1);
116130
if (endIndex != -1) {
@@ -126,6 +140,33 @@ private String extractFilename(String contentDisposition) {
126140
return filename;
127141
}
128142

143+
private String extractFilenameWithCharset(String contentDisposition) {
144+
String filename = extractFilename(contentDisposition, FILENAME_WITH_CHARSET_KEY);
145+
if (filename == null) {
146+
return null;
147+
}
148+
int index = filename.indexOf("'");
149+
if (index != -1) {
150+
Charset charset = null;
151+
try {
152+
charset = Charset.forName(filename.substring(0, index));
153+
}
154+
catch (IllegalArgumentException ex) {
155+
// ignore
156+
}
157+
filename = filename.substring(index + 1);
158+
// Skip language information..
159+
index = filename.indexOf("'");
160+
if (index != -1) {
161+
filename = filename.substring(index + 1);
162+
}
163+
if (charset != null) {
164+
filename = new String(filename.getBytes(US_ASCII), charset);
165+
}
166+
}
167+
return filename;
168+
}
169+
129170

130171
@Override
131172
protected void initializeMultipart() {

spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartIntegrationTests.java

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,16 @@
1919
import java.net.URI;
2020
import java.nio.charset.Charset;
2121
import java.util.ArrayList;
22-
import java.util.Arrays;
22+
import java.util.Collections;
2323
import java.util.List;
24-
24+
import java.util.Map;
2525
import javax.servlet.MultipartConfigElement;
2626

2727
import org.eclipse.jetty.server.Connector;
2828
import org.eclipse.jetty.server.NetworkConnector;
2929
import org.eclipse.jetty.server.Server;
3030
import org.eclipse.jetty.servlet.ServletContextHandler;
3131
import org.eclipse.jetty.servlet.ServletHolder;
32-
3332
import org.junit.AfterClass;
3433
import org.junit.Assert;
3534
import org.junit.Before;
@@ -43,6 +42,7 @@
4342
import org.springframework.http.HttpHeaders;
4443
import org.springframework.http.HttpStatus;
4544
import org.springframework.http.MediaType;
45+
import org.springframework.http.RequestEntity;
4646
import org.springframework.http.ResponseEntity;
4747
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
4848
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
@@ -52,9 +52,9 @@
5252
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
5353
import org.springframework.stereotype.Controller;
5454
import org.springframework.util.LinkedMultiValueMap;
55+
import org.springframework.util.MimeTypeUtils;
5556
import org.springframework.util.MultiValueMap;
5657
import org.springframework.web.bind.annotation.RequestMapping;
57-
import org.springframework.web.bind.annotation.RequestMethod;
5858
import org.springframework.web.bind.annotation.RequestPart;
5959
import org.springframework.web.client.RestTemplate;
6060
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
@@ -66,7 +66,8 @@
6666
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
6767
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
6868

69-
import static org.junit.Assert.*;
69+
import static org.junit.Assert.assertEquals;
70+
import static org.springframework.web.bind.annotation.RequestMethod.POST;
7071

7172
/**
7273
* Test access to parts of a multipart request with {@link RequestPart}.
@@ -117,7 +118,7 @@ public static void startServer() throws Exception {
117118
@Before
118119
public void setUp() {
119120
ByteArrayHttpMessageConverter emptyBodyConverter = new ByteArrayHttpMessageConverter();
120-
emptyBodyConverter.setSupportedMediaTypes(Arrays.asList(MediaType.APPLICATION_JSON));
121+
emptyBodyConverter.setSupportedMediaTypes(Collections.singletonList(MediaType.APPLICATION_JSON));
121122

122123
List<HttpMessageConverter<?>> converters = new ArrayList<>(3);
123124
converters.add(emptyBodyConverter);
@@ -129,7 +130,7 @@ public void setUp() {
129130
converter.setPartConverters(converters);
130131

131132
restTemplate = new RestTemplate(new HttpComponentsClientHttpRequestFactory());
132-
restTemplate.setMessageConverters(Arrays.<HttpMessageConverter<?>>asList(converter));
133+
restTemplate.setMessageConverters(Collections.singletonList(converter));
133134
}
134135

135136
@AfterClass
@@ -150,6 +151,37 @@ public void standardMultipartResolver() throws Exception {
150151
testCreate(baseUrl + "/standard-resolver/test");
151152
}
152153

154+
// SPR-13319
155+
156+
@Test
157+
public void standardMultipartResolverWithEncodedFileName() throws Exception {
158+
159+
byte[] boundary = MimeTypeUtils.generateMultipartBoundary();
160+
String boundaryText = new String(boundary, "US-ASCII");
161+
Map<String, String> params = Collections.singletonMap("boundary", boundaryText);
162+
163+
String content =
164+
"--" + boundaryText + "\n" +
165+
"Content-Disposition: form-data; name=\"file\"; filename*=\"utf-8''%C3%A9l%C3%A8ve.txt\"\n" +
166+
"Content-Type: text/plain\n" +
167+
"Content-Length: 7\n" +
168+
"\n" +
169+
"content\n" +
170+
"--" + boundaryText + "--";
171+
172+
RequestEntity<byte[]> requestEntity =
173+
RequestEntity.post(new URI(baseUrl + "/standard-resolver/spr13319"))
174+
.contentType(new MediaType(MediaType.MULTIPART_FORM_DATA, params))
175+
.body(content.getBytes(Charset.forName("us-ascii")));
176+
177+
ByteArrayHttpMessageConverter converter = new ByteArrayHttpMessageConverter();
178+
converter.setSupportedMediaTypes(Collections.singletonList(MediaType.MULTIPART_FORM_DATA));
179+
this.restTemplate.setMessageConverters(Collections.singletonList(converter));
180+
181+
ResponseEntity<Void> responseEntity = restTemplate.exchange(requestEntity, Void.class);
182+
assertEquals(HttpStatus.OK, responseEntity.getStatusCode());
183+
}
184+
153185
private void testCreate(String url) {
154186
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<String, Object>();
155187
parts.add("json-data", new HttpEntity<TestData>(new TestData("Jason")));
@@ -176,6 +208,7 @@ public RequestPartTestController controller() {
176208
}
177209

178210
@Configuration
211+
@SuppressWarnings("unused")
179212
static class CommonsMultipartResolverTestConfig extends RequestPartTestConfig {
180213

181214
@Bean
@@ -185,6 +218,7 @@ public MultipartResolver multipartResolver() {
185218
}
186219

187220
@Configuration
221+
@SuppressWarnings("unused")
188222
static class StandardMultipartResolverTestConfig extends RequestPartTestConfig {
189223

190224
@Bean
@@ -194,9 +228,10 @@ public MultipartResolver multipartResolver() {
194228
}
195229

196230
@Controller
231+
@SuppressWarnings("unused")
197232
private static class RequestPartTestController {
198233

199-
@RequestMapping(value = "/test", method = RequestMethod.POST, consumes = { "multipart/mixed", "multipart/form-data" })
234+
@RequestMapping(value = "/test", method = POST, consumes = { "multipart/mixed", "multipart/form-data" })
200235
public ResponseEntity<Object> create(@RequestPart(name = "json-data") TestData testData,
201236
@RequestPart("file-data") MultipartFile file,
202237
@RequestPart(name = "empty-data", required = false) TestData emptyData,
@@ -209,6 +244,12 @@ public ResponseEntity<Object> create(@RequestPart(name = "json-data") TestData t
209244
headers.setLocation(URI.create(url));
210245
return new ResponseEntity<Object>(headers, HttpStatus.CREATED);
211246
}
247+
248+
@RequestMapping(value = "/spr13319", method = POST, consumes = "multipart/form-data")
249+
public ResponseEntity<Void> create(@RequestPart("file") MultipartFile multipartFile) {
250+
assertEquals("%C3%A9l%C3%A8ve.txt", multipartFile.getOriginalFilename());
251+
return ResponseEntity.ok().build();
252+
}
212253
}
213254

214255
@SuppressWarnings("unused")

0 commit comments

Comments
 (0)