Support multipart filename with charset
StandardMultipartHttpServletRequest now supports filenames with charset information. Issue: SPR-13319
This commit is contained in:
parent
4314da922a
commit
1a9e42b49d
|
@ -20,6 +20,7 @@ import java.io.File;
|
|||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.Serializable;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
@ -44,6 +45,7 @@ import org.springframework.web.multipart.MultipartFile;
|
|||
* methods - without any custom processing on our side.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 3.1
|
||||
*/
|
||||
public class StandardMultipartHttpServletRequest extends AbstractMultipartHttpServletRequest {
|
||||
|
@ -52,6 +54,11 @@ public class StandardMultipartHttpServletRequest extends AbstractMultipartHttpSe
|
|||
|
||||
private static final String FILENAME_KEY = "filename=";
|
||||
|
||||
private static final String FILENAME_WITH_CHARSET_KEY = "filename*=";
|
||||
|
||||
private static final Charset US_ASCII = Charset.forName("us-ascii");
|
||||
|
||||
|
||||
private Set<String> multipartParameterNames;
|
||||
|
||||
|
||||
|
@ -86,7 +93,11 @@ public class StandardMultipartHttpServletRequest extends AbstractMultipartHttpSe
|
|||
this.multipartParameterNames = new LinkedHashSet<String>(parts.size());
|
||||
MultiValueMap<String, MultipartFile> files = new LinkedMultiValueMap<String, MultipartFile>(parts.size());
|
||||
for (Part part : parts) {
|
||||
String filename = extractFilename(part.getHeader(CONTENT_DISPOSITION));
|
||||
String disposition = part.getHeader(CONTENT_DISPOSITION);
|
||||
String filename = extractFilename(disposition);
|
||||
if (filename == null) {
|
||||
filename = extractFilenameWithCharset(disposition);
|
||||
}
|
||||
if (filename != null) {
|
||||
files.add(part.getName(), new StandardMultipartFile(part, filename));
|
||||
}
|
||||
|
@ -102,15 +113,18 @@ public class StandardMultipartHttpServletRequest extends AbstractMultipartHttpSe
|
|||
}
|
||||
|
||||
private String extractFilename(String contentDisposition) {
|
||||
return extractFilename(contentDisposition, FILENAME_KEY);
|
||||
}
|
||||
|
||||
private String extractFilename(String contentDisposition, String key) {
|
||||
if (contentDisposition == null) {
|
||||
return null;
|
||||
}
|
||||
// TODO: can only handle the typical case at the moment
|
||||
int startIndex = contentDisposition.indexOf(FILENAME_KEY);
|
||||
int startIndex = contentDisposition.indexOf(key);
|
||||
if (startIndex == -1) {
|
||||
return null;
|
||||
}
|
||||
String filename = contentDisposition.substring(startIndex + FILENAME_KEY.length());
|
||||
String filename = contentDisposition.substring(startIndex + key.length());
|
||||
if (filename.startsWith("\"")) {
|
||||
int endIndex = filename.indexOf("\"", 1);
|
||||
if (endIndex != -1) {
|
||||
|
@ -126,6 +140,33 @@ public class StandardMultipartHttpServletRequest extends AbstractMultipartHttpSe
|
|||
return filename;
|
||||
}
|
||||
|
||||
private String extractFilenameWithCharset(String contentDisposition) {
|
||||
String filename = extractFilename(contentDisposition, FILENAME_WITH_CHARSET_KEY);
|
||||
if (filename == null) {
|
||||
return null;
|
||||
}
|
||||
int index = filename.indexOf("'");
|
||||
if (index != -1) {
|
||||
Charset charset = null;
|
||||
try {
|
||||
charset = Charset.forName(filename.substring(0, index));
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
// ignore
|
||||
}
|
||||
filename = filename.substring(index + 1);
|
||||
// Skip language information..
|
||||
index = filename.indexOf("'");
|
||||
if (index != -1) {
|
||||
filename = filename.substring(index + 1);
|
||||
}
|
||||
if (charset != null) {
|
||||
filename = new String(filename.getBytes(US_ASCII), charset);
|
||||
}
|
||||
}
|
||||
return filename;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void initializeMultipart() {
|
||||
|
|
|
@ -19,9 +19,9 @@ package org.springframework.web.servlet.mvc.method.annotation;
|
|||
import java.net.URI;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import java.util.Map;
|
||||
import javax.servlet.MultipartConfigElement;
|
||||
|
||||
import org.eclipse.jetty.server.Connector;
|
||||
|
@ -29,7 +29,6 @@ import org.eclipse.jetty.server.NetworkConnector;
|
|||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.servlet.ServletContextHandler;
|
||||
import org.eclipse.jetty.servlet.ServletHolder;
|
||||
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
|
@ -43,6 +42,7 @@ import org.springframework.http.HttpEntity;
|
|||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.RequestEntity;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
|
||||
|
@ -52,9 +52,9 @@ import org.springframework.http.converter.json.MappingJackson2HttpMessageConvert
|
|||
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MimeTypeUtils;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
|
||||
|
@ -66,7 +66,8 @@ import org.springframework.web.servlet.DispatcherServlet;
|
|||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.springframework.web.bind.annotation.RequestMethod.POST;
|
||||
|
||||
/**
|
||||
* Test access to parts of a multipart request with {@link RequestPart}.
|
||||
|
@ -117,7 +118,7 @@ public class RequestPartIntegrationTests {
|
|||
@Before
|
||||
public void setUp() {
|
||||
ByteArrayHttpMessageConverter emptyBodyConverter = new ByteArrayHttpMessageConverter();
|
||||
emptyBodyConverter.setSupportedMediaTypes(Arrays.asList(MediaType.APPLICATION_JSON));
|
||||
emptyBodyConverter.setSupportedMediaTypes(Collections.singletonList(MediaType.APPLICATION_JSON));
|
||||
|
||||
List<HttpMessageConverter<?>> converters = new ArrayList<>(3);
|
||||
converters.add(emptyBodyConverter);
|
||||
|
@ -129,7 +130,7 @@ public class RequestPartIntegrationTests {
|
|||
converter.setPartConverters(converters);
|
||||
|
||||
restTemplate = new RestTemplate(new HttpComponentsClientHttpRequestFactory());
|
||||
restTemplate.setMessageConverters(Arrays.<HttpMessageConverter<?>>asList(converter));
|
||||
restTemplate.setMessageConverters(Collections.singletonList(converter));
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
|
@ -150,6 +151,37 @@ public class RequestPartIntegrationTests {
|
|||
testCreate(baseUrl + "/standard-resolver/test");
|
||||
}
|
||||
|
||||
// SPR-13319
|
||||
|
||||
@Test
|
||||
public void standardMultipartResolverWithEncodedFileName() throws Exception {
|
||||
|
||||
byte[] boundary = MimeTypeUtils.generateMultipartBoundary();
|
||||
String boundaryText = new String(boundary, "US-ASCII");
|
||||
Map<String, String> params = Collections.singletonMap("boundary", boundaryText);
|
||||
|
||||
String content =
|
||||
"--" + boundaryText + "\n" +
|
||||
"Content-Disposition: form-data; name=\"file\"; filename*=\"utf-8''%C3%A9l%C3%A8ve.txt\"\n" +
|
||||
"Content-Type: text/plain\n" +
|
||||
"Content-Length: 7\n" +
|
||||
"\n" +
|
||||
"content\n" +
|
||||
"--" + boundaryText + "--";
|
||||
|
||||
RequestEntity<byte[]> requestEntity =
|
||||
RequestEntity.post(new URI(baseUrl + "/standard-resolver/spr13319"))
|
||||
.contentType(new MediaType(MediaType.MULTIPART_FORM_DATA, params))
|
||||
.body(content.getBytes(Charset.forName("us-ascii")));
|
||||
|
||||
ByteArrayHttpMessageConverter converter = new ByteArrayHttpMessageConverter();
|
||||
converter.setSupportedMediaTypes(Collections.singletonList(MediaType.MULTIPART_FORM_DATA));
|
||||
this.restTemplate.setMessageConverters(Collections.singletonList(converter));
|
||||
|
||||
ResponseEntity<Void> responseEntity = restTemplate.exchange(requestEntity, Void.class);
|
||||
assertEquals(HttpStatus.OK, responseEntity.getStatusCode());
|
||||
}
|
||||
|
||||
private void testCreate(String url) {
|
||||
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<String, Object>();
|
||||
parts.add("json-data", new HttpEntity<TestData>(new TestData("Jason")));
|
||||
|
@ -176,6 +208,7 @@ public class RequestPartIntegrationTests {
|
|||
}
|
||||
|
||||
@Configuration
|
||||
@SuppressWarnings("unused")
|
||||
static class CommonsMultipartResolverTestConfig extends RequestPartTestConfig {
|
||||
|
||||
@Bean
|
||||
|
@ -185,6 +218,7 @@ public class RequestPartIntegrationTests {
|
|||
}
|
||||
|
||||
@Configuration
|
||||
@SuppressWarnings("unused")
|
||||
static class StandardMultipartResolverTestConfig extends RequestPartTestConfig {
|
||||
|
||||
@Bean
|
||||
|
@ -194,9 +228,10 @@ public class RequestPartIntegrationTests {
|
|||
}
|
||||
|
||||
@Controller
|
||||
@SuppressWarnings("unused")
|
||||
private static class RequestPartTestController {
|
||||
|
||||
@RequestMapping(value = "/test", method = RequestMethod.POST, consumes = { "multipart/mixed", "multipart/form-data" })
|
||||
@RequestMapping(value = "/test", method = POST, consumes = { "multipart/mixed", "multipart/form-data" })
|
||||
public ResponseEntity<Object> create(@RequestPart(name = "json-data") TestData testData,
|
||||
@RequestPart("file-data") MultipartFile file,
|
||||
@RequestPart(name = "empty-data", required = false) TestData emptyData,
|
||||
|
@ -209,6 +244,12 @@ public class RequestPartIntegrationTests {
|
|||
headers.setLocation(URI.create(url));
|
||||
return new ResponseEntity<Object>(headers, HttpStatus.CREATED);
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/spr13319", method = POST, consumes = "multipart/form-data")
|
||||
public ResponseEntity<Void> create(@RequestPart("file") MultipartFile multipartFile) {
|
||||
assertEquals("%C3%A9l%C3%A8ve.txt", multipartFile.getOriginalFilename());
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
|
|
Loading…
Reference in New Issue