mirror of
https://github.com/spring-projects/spring-framework.git
synced 2026-09-21 13:31:46 +00:00
Move multipart support to dedicated converter
Prior to this commit, gh-36255 introduced the new `MultipartHttpMessageConverter`, focusing on multipart message conversion in a separate converter. The `FormHttpMessageConverter` did conflate URL encoded forms and multipart messages in the same converter. With the introduction of the new converter and related types in the same package (with `Part`, `FormFieldPart` and `FilePart`), we can now revisit this arrangement. This commit restricts the `FormHttpMessageConverter` to URL encoded forms only and as a result, changes its implementation to only consider `MultiValueMap<String, String>` types for reading and writing HTTP messages. Because type erasure, this converter is now a `SmartHttpMessageConverter` to get better type information with `ResolvableType`. As a result, the `AllEncompassingFormHttpMessageConverter` is formally deprecated and replaced by the `MultipartHttpMessageConverter`, by setting part converters explicitly in its constructor. Closes gh-36256
This commit is contained in:
+26
-15
@@ -33,9 +33,9 @@ import org.springframework.http.converter.feed.AtomFeedHttpMessageConverter;
|
||||
import org.springframework.http.converter.feed.RssChannelHttpMessageConverter;
|
||||
import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter;
|
||||
import org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverter;
|
||||
import org.springframework.http.converter.multipart.MultipartHttpMessageConverter;
|
||||
import org.springframework.http.converter.protobuf.KotlinSerializationProtobufHttpMessageConverter;
|
||||
import org.springframework.http.converter.smile.JacksonSmileHttpMessageConverter;
|
||||
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
|
||||
import org.springframework.http.converter.xml.JacksonXmlHttpMessageConverter;
|
||||
import org.springframework.http.converter.yaml.JacksonYamlHttpMessageConverter;
|
||||
|
||||
@@ -68,6 +68,13 @@ class DefaultHttpMessageConvertersTests {
|
||||
.withMessage("converter should support 'text/plain'");
|
||||
}
|
||||
|
||||
@Test
|
||||
void failsWhenFormConverterDoesNotSupportMediaType() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> HttpMessageConverters.forClient().withFormConverter(new CustomHttpMessageConverter()).build())
|
||||
.withMessage("converter should support 'application/x-www-form-urlencoded'");
|
||||
}
|
||||
|
||||
@Test
|
||||
void failsWhenJsonConverterDoesNotSupportMediaType() {
|
||||
assertThatIllegalArgumentException()
|
||||
@@ -116,8 +123,9 @@ class DefaultHttpMessageConvertersTests {
|
||||
void defaultConverters() {
|
||||
var converters = HttpMessageConverters.forClient().registerDefaults().build();
|
||||
assertThat(converters).hasExactlyElementsOfTypes(ByteArrayHttpMessageConverter.class,
|
||||
StringHttpMessageConverter.class, ResourceHttpMessageConverter.class,
|
||||
AllEncompassingFormHttpMessageConverter.class, KotlinSerializationJsonHttpMessageConverter.class,
|
||||
StringHttpMessageConverter.class, FormHttpMessageConverter.class,
|
||||
ResourceHttpMessageConverter.class, MultipartHttpMessageConverter.class,
|
||||
KotlinSerializationJsonHttpMessageConverter.class,
|
||||
JacksonJsonHttpMessageConverter.class, JacksonSmileHttpMessageConverter.class,
|
||||
KotlinSerializationCborHttpMessageConverter.class, JacksonCborHttpMessageConverter.class,
|
||||
JacksonYamlHttpMessageConverter.class, JacksonXmlHttpMessageConverter.class,
|
||||
@@ -134,7 +142,7 @@ class DefaultHttpMessageConvertersTests {
|
||||
@Test
|
||||
void multipartConverterContainsOtherConverters() {
|
||||
var converters = HttpMessageConverters.forClient().registerDefaults().build();
|
||||
var multipartConverter = findMessageConverter(AllEncompassingFormHttpMessageConverter.class, converters);
|
||||
var multipartConverter = findMessageConverter(MultipartHttpMessageConverter.class, converters);
|
||||
|
||||
assertThat(multipartConverter.getPartConverters()).hasExactlyElementsOfTypes(
|
||||
ByteArrayHttpMessageConverter.class, StringHttpMessageConverter.class,
|
||||
@@ -150,7 +158,7 @@ class DefaultHttpMessageConvertersTests {
|
||||
void registerCustomMessageConverter() {
|
||||
var converters = HttpMessageConverters.forClient()
|
||||
.addCustomConverter(new CustomHttpMessageConverter()).build();
|
||||
assertThat(converters).hasExactlyElementsOfTypes(CustomHttpMessageConverter.class, AllEncompassingFormHttpMessageConverter.class);
|
||||
assertThat(converters).hasExactlyElementsOfTypes(CustomHttpMessageConverter.class, MultipartHttpMessageConverter.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -159,8 +167,9 @@ class DefaultHttpMessageConvertersTests {
|
||||
.addCustomConverter(new CustomHttpMessageConverter()).build();
|
||||
assertThat(converters).hasExactlyElementsOfTypes(
|
||||
CustomHttpMessageConverter.class, ByteArrayHttpMessageConverter.class,
|
||||
StringHttpMessageConverter.class, ResourceHttpMessageConverter.class,
|
||||
AllEncompassingFormHttpMessageConverter.class, KotlinSerializationJsonHttpMessageConverter.class,
|
||||
StringHttpMessageConverter.class, FormHttpMessageConverter.class,
|
||||
ResourceHttpMessageConverter.class, MultipartHttpMessageConverter.class,
|
||||
KotlinSerializationJsonHttpMessageConverter.class,
|
||||
JacksonJsonHttpMessageConverter.class, JacksonSmileHttpMessageConverter.class,
|
||||
KotlinSerializationCborHttpMessageConverter.class, JacksonCborHttpMessageConverter.class,
|
||||
JacksonYamlHttpMessageConverter.class, JacksonXmlHttpMessageConverter.class,
|
||||
@@ -172,7 +181,7 @@ class DefaultHttpMessageConvertersTests {
|
||||
void registerCustomConverterInMultipartConverter() {
|
||||
var converters = HttpMessageConverters.forClient().registerDefaults()
|
||||
.addCustomConverter(new CustomHttpMessageConverter()).build();
|
||||
var multipartConverter = findMessageConverter(AllEncompassingFormHttpMessageConverter.class, converters);
|
||||
var multipartConverter = findMessageConverter(MultipartHttpMessageConverter.class, converters);
|
||||
assertThat(multipartConverter.getPartConverters()).hasAtLeastOneElementOfType(CustomHttpMessageConverter.class);
|
||||
}
|
||||
|
||||
@@ -248,8 +257,9 @@ class DefaultHttpMessageConvertersTests {
|
||||
var converters = HttpMessageConverters.forServer().registerDefaults().build();
|
||||
assertThat(converters).hasExactlyElementsOfTypes(
|
||||
ByteArrayHttpMessageConverter.class, StringHttpMessageConverter.class,
|
||||
ResourceHttpMessageConverter.class, ResourceRegionHttpMessageConverter.class,
|
||||
AllEncompassingFormHttpMessageConverter.class, KotlinSerializationJsonHttpMessageConverter.class,
|
||||
FormHttpMessageConverter.class, ResourceHttpMessageConverter.class,
|
||||
ResourceRegionHttpMessageConverter.class, MultipartHttpMessageConverter.class,
|
||||
KotlinSerializationJsonHttpMessageConverter.class,
|
||||
JacksonJsonHttpMessageConverter.class, JacksonSmileHttpMessageConverter.class,
|
||||
KotlinSerializationCborHttpMessageConverter.class, JacksonCborHttpMessageConverter.class,
|
||||
JacksonYamlHttpMessageConverter.class, JacksonXmlHttpMessageConverter.class,
|
||||
@@ -266,7 +276,7 @@ class DefaultHttpMessageConvertersTests {
|
||||
@Test
|
||||
void multipartConverterContainsOtherConverters() {
|
||||
var converters = HttpMessageConverters.forServer().registerDefaults().build();
|
||||
var multipartConverter = findMessageConverter(AllEncompassingFormHttpMessageConverter.class, converters);
|
||||
var multipartConverter = findMessageConverter(MultipartHttpMessageConverter.class, converters);
|
||||
|
||||
assertThat(multipartConverter.getPartConverters()).hasExactlyElementsOfTypes(
|
||||
ByteArrayHttpMessageConverter.class, StringHttpMessageConverter.class,
|
||||
@@ -282,7 +292,7 @@ class DefaultHttpMessageConvertersTests {
|
||||
void registerCustomMessageConverter() {
|
||||
var converters = HttpMessageConverters.forServer()
|
||||
.addCustomConverter(new CustomHttpMessageConverter()).build();
|
||||
assertThat(converters).hasExactlyElementsOfTypes(CustomHttpMessageConverter.class, AllEncompassingFormHttpMessageConverter.class);
|
||||
assertThat(converters).hasExactlyElementsOfTypes(CustomHttpMessageConverter.class, MultipartHttpMessageConverter.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -292,8 +302,9 @@ class DefaultHttpMessageConvertersTests {
|
||||
assertThat(converters).hasExactlyElementsOfTypes(
|
||||
CustomHttpMessageConverter.class,
|
||||
ByteArrayHttpMessageConverter.class, StringHttpMessageConverter.class,
|
||||
ResourceHttpMessageConverter.class, ResourceRegionHttpMessageConverter.class,
|
||||
AllEncompassingFormHttpMessageConverter.class, KotlinSerializationJsonHttpMessageConverter.class,
|
||||
FormHttpMessageConverter.class, ResourceHttpMessageConverter.class,
|
||||
ResourceRegionHttpMessageConverter.class, MultipartHttpMessageConverter.class,
|
||||
KotlinSerializationJsonHttpMessageConverter.class,
|
||||
JacksonJsonHttpMessageConverter.class, JacksonSmileHttpMessageConverter.class,
|
||||
KotlinSerializationCborHttpMessageConverter.class, JacksonCborHttpMessageConverter.class,
|
||||
JacksonYamlHttpMessageConverter.class, JacksonXmlHttpMessageConverter.class,
|
||||
@@ -305,7 +316,7 @@ class DefaultHttpMessageConvertersTests {
|
||||
void registerCustomConverterInMultipartConverter() {
|
||||
var converters = HttpMessageConverters.forServer().registerDefaults()
|
||||
.addCustomConverter(new CustomHttpMessageConverter()).build();
|
||||
var multipartConverter = findMessageConverter(AllEncompassingFormHttpMessageConverter.class, converters);
|
||||
var multipartConverter = findMessageConverter(MultipartHttpMessageConverter.class, converters);
|
||||
assertThat(multipartConverter.getPartConverters()).hasAtLeastOneElementOfType(CustomHttpMessageConverter.class);
|
||||
}
|
||||
|
||||
|
||||
+37
-359
@@ -16,33 +16,14 @@
|
||||
|
||||
package org.springframework.http.converter;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.StringReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.xml.transform.Source;
|
||||
import javax.xml.transform.stream.StreamSource;
|
||||
|
||||
import org.apache.tomcat.util.http.fileupload.FileItem;
|
||||
import org.apache.tomcat.util.http.fileupload.FileUpload;
|
||||
import org.apache.tomcat.util.http.fileupload.RequestContext;
|
||||
import org.apache.tomcat.util.http.fileupload.UploadContext;
|
||||
import org.apache.tomcat.util.http.fileupload.disk.DiskFileItemFactory;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.StreamingHttpOutputMessage;
|
||||
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
|
||||
import org.springframework.http.converter.xml.SourceHttpMessageConverter;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.testfixture.http.MockHttpInputMessage;
|
||||
@@ -52,15 +33,12 @@ import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED;
|
||||
import static org.springframework.http.MediaType.APPLICATION_JSON;
|
||||
import static org.springframework.http.MediaType.MULTIPART_FORM_DATA;
|
||||
import static org.springframework.http.MediaType.MULTIPART_MIXED;
|
||||
import static org.springframework.http.MediaType.MULTIPART_RELATED;
|
||||
import static org.springframework.http.MediaType.TEXT_XML;
|
||||
|
||||
/**
|
||||
* Tests for {@link FormHttpMessageConverter} and
|
||||
* {@link AllEncompassingFormHttpMessageConverter}.
|
||||
* Tests for {@link FormHttpMessageConverter}.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @author Rossen Stoyanchev
|
||||
@@ -69,16 +47,25 @@ import static org.springframework.http.MediaType.TEXT_XML;
|
||||
*/
|
||||
class FormHttpMessageConverterTests {
|
||||
|
||||
private final FormHttpMessageConverter converter = new AllEncompassingFormHttpMessageConverter();
|
||||
private static final ResolvableType LINKED_MULTI_VALUE_MAP =
|
||||
ResolvableType.forClassWithGenerics(LinkedMultiValueMap.class, String.class, String.class);
|
||||
|
||||
private static final ResolvableType MULTI_VALUE_MAP =
|
||||
ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class);
|
||||
|
||||
private final FormHttpMessageConverter converter = new FormHttpMessageConverter();
|
||||
|
||||
|
||||
|
||||
@Test
|
||||
void canRead() {
|
||||
assertCanRead(MultiValueMap.class, null);
|
||||
assertCanRead(APPLICATION_FORM_URLENCODED);
|
||||
assertCanRead(LINKED_MULTI_VALUE_MAP, APPLICATION_FORM_URLENCODED);
|
||||
assertCanRead(ResolvableType.forClass(LinkedMultiValueMap.class), APPLICATION_FORM_URLENCODED);
|
||||
|
||||
assertCannotRead(String.class, null);
|
||||
assertCannotRead(String.class, APPLICATION_FORM_URLENCODED);
|
||||
ResolvableType mapStringObject = ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, Object.class);
|
||||
assertCannotRead(mapStringObject, null);
|
||||
assertCannotRead(mapStringObject, APPLICATION_FORM_URLENCODED);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -93,30 +80,18 @@ class FormHttpMessageConverterTests {
|
||||
@Test
|
||||
void canWrite() {
|
||||
assertCanWrite(APPLICATION_FORM_URLENCODED);
|
||||
assertCanWrite(MULTIPART_FORM_DATA);
|
||||
assertCanWrite(MULTIPART_MIXED);
|
||||
assertCanWrite(MULTIPART_RELATED);
|
||||
assertCanWrite(new MediaType("multipart", "form-data", UTF_8));
|
||||
assertCanWrite(MediaType.ALL);
|
||||
assertCanWrite(null);
|
||||
assertCanWrite(LINKED_MULTI_VALUE_MAP, APPLICATION_FORM_URLENCODED);
|
||||
assertCanWrite(ResolvableType.forClass(LinkedMultiValueMap.class), APPLICATION_FORM_URLENCODED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void setSupportedMediaTypes() {
|
||||
this.converter.setSupportedMediaTypes(List.of(MULTIPART_FORM_DATA));
|
||||
void cannotWriteMultipart() {
|
||||
assertCannotWrite(MULTIPART_FORM_DATA);
|
||||
assertCannotWrite(MULTIPART_MIXED);
|
||||
|
||||
this.converter.setSupportedMediaTypes(List.of(MULTIPART_MIXED));
|
||||
assertCanWrite(MULTIPART_MIXED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void addSupportedMediaTypes() {
|
||||
this.converter.setSupportedMediaTypes(List.of(MULTIPART_FORM_DATA));
|
||||
assertCannotWrite(MULTIPART_MIXED);
|
||||
|
||||
this.converter.addSupportedMediaTypes(MULTIPART_RELATED);
|
||||
assertCanWrite(MULTIPART_RELATED);
|
||||
assertCannotWrite(MULTIPART_RELATED);
|
||||
assertCannotWrite(new MediaType("multipart", "form-data", UTF_8));
|
||||
assertCannotWrite(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -173,243 +148,12 @@ class FormHttpMessageConverterTests {
|
||||
.as("Invalid content-length").isEqualTo(outputMessage.getBodyAsBytes().length);
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeMultipart() throws Exception {
|
||||
|
||||
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
|
||||
parts.add("name 1", "value 1");
|
||||
parts.add("name 2", "value 2+1");
|
||||
parts.add("name 2", "value 2+2");
|
||||
parts.add("name 3", null);
|
||||
|
||||
Resource logo = new ClassPathResource("/org/springframework/http/converter/logo.jpg");
|
||||
parts.add("logo", logo);
|
||||
|
||||
// SPR-12108
|
||||
Resource utf8 = new ClassPathResource("/org/springframework/http/converter/logo.jpg") {
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return "Hall\u00F6le.jpg";
|
||||
}
|
||||
};
|
||||
parts.add("utf8", utf8);
|
||||
|
||||
MyBean myBean = new MyBean();
|
||||
myBean.setString("foo");
|
||||
HttpHeaders entityHeaders = new HttpHeaders();
|
||||
entityHeaders.setContentType(APPLICATION_JSON);
|
||||
HttpEntity<MyBean> entity = new HttpEntity<>(myBean, entityHeaders);
|
||||
parts.add("json", entity);
|
||||
|
||||
Map<String, String> parameters = new LinkedHashMap<>(2);
|
||||
parameters.put("charset", UTF_8.name());
|
||||
parameters.put("foo", "bar");
|
||||
|
||||
StreamingMockHttpOutputMessage outputMessage = new StreamingMockHttpOutputMessage();
|
||||
this.converter.write(parts, new MediaType("multipart", "form-data", parameters), outputMessage);
|
||||
|
||||
final MediaType contentType = outputMessage.getHeaders().getContentType();
|
||||
assertThat(contentType.getParameters()).containsKeys("charset", "boundary", "foo"); // gh-21568, gh-25839
|
||||
|
||||
// see if Commons FileUpload can read what we wrote
|
||||
FileUpload fileUpload = new FileUpload();
|
||||
fileUpload.setFileItemFactory(new DiskFileItemFactory());
|
||||
RequestContext requestContext = new MockHttpOutputMessageRequestContext(outputMessage);
|
||||
List<FileItem> items = fileUpload.parseRequest(requestContext);
|
||||
assertThat(items).hasSize(6);
|
||||
FileItem item = items.get(0);
|
||||
assertThat(item.isFormField()).isTrue();
|
||||
assertThat(item.getFieldName()).isEqualTo("name 1");
|
||||
assertThat(item.getString()).isEqualTo("value 1");
|
||||
|
||||
item = items.get(1);
|
||||
assertThat(item.isFormField()).isTrue();
|
||||
assertThat(item.getFieldName()).isEqualTo("name 2");
|
||||
assertThat(item.getString()).isEqualTo("value 2+1");
|
||||
|
||||
item = items.get(2);
|
||||
assertThat(item.isFormField()).isTrue();
|
||||
assertThat(item.getFieldName()).isEqualTo("name 2");
|
||||
assertThat(item.getString()).isEqualTo("value 2+2");
|
||||
|
||||
item = items.get(3);
|
||||
assertThat(item.isFormField()).isFalse();
|
||||
assertThat(item.getFieldName()).isEqualTo("logo");
|
||||
assertThat(item.getName()).isEqualTo("logo.jpg");
|
||||
assertThat(item.getContentType()).isEqualTo("image/jpeg");
|
||||
assertThat(item.getSize()).isEqualTo(logo.getFile().length());
|
||||
|
||||
item = items.get(4);
|
||||
assertThat(item.isFormField()).isFalse();
|
||||
assertThat(item.getFieldName()).isEqualTo("utf8");
|
||||
assertThat(item.getName()).isEqualTo("Hall\u00F6le.jpg");
|
||||
assertThat(item.getContentType()).isEqualTo("image/jpeg");
|
||||
assertThat(item.getSize()).isEqualTo(logo.getFile().length());
|
||||
|
||||
item = items.get(5);
|
||||
assertThat(item.getFieldName()).isEqualTo("json");
|
||||
assertThat(item.getContentType()).isEqualTo("application/json");
|
||||
|
||||
assertThat(outputMessage.wasRepeatable()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeMultipartWithSourceHttpMessageConverter() throws Exception {
|
||||
|
||||
converter.setPartConverters(List.of(
|
||||
new StringHttpMessageConverter(),
|
||||
new ResourceHttpMessageConverter(),
|
||||
new SourceHttpMessageConverter<>()));
|
||||
|
||||
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
|
||||
parts.add("name 1", "value 1");
|
||||
parts.add("name 2", "value 2+1");
|
||||
parts.add("name 2", "value 2+2");
|
||||
parts.add("name 3", null);
|
||||
|
||||
Resource logo = new ClassPathResource("/org/springframework/http/converter/logo.jpg");
|
||||
parts.add("logo", logo);
|
||||
|
||||
// SPR-12108
|
||||
Resource utf8 = new ClassPathResource("/org/springframework/http/converter/logo.jpg") {
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return "Hall\u00F6le.jpg";
|
||||
}
|
||||
};
|
||||
parts.add("utf8", utf8);
|
||||
|
||||
Source xml = new StreamSource(new StringReader("<root><child/></root>"));
|
||||
HttpHeaders entityHeaders = new HttpHeaders();
|
||||
entityHeaders.setContentType(TEXT_XML);
|
||||
HttpEntity<Source> entity = new HttpEntity<>(xml, entityHeaders);
|
||||
parts.add("xml", entity);
|
||||
|
||||
Map<String, String> parameters = new LinkedHashMap<>(2);
|
||||
parameters.put("charset", UTF_8.name());
|
||||
parameters.put("foo", "bar");
|
||||
|
||||
StreamingMockHttpOutputMessage outputMessage = new StreamingMockHttpOutputMessage();
|
||||
this.converter.write(parts, new MediaType("multipart", "form-data", parameters), outputMessage);
|
||||
|
||||
final MediaType contentType = outputMessage.getHeaders().getContentType();
|
||||
assertThat(contentType.getParameters()).containsKeys("charset", "boundary", "foo"); // gh-21568, gh-25839
|
||||
|
||||
// see if Commons FileUpload can read what we wrote
|
||||
FileUpload fileUpload = new FileUpload();
|
||||
fileUpload.setFileItemFactory(new DiskFileItemFactory());
|
||||
RequestContext requestContext = new MockHttpOutputMessageRequestContext(outputMessage);
|
||||
List<FileItem> items = fileUpload.parseRequest(requestContext);
|
||||
assertThat(items).hasSize(6);
|
||||
FileItem item = items.get(0);
|
||||
assertThat(item.isFormField()).isTrue();
|
||||
assertThat(item.getFieldName()).isEqualTo("name 1");
|
||||
assertThat(item.getString()).isEqualTo("value 1");
|
||||
|
||||
item = items.get(1);
|
||||
assertThat(item.isFormField()).isTrue();
|
||||
assertThat(item.getFieldName()).isEqualTo("name 2");
|
||||
assertThat(item.getString()).isEqualTo("value 2+1");
|
||||
|
||||
item = items.get(2);
|
||||
assertThat(item.isFormField()).isTrue();
|
||||
assertThat(item.getFieldName()).isEqualTo("name 2");
|
||||
assertThat(item.getString()).isEqualTo("value 2+2");
|
||||
|
||||
item = items.get(3);
|
||||
assertThat(item.isFormField()).isFalse();
|
||||
assertThat(item.getFieldName()).isEqualTo("logo");
|
||||
assertThat(item.getName()).isEqualTo("logo.jpg");
|
||||
assertThat(item.getContentType()).isEqualTo("image/jpeg");
|
||||
assertThat(item.getSize()).isEqualTo(logo.getFile().length());
|
||||
|
||||
item = items.get(4);
|
||||
assertThat(item.isFormField()).isFalse();
|
||||
assertThat(item.getFieldName()).isEqualTo("utf8");
|
||||
assertThat(item.getName()).isEqualTo("Hall\u00F6le.jpg");
|
||||
assertThat(item.getContentType()).isEqualTo("image/jpeg");
|
||||
assertThat(item.getSize()).isEqualTo(logo.getFile().length());
|
||||
|
||||
item = items.get(5);
|
||||
assertThat(item.getFieldName()).isEqualTo("xml");
|
||||
assertThat(item.getContentType()).isEqualTo("text/xml");
|
||||
|
||||
assertThat(outputMessage.wasRepeatable()).isFalse();
|
||||
}
|
||||
|
||||
@Test // SPR-13309
|
||||
void writeMultipartOrder() throws Exception {
|
||||
MyBean myBean = new MyBean();
|
||||
myBean.setString("foo");
|
||||
|
||||
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
|
||||
parts.add("part1", myBean);
|
||||
|
||||
HttpHeaders entityHeaders = new HttpHeaders();
|
||||
entityHeaders.setContentType(TEXT_XML);
|
||||
HttpEntity<MyBean> entity = new HttpEntity<>(myBean, entityHeaders);
|
||||
parts.add("part2", entity);
|
||||
|
||||
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
|
||||
this.converter.setMultipartCharset(UTF_8);
|
||||
this.converter.write(parts, new MediaType("multipart", "form-data", UTF_8), outputMessage);
|
||||
|
||||
final MediaType contentType = outputMessage.getHeaders().getContentType();
|
||||
assertThat(contentType.getParameter("boundary")).as("No boundary found").isNotNull();
|
||||
|
||||
// see if Commons FileUpload can read what we wrote
|
||||
FileUpload fileUpload = new FileUpload();
|
||||
fileUpload.setFileItemFactory(new DiskFileItemFactory());
|
||||
RequestContext requestContext = new MockHttpOutputMessageRequestContext(outputMessage);
|
||||
List<FileItem> items = fileUpload.parseRequest(requestContext);
|
||||
assertThat(items).hasSize(2);
|
||||
|
||||
FileItem item = items.get(0);
|
||||
assertThat(item.isFormField()).isTrue();
|
||||
assertThat(item.getFieldName()).isEqualTo("part1");
|
||||
assertThat(item.getString()).isEqualTo("{\"string\":\"foo\"}");
|
||||
|
||||
item = items.get(1);
|
||||
assertThat(item.isFormField()).isTrue();
|
||||
assertThat(item.getFieldName()).isEqualTo("part2");
|
||||
|
||||
// With developer builds we get: <MyBean><string>foo</string></MyBean>
|
||||
// But on CI server we get: <MyBean xmlns=""><string>foo</string></MyBean>
|
||||
// So... we make a compromise:
|
||||
assertThat(item.getString())
|
||||
.startsWith("<MyBean")
|
||||
.endsWith("><string>foo</string></MyBean>");
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeMultipartCharset() throws Exception {
|
||||
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
|
||||
Resource logo = new ClassPathResource("/org/springframework/http/converter/logo.jpg");
|
||||
parts.add("logo", logo);
|
||||
|
||||
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
|
||||
this.converter.write(parts, MULTIPART_FORM_DATA, outputMessage);
|
||||
|
||||
MediaType contentType = outputMessage.getHeaders().getContentType();
|
||||
Map<String, String> parameters = contentType.getParameters();
|
||||
assertThat(parameters).containsOnlyKeys("boundary");
|
||||
|
||||
this.converter.setCharset(StandardCharsets.ISO_8859_1);
|
||||
|
||||
outputMessage = new MockHttpOutputMessage();
|
||||
this.converter.write(parts, MULTIPART_FORM_DATA, outputMessage);
|
||||
|
||||
parameters = outputMessage.getHeaders().getContentType().getParameters();
|
||||
assertThat(parameters).containsOnlyKeys("boundary", "charset");
|
||||
assertThat(parameters).containsEntry("charset", "ISO-8859-1");
|
||||
}
|
||||
|
||||
private void assertCanRead(MediaType mediaType) {
|
||||
assertCanRead(MultiValueMap.class, mediaType);
|
||||
assertCanRead(MULTI_VALUE_MAP, mediaType);
|
||||
}
|
||||
|
||||
private void assertCanRead(Class<?> clazz, MediaType mediaType) {
|
||||
assertThat(this.converter.canRead(clazz, mediaType)).as(clazz.getSimpleName() + " : " + mediaType).isTrue();
|
||||
private void assertCanRead(ResolvableType type, MediaType mediaType) {
|
||||
assertThat(this.converter.canRead(type, mediaType)).as(type.toClass().getSimpleName() + " : " + mediaType).isTrue();
|
||||
}
|
||||
|
||||
private void asssertCannotReadMultipart() {
|
||||
@@ -420,21 +164,25 @@ class FormHttpMessageConverterTests {
|
||||
}
|
||||
|
||||
private void assertCannotRead(MediaType mediaType) {
|
||||
assertCannotRead(MultiValueMap.class, mediaType);
|
||||
assertCannotRead(MULTI_VALUE_MAP, mediaType);
|
||||
}
|
||||
|
||||
private void assertCannotRead(Class<?> clazz, MediaType mediaType) {
|
||||
assertThat(this.converter.canRead(clazz, mediaType)).as(clazz.getSimpleName() + " : " + mediaType).isFalse();
|
||||
private void assertCannotRead(ResolvableType type, MediaType mediaType) {
|
||||
assertThat(this.converter.canRead(type, mediaType)).as(type + " : " + mediaType).isFalse();
|
||||
}
|
||||
|
||||
private void assertCanWrite(ResolvableType type, MediaType mediaType) {
|
||||
assertThat(this.converter.canWrite(type, LinkedMultiValueMap.class, mediaType))
|
||||
.as(type + " : " + mediaType).isTrue();
|
||||
}
|
||||
|
||||
private void assertCanWrite(MediaType mediaType) {
|
||||
Class<?> clazz = MultiValueMap.class;
|
||||
assertThat(this.converter.canWrite(clazz, mediaType)).as(clazz.getSimpleName() + " : " + mediaType).isTrue();
|
||||
assertCanWrite(MULTI_VALUE_MAP, mediaType);
|
||||
}
|
||||
|
||||
private void assertCannotWrite(MediaType mediaType) {
|
||||
Class<?> clazz = MultiValueMap.class;
|
||||
assertThat(this.converter.canWrite(clazz, mediaType)).as(clazz.getSimpleName() + " : " + mediaType).isFalse();
|
||||
assertThat(this.converter.canWrite(MULTI_VALUE_MAP, MultiValueMap.class, mediaType))
|
||||
.as(MultiValueMap.class.getSimpleName() + " : " + mediaType).isFalse();
|
||||
}
|
||||
|
||||
private void assertInvalidFormIsRejectedWithSpecificException(String body) {
|
||||
@@ -442,80 +190,10 @@ class FormHttpMessageConverterTests {
|
||||
inputMessage.getHeaders().setContentType(
|
||||
new MediaType("application", "x-www-form-urlencoded", StandardCharsets.ISO_8859_1));
|
||||
|
||||
assertThatThrownBy(() -> this.converter.read(null, inputMessage))
|
||||
assertThatThrownBy(() -> this.converter.read(MULTI_VALUE_MAP, inputMessage, null))
|
||||
.isInstanceOf(HttpMessageNotReadableException.class)
|
||||
.hasCauseInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Could not decode HTTP form payload");
|
||||
}
|
||||
|
||||
|
||||
private static class StreamingMockHttpOutputMessage extends MockHttpOutputMessage implements StreamingHttpOutputMessage {
|
||||
|
||||
private boolean repeatable;
|
||||
|
||||
public boolean wasRepeatable() {
|
||||
return this.repeatable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBody(Body body) {
|
||||
try {
|
||||
this.repeatable = body.repeatable();
|
||||
body.writeTo(getBody());
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class MockHttpOutputMessageRequestContext implements UploadContext {
|
||||
|
||||
private final MockHttpOutputMessage outputMessage;
|
||||
|
||||
private final byte[] body;
|
||||
|
||||
private MockHttpOutputMessageRequestContext(MockHttpOutputMessage outputMessage) {
|
||||
this.outputMessage = outputMessage;
|
||||
this.body = this.outputMessage.getBodyAsBytes();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCharacterEncoding() {
|
||||
MediaType type = this.outputMessage.getHeaders().getContentType();
|
||||
return (type != null && type.getCharset() != null ? type.getCharset().name() : null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getContentType() {
|
||||
MediaType type = this.outputMessage.getHeaders().getContentType();
|
||||
return (type != null ? type.toString() : null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() {
|
||||
return new ByteArrayInputStream(body);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long contentLength() {
|
||||
return body.length;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static class MyBean {
|
||||
|
||||
private String string;
|
||||
|
||||
public String getString() {
|
||||
return this.string;
|
||||
}
|
||||
|
||||
public void setString(String string) {
|
||||
this.string = string;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+20
@@ -44,6 +44,7 @@ import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.StreamingHttpOutputMessage;
|
||||
import org.springframework.http.converter.AbstractHttpMessageConverter;
|
||||
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageConversionException;
|
||||
import org.springframework.http.converter.ResourceHttpMessageConverter;
|
||||
@@ -123,6 +124,25 @@ class MultipartHttpMessageConverterTests {
|
||||
assertCanWrite(MULTIPART_RELATED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void applyDefaultCharsetToPartConverters() {
|
||||
this.converter.getPartConverters().forEach(converter -> {
|
||||
if (converter instanceof AbstractHttpMessageConverter<?> abstractConverter) {
|
||||
assertThat(abstractConverter.getDefaultCharset()).isIn(null, StandardCharsets.UTF_8);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void customCharsetAppliedToPartConverters() {
|
||||
this.converter.setCharset(StandardCharsets.UTF_16);
|
||||
this.converter.getPartConverters().forEach(converter -> {
|
||||
if (converter instanceof AbstractHttpMessageConverter<?> abstractConverter) {
|
||||
assertThat(abstractConverter.getDefaultCharset()).isIn(null, StandardCharsets.UTF_16);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private void assertCanRead(MediaType mediaType) {
|
||||
assertCanRead(ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, Part.class), mediaType);
|
||||
|
||||
@@ -33,7 +33,7 @@ import org.springframework.http.client.JettyClientHttpRequestFactory;
|
||||
import org.springframework.http.client.support.BasicAuthenticationInterceptor;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.StringHttpMessageConverter;
|
||||
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
|
||||
import org.springframework.http.converter.multipart.MultipartHttpMessageConverter;
|
||||
import org.springframework.web.util.DefaultUriBuilderFactory;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -154,7 +154,7 @@ class RestClientBuilderTests {
|
||||
|
||||
assertThat(fieldValue("messageConverters", restClient))
|
||||
.asInstanceOf(InstanceOfAssertFactories.LIST)
|
||||
.hasExactlyElementsOfTypes(StringHttpMessageConverter.class, AllEncompassingFormHttpMessageConverter.class);
|
||||
.hasExactlyElementsOfTypes(StringHttpMessageConverter.class, MultipartHttpMessageConverter.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+7
-5
@@ -53,8 +53,8 @@ import org.springframework.http.client.JdkClientHttpRequestFactory;
|
||||
import org.springframework.http.client.JettyClientHttpRequestFactory;
|
||||
import org.springframework.http.client.ReactorClientHttpRequestFactory;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
import org.springframework.http.converter.FormHttpMessageConverter;
|
||||
import org.springframework.http.converter.json.MappingJacksonValue;
|
||||
import org.springframework.http.converter.multipart.MultipartHttpMessageConverter;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
@@ -348,23 +348,25 @@ class RestTemplateIntegrationTests extends AbstractMockWebServerTests {
|
||||
|
||||
private void addSupportedMediaTypeToFormHttpMessageConverter(MediaType mediaType) {
|
||||
this.template.getMessageConverters().stream()
|
||||
.filter(FormHttpMessageConverter.class::isInstance)
|
||||
.map(FormHttpMessageConverter.class::cast)
|
||||
.filter(MultipartHttpMessageConverter.class::isInstance)
|
||||
.map(MultipartHttpMessageConverter.class::cast)
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalStateException("Failed to find FormHttpMessageConverter"))
|
||||
.orElseThrow(() -> new IllegalStateException("Failed to find MultipartHttpMessageConverter"))
|
||||
.addSupportedMediaTypes(mediaType);
|
||||
}
|
||||
|
||||
@ParameterizedRestTemplateTest
|
||||
void form(ClientHttpRequestFactory clientHttpRequestFactory) {
|
||||
setUpClient(clientHttpRequestFactory);
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||
|
||||
MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
|
||||
form.add("name 1", "value 1");
|
||||
form.add("name 2", "value 2+1");
|
||||
form.add("name 2", "value 2+2");
|
||||
|
||||
template.postForLocation(baseUrl + "/form", form);
|
||||
template.exchange(baseUrl + "/form", POST, new HttpEntity<>(form, headers), Void.class);
|
||||
}
|
||||
|
||||
@ParameterizedRestTemplateTest
|
||||
|
||||
+4
-2
@@ -24,7 +24,8 @@ import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.Part;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.http.converter.FormHttpMessageConverter;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.multipart.MultipartHttpMessageConverter;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.multipart.MaxUploadSizeExceededException;
|
||||
@@ -87,7 +88,7 @@ class StandardMultipartHttpServletRequestTests {
|
||||
map.add(name, multipartFile.getResource());
|
||||
|
||||
MockHttpOutputMessage output = new MockHttpOutputMessage();
|
||||
new FormHttpMessageConverter().write(map, null, output);
|
||||
new MultipartHttpMessageConverter().write(map, null, output);
|
||||
|
||||
assertThat(output.getBodyAsString(StandardCharsets.UTF_8)).contains("""
|
||||
Content-Disposition: form-data; name="file"; filename="myFile.txt"
|
||||
@@ -166,6 +167,7 @@ class StandardMultipartHttpServletRequestTests {
|
||||
|
||||
private static StandardMultipartHttpServletRequest requestWithPart(String name, String disposition, String content) {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setContentType(MediaType.MULTIPART_FORM_DATA_VALUE);
|
||||
MockPart part = new MockPart(name, null, content.getBytes(StandardCharsets.UTF_8));
|
||||
part.getHeaders().set("Content-Disposition", disposition);
|
||||
request.addPart(part);
|
||||
|
||||
Reference in New Issue
Block a user