Support multipart filename with charset
StandardMultipartHttpServletRequest now supports filenames with charset information. Issue: SPR-13319
This commit is contained in:
@@ -20,6 +20,7 @@ import java.io.File;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@@ -44,6 +45,7 @@ import org.springframework.web.multipart.MultipartFile;
|
|||||||
* methods - without any custom processing on our side.
|
* methods - without any custom processing on our side.
|
||||||
*
|
*
|
||||||
* @author Juergen Hoeller
|
* @author Juergen Hoeller
|
||||||
|
* @author Rossen Stoyanchev
|
||||||
* @since 3.1
|
* @since 3.1
|
||||||
*/
|
*/
|
||||||
public class StandardMultipartHttpServletRequest extends AbstractMultipartHttpServletRequest {
|
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_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;
|
private Set<String> multipartParameterNames;
|
||||||
|
|
||||||
|
|
||||||
@@ -86,7 +93,11 @@ public class StandardMultipartHttpServletRequest extends AbstractMultipartHttpSe
|
|||||||
this.multipartParameterNames = new LinkedHashSet<String>(parts.size());
|
this.multipartParameterNames = new LinkedHashSet<String>(parts.size());
|
||||||
MultiValueMap<String, MultipartFile> files = new LinkedMultiValueMap<String, MultipartFile>(parts.size());
|
MultiValueMap<String, MultipartFile> files = new LinkedMultiValueMap<String, MultipartFile>(parts.size());
|
||||||
for (Part part : parts) {
|
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) {
|
if (filename != null) {
|
||||||
files.add(part.getName(), new StandardMultipartFile(part, filename));
|
files.add(part.getName(), new StandardMultipartFile(part, filename));
|
||||||
}
|
}
|
||||||
@@ -102,15 +113,18 @@ public class StandardMultipartHttpServletRequest extends AbstractMultipartHttpSe
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String extractFilename(String contentDisposition) {
|
private String extractFilename(String contentDisposition) {
|
||||||
|
return extractFilename(contentDisposition, FILENAME_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractFilename(String contentDisposition, String key) {
|
||||||
if (contentDisposition == null) {
|
if (contentDisposition == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// TODO: can only handle the typical case at the moment
|
int startIndex = contentDisposition.indexOf(key);
|
||||||
int startIndex = contentDisposition.indexOf(FILENAME_KEY);
|
|
||||||
if (startIndex == -1) {
|
if (startIndex == -1) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
String filename = contentDisposition.substring(startIndex + FILENAME_KEY.length());
|
String filename = contentDisposition.substring(startIndex + key.length());
|
||||||
if (filename.startsWith("\"")) {
|
if (filename.startsWith("\"")) {
|
||||||
int endIndex = filename.indexOf("\"", 1);
|
int endIndex = filename.indexOf("\"", 1);
|
||||||
if (endIndex != -1) {
|
if (endIndex != -1) {
|
||||||
@@ -126,6 +140,33 @@ public class StandardMultipartHttpServletRequest extends AbstractMultipartHttpSe
|
|||||||
return filename;
|
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
|
@Override
|
||||||
protected void initializeMultipart() {
|
protected void initializeMultipart() {
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ package org.springframework.web.servlet.mvc.method.annotation;
|
|||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import javax.servlet.MultipartConfigElement;
|
import javax.servlet.MultipartConfigElement;
|
||||||
|
|
||||||
import org.eclipse.jetty.server.Connector;
|
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.server.Server;
|
||||||
import org.eclipse.jetty.servlet.ServletContextHandler;
|
import org.eclipse.jetty.servlet.ServletContextHandler;
|
||||||
import org.eclipse.jetty.servlet.ServletHolder;
|
import org.eclipse.jetty.servlet.ServletHolder;
|
||||||
|
|
||||||
import org.junit.AfterClass;
|
import org.junit.AfterClass;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
@@ -43,6 +42,7 @@ import org.springframework.http.HttpEntity;
|
|||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.RequestEntity;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||||
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
|
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.http.converter.support.AllEncompassingFormHttpMessageConverter;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.util.LinkedMultiValueMap;
|
import org.springframework.util.LinkedMultiValueMap;
|
||||||
|
import org.springframework.util.MimeTypeUtils;
|
||||||
import org.springframework.util.MultiValueMap;
|
import org.springframework.util.MultiValueMap;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
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.bind.annotation.RequestPart;
|
||||||
import org.springframework.web.client.RestTemplate;
|
import org.springframework.web.client.RestTemplate;
|
||||||
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
|
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.EnableWebMvc;
|
||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
|
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}.
|
* Test access to parts of a multipart request with {@link RequestPart}.
|
||||||
@@ -117,7 +118,7 @@ public class RequestPartIntegrationTests {
|
|||||||
@Before
|
@Before
|
||||||
public void setUp() {
|
public void setUp() {
|
||||||
ByteArrayHttpMessageConverter emptyBodyConverter = new ByteArrayHttpMessageConverter();
|
ByteArrayHttpMessageConverter emptyBodyConverter = new ByteArrayHttpMessageConverter();
|
||||||
emptyBodyConverter.setSupportedMediaTypes(Arrays.asList(MediaType.APPLICATION_JSON));
|
emptyBodyConverter.setSupportedMediaTypes(Collections.singletonList(MediaType.APPLICATION_JSON));
|
||||||
|
|
||||||
List<HttpMessageConverter<?>> converters = new ArrayList<>(3);
|
List<HttpMessageConverter<?>> converters = new ArrayList<>(3);
|
||||||
converters.add(emptyBodyConverter);
|
converters.add(emptyBodyConverter);
|
||||||
@@ -129,7 +130,7 @@ public class RequestPartIntegrationTests {
|
|||||||
converter.setPartConverters(converters);
|
converter.setPartConverters(converters);
|
||||||
|
|
||||||
restTemplate = new RestTemplate(new HttpComponentsClientHttpRequestFactory());
|
restTemplate = new RestTemplate(new HttpComponentsClientHttpRequestFactory());
|
||||||
restTemplate.setMessageConverters(Arrays.<HttpMessageConverter<?>>asList(converter));
|
restTemplate.setMessageConverters(Collections.singletonList(converter));
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterClass
|
@AfterClass
|
||||||
@@ -150,6 +151,37 @@ public class RequestPartIntegrationTests {
|
|||||||
testCreate(baseUrl + "/standard-resolver/test");
|
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) {
|
private void testCreate(String url) {
|
||||||
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<String, Object>();
|
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<String, Object>();
|
||||||
parts.add("json-data", new HttpEntity<TestData>(new TestData("Jason")));
|
parts.add("json-data", new HttpEntity<TestData>(new TestData("Jason")));
|
||||||
@@ -176,6 +208,7 @@ public class RequestPartIntegrationTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
|
@SuppressWarnings("unused")
|
||||||
static class CommonsMultipartResolverTestConfig extends RequestPartTestConfig {
|
static class CommonsMultipartResolverTestConfig extends RequestPartTestConfig {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@@ -185,6 +218,7 @@ public class RequestPartIntegrationTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
|
@SuppressWarnings("unused")
|
||||||
static class StandardMultipartResolverTestConfig extends RequestPartTestConfig {
|
static class StandardMultipartResolverTestConfig extends RequestPartTestConfig {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@@ -194,9 +228,10 @@ public class RequestPartIntegrationTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
|
@SuppressWarnings("unused")
|
||||||
private static class RequestPartTestController {
|
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,
|
public ResponseEntity<Object> create(@RequestPart(name = "json-data") TestData testData,
|
||||||
@RequestPart("file-data") MultipartFile file,
|
@RequestPart("file-data") MultipartFile file,
|
||||||
@RequestPart(name = "empty-data", required = false) TestData emptyData,
|
@RequestPart(name = "empty-data", required = false) TestData emptyData,
|
||||||
@@ -209,6 +244,12 @@ public class RequestPartIntegrationTests {
|
|||||||
headers.setLocation(URI.create(url));
|
headers.setLocation(URI.create(url));
|
||||||
return new ResponseEntity<Object>(headers, HttpStatus.CREATED);
|
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")
|
@SuppressWarnings("unused")
|
||||||
|
|||||||
Reference in New Issue
Block a user