mirror of
https://github.com/spring-projects/spring-framework
synced 2026-06-08 17:33:33 +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:
@@ -402,6 +402,27 @@ To serialize only a subset of the object properties, you can specify a {baeldung
|
||||
.toBodilessEntity();
|
||||
----
|
||||
|
||||
==== URL encoded Forms
|
||||
|
||||
URL encoded forms, using the `"application/x-www-form-urlencoded"` media type, are useful for sending String key/values over the wire.
|
||||
This is supported by the `FormHttpMessageConverter`, if the application uses a `MultiValueMap<String, String>` as source instance
|
||||
or a target type.
|
||||
|
||||
For example:
|
||||
|
||||
[source,java,indent=0,subs="verbatim"]
|
||||
----
|
||||
MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
|
||||
form.add("project", "Spring Framework");
|
||||
form.add("module", "spring-web");
|
||||
ResponseEntity<Void> response = this.restClient.post()
|
||||
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
.body(form)
|
||||
.retrieve()
|
||||
.toBodilessEntity();
|
||||
----
|
||||
|
||||
|
||||
==== Multipart
|
||||
|
||||
To send multipart data, you need to provide a `MultiValueMap<String, Object>` whose values may be an `Object` for part content, a `Resource` for a file part, or an `HttpEntity` for part content with headers.
|
||||
@@ -419,18 +440,22 @@ For example:
|
||||
headers.setContentType(MediaType.APPLICATION_XML);
|
||||
parts.add("xmlPart", new HttpEntity<>(myBean, headers));
|
||||
|
||||
// send using RestClient.post or RestTemplate.postForEntity
|
||||
ResponseEntity<Void> response = this.restClient.post()
|
||||
.contentType(MediaType.MULTIPART_FORM_DATA)
|
||||
.body(parts)
|
||||
.retrieve()
|
||||
.toBodilessEntity();
|
||||
----
|
||||
|
||||
In most cases, you do not have to specify the `Content-Type` for each part.
|
||||
The content type is determined automatically based on the `HttpMessageConverter` chosen to serialize it or, in the case of a `Resource`, based on the file extension.
|
||||
If necessary, you can explicitly provide the `MediaType` with an `HttpEntity` wrapper.
|
||||
|
||||
Once the `MultiValueMap` is ready, you can use it as the body of a `POST` request, using `RestClient.post().body(parts)` (or `RestTemplate.postForObject`).
|
||||
The `Content-Type` is set to `multipart/form-data` by the `MultiPartHttpMessageConverter`.
|
||||
As seen in the previous section, `MultiValueMap` types can also be used for URL encoded forms.
|
||||
It is preferable to explicitly set the media type in the `Content-Type` or `Accept` HTTP request headers to ensure that the expected
|
||||
message converter is used.
|
||||
|
||||
If the `MultiValueMap` contains at least one non-`String` value, the `Content-Type` is set to `multipart/form-data` by the `FormHttpMessageConverter`.
|
||||
If the `MultiValueMap` has `String` values, the `Content-Type` defaults to `application/x-www-form-urlencoded`.
|
||||
If necessary the `Content-Type` may also be set explicitly.
|
||||
|
||||
[[rest-request-factories]]
|
||||
=== Client Request Factories
|
||||
|
||||
@@ -23,13 +23,16 @@ For all converters, a default media type is used, but you can override it by set
|
||||
By default, this converter supports all text media types(`text/{asterisk}`) and writes with a `Content-Type` of `text/plain`.
|
||||
|
||||
| `FormHttpMessageConverter`
|
||||
| An `HttpMessageConverter` implementation that can read and write form data from the HTTP request and response.
|
||||
| An `HttpMessageConverter` implementation that can read and write URL encoded forms.
|
||||
By default, this converter reads and writes the `application/x-www-form-urlencoded` media type.
|
||||
Form data is read from and written into a `MultiValueMap<String, String>`.
|
||||
The converter can also write (but not read) multipart data read from a `MultiValueMap<String, Object>`.
|
||||
By default, `multipart/form-data` is supported.
|
||||
Additional multipart subtypes can be supported for writing form data.
|
||||
Consult the javadoc for `FormHttpMessageConverter` for further details.
|
||||
|
||||
| `MultipartHttpMessageConverter`
|
||||
| An `HttpMessageConverter` implementation that can read and write multipart messages.
|
||||
`MultiValueMap<String, Object>` can be written to multipart messages, converting each part independently using
|
||||
the configured message converters. Multipart messages can be read into `MultiValueMap<String, Part>`, each value
|
||||
being a `Part` or one of its subtypes (`FormFieldPart` and `FilePart`).
|
||||
By default, `multipart/form-data` is supported. Additional multipart subtypes can be supported for writing form data.
|
||||
|
||||
| `ByteArrayHttpMessageConverter`
|
||||
| An `HttpMessageConverter` implementation that can read and write byte arrays from the HTTP request and response.
|
||||
|
||||
+3
-1
@@ -35,6 +35,7 @@ import org.hamcrest.Matcher;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.w3c.dom.Node;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
@@ -180,7 +181,8 @@ public class ContentRequestMatchers {
|
||||
MockClientHttpRequest mockRequest = (MockClientHttpRequest) request;
|
||||
MockHttpInputMessage message = new MockHttpInputMessage(mockRequest.getBodyAsBytes());
|
||||
message.getHeaders().putAll(mockRequest.getHeaders());
|
||||
MultiValueMap<String, String> actualMap = new FormHttpMessageConverter().read(null, message);
|
||||
MultiValueMap<String, String> actualMap = new FormHttpMessageConverter()
|
||||
.read(ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class), message, null);
|
||||
if (containsExactly) {
|
||||
assertEquals("Form data", expectedMap, actualMap);
|
||||
}
|
||||
|
||||
+3
-2
@@ -46,6 +46,7 @@ import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.beans.Mergeable;
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpInputMessage;
|
||||
import org.springframework.http.HttpMethod;
|
||||
@@ -1021,7 +1022,6 @@ public abstract class AbstractMockHttpServletRequestBuilder<B extends AbstractMo
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private MultiValueMap<String, String> parseFormData(MediaType mediaType) {
|
||||
HttpInputMessage message = new HttpInputMessage() {
|
||||
@Override
|
||||
@@ -1038,7 +1038,8 @@ public abstract class AbstractMockHttpServletRequestBuilder<B extends AbstractMo
|
||||
};
|
||||
|
||||
try {
|
||||
return new FormHttpMessageConverter().read(null, message);
|
||||
return new FormHttpMessageConverter()
|
||||
.read(ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class), message, null);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new IllegalStateException("Failed to parse form data in request body", ex);
|
||||
|
||||
+4
-2
@@ -25,9 +25,10 @@ import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpOutputMessage;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.FormHttpMessageConverter;
|
||||
import org.springframework.http.converter.multipart.MultipartHttpMessageConverter;
|
||||
import org.springframework.mock.http.client.MockClientHttpRequest;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
@@ -55,6 +56,7 @@ class MultipartRequestMatchersTests {
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
this.request.setMethod(HttpMethod.POST);
|
||||
this.request.getHeaders().setContentType(MediaType.MULTIPART_FORM_DATA);
|
||||
}
|
||||
|
||||
@@ -188,7 +190,7 @@ class MultipartRequestMatchersTests {
|
||||
}
|
||||
|
||||
private void writeForm() throws IOException {
|
||||
new FormHttpMessageConverter().write(this.input, MediaType.MULTIPART_FORM_DATA,
|
||||
new MultipartHttpMessageConverter().write(this.input, MediaType.MULTIPART_FORM_DATA,
|
||||
new HttpOutputMessage() {
|
||||
@Override
|
||||
public OutputStream getBody() throws IOException {
|
||||
|
||||
+36
-5
@@ -34,10 +34,10 @@ import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter;
|
||||
import org.springframework.http.converter.json.JsonbHttpMessageConverter;
|
||||
import org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverter;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
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.smile.MappingJackson2SmileHttpMessageConverter;
|
||||
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
|
||||
import org.springframework.http.converter.xml.JacksonXmlHttpMessageConverter;
|
||||
import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter;
|
||||
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
|
||||
@@ -120,6 +120,8 @@ class DefaultHttpMessageConverters implements HttpMessageConverters {
|
||||
|
||||
@Nullable HttpMessageConverter<?> resourceRegionConverter;
|
||||
|
||||
@Nullable HttpMessageConverter<?> formConverter;
|
||||
|
||||
@Nullable Consumer<HttpMessageConverter<?>> configurer;
|
||||
|
||||
@Nullable Consumer<List<HttpMessageConverter<?>>> convertersListConfigurer;
|
||||
@@ -175,6 +177,11 @@ class DefaultHttpMessageConverters implements HttpMessageConverters {
|
||||
this.stringConverter = stringConverter;
|
||||
}
|
||||
|
||||
public void setFormConverter(HttpMessageConverter<?> formConverter) {
|
||||
checkConverterSupports(formConverter, MediaType.APPLICATION_FORM_URLENCODED);
|
||||
this.formConverter = formConverter;
|
||||
}
|
||||
|
||||
void setKotlinSerializationJsonConverter(HttpMessageConverter<?> kotlinJsonConverter) {
|
||||
Assert.notNull(kotlinJsonConverter, "kotlinJsonConverter must not be null");
|
||||
this.kotlinJsonConverter = kotlinJsonConverter;
|
||||
@@ -241,6 +248,9 @@ class DefaultHttpMessageConverters implements HttpMessageConverters {
|
||||
if (this.stringConverter != null) {
|
||||
converters.add(this.stringConverter);
|
||||
}
|
||||
if (this.formConverter != null) {
|
||||
converters.add(this.formConverter);
|
||||
}
|
||||
return converters;
|
||||
}
|
||||
|
||||
@@ -289,6 +299,9 @@ class DefaultHttpMessageConverters implements HttpMessageConverters {
|
||||
if (this.stringConverter == null) {
|
||||
this.stringConverter = new StringHttpMessageConverter();
|
||||
}
|
||||
if (this.formConverter == null) {
|
||||
this.formConverter = new FormHttpMessageConverter();
|
||||
}
|
||||
if (this.kotlinJsonConverter == null) {
|
||||
if (KOTLIN_SERIALIZATION_JSON_PRESENT) {
|
||||
if (this.jsonConverter != null || JACKSON_PRESENT || JACKSON_2_PRESENT || GSON_PRESENT || JSONB_PRESENT) {
|
||||
@@ -401,6 +414,12 @@ class DefaultHttpMessageConverters implements HttpMessageConverters {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientBuilder withFormConverter(HttpMessageConverter<?> formMessageConverter) {
|
||||
setFormConverter(formMessageConverter);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientBuilder withKotlinSerializationJsonConverter(HttpMessageConverter<?> kotlinSerializationJsonConverter) {
|
||||
setKotlinSerializationJsonConverter(kotlinSerializationJsonConverter);
|
||||
@@ -487,14 +506,17 @@ class DefaultHttpMessageConverters implements HttpMessageConverters {
|
||||
List<HttpMessageConverter<?>> partConverters = new ArrayList<>(this.getCustomConverters());
|
||||
List<HttpMessageConverter<?>> allConverters = new ArrayList<>(this.getCustomConverters());
|
||||
if (this.registerDefaults) {
|
||||
partConverters.addAll(this.getCoreConverters());
|
||||
allConverters.addAll(this.getBaseConverters());
|
||||
if (this.resourceConverter != null) {
|
||||
allConverters.add(this.resourceConverter);
|
||||
}
|
||||
// use separate instances of base converters for multipart
|
||||
partConverters.addAll(List.of(new ByteArrayHttpMessageConverter(),
|
||||
new StringHttpMessageConverter(), new ResourceHttpMessageConverter()));
|
||||
partConverters.addAll(this.getCoreConverters());
|
||||
}
|
||||
if (!partConverters.isEmpty() || !allConverters.isEmpty()) {
|
||||
allConverters.add(new AllEncompassingFormHttpMessageConverter(partConverters));
|
||||
allConverters.add(new MultipartHttpMessageConverter(partConverters));
|
||||
}
|
||||
if (this.registerDefaults) {
|
||||
allConverters.addAll(this.getCoreConverters());
|
||||
@@ -530,6 +552,12 @@ class DefaultHttpMessageConverters implements HttpMessageConverters {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServerBuilder withFormConverter(HttpMessageConverter<?> formMessageConverter) {
|
||||
setFormConverter(formMessageConverter);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServerBuilder withKotlinSerializationJsonConverter(HttpMessageConverter<?> kotlinSerializationJsonConverter) {
|
||||
setKotlinSerializationJsonConverter(kotlinSerializationJsonConverter);
|
||||
@@ -614,7 +642,6 @@ class DefaultHttpMessageConverters implements HttpMessageConverters {
|
||||
List<HttpMessageConverter<?>> partConverters = new ArrayList<>(this.getCustomConverters());
|
||||
List<HttpMessageConverter<?>> allConverters = new ArrayList<>(this.getCustomConverters());
|
||||
if (this.registerDefaults) {
|
||||
partConverters.addAll(this.getCoreConverters());
|
||||
allConverters.addAll(this.getBaseConverters());
|
||||
if (this.resourceConverter != null) {
|
||||
allConverters.add(this.resourceConverter);
|
||||
@@ -622,9 +649,13 @@ class DefaultHttpMessageConverters implements HttpMessageConverters {
|
||||
if (this.resourceRegionConverter != null) {
|
||||
allConverters.add(this.resourceRegionConverter);
|
||||
}
|
||||
// use separate instances of base converters for multipart
|
||||
partConverters.addAll(List.of(new ByteArrayHttpMessageConverter(),
|
||||
new StringHttpMessageConverter(), new ResourceHttpMessageConverter()));
|
||||
partConverters.addAll(this.getCoreConverters());
|
||||
}
|
||||
if (!partConverters.isEmpty() || !allConverters.isEmpty()) {
|
||||
allConverters.add(new AllEncompassingFormHttpMessageConverter(partConverters));
|
||||
allConverters.add(new MultipartHttpMessageConverter(partConverters));
|
||||
}
|
||||
if (this.registerDefaults) {
|
||||
allConverters.addAll(this.getCoreConverters());
|
||||
|
||||
+31
-524
@@ -16,26 +16,17 @@
|
||||
|
||||
package org.springframework.http.converter;
|
||||
|
||||
import java.io.FilterOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URLDecoder;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.ContentDisposition;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.http.HttpInputMessage;
|
||||
import org.springframework.http.HttpOutputMessage;
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -43,280 +34,84 @@ import org.springframework.http.StreamingHttpOutputMessage;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MimeTypeUtils;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StreamUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Implementation of {@link HttpMessageConverter} to read and write 'normal' HTML
|
||||
* forms and also to write (but not read) multipart data (for example, file uploads).
|
||||
* Implementation of {@link HttpMessageConverter} to read and
|
||||
* write URL encoded forms. For multipart support, see the
|
||||
* {@link org.springframework.http.converter.multipart.MultipartHttpMessageConverter}.
|
||||
*
|
||||
* <p>In other words, this converter can read and write the
|
||||
* <p>This converter can read and write the
|
||||
* {@code "application/x-www-form-urlencoded"} media type as
|
||||
* {@link MultiValueMap MultiValueMap<String, String>}, and it can also
|
||||
* write (but not read) the {@code "multipart/form-data"} and
|
||||
* {@code "multipart/mixed"} media types as
|
||||
* {@link MultiValueMap MultiValueMap<String, Object>}.
|
||||
*
|
||||
* <h3>Multipart Data</h3>
|
||||
*
|
||||
* <p>By default, {@code "multipart/form-data"} is used as the content type when
|
||||
* {@linkplain #write writing} multipart data. It is also possible to write
|
||||
* multipart data using other multipart subtypes such as {@code "multipart/mixed"}
|
||||
* and {@code "multipart/related"}, as long as the multipart subtype is registered
|
||||
* as a {@linkplain #getSupportedMediaTypes supported media type} <em>and</em> the
|
||||
* desired multipart subtype is specified as the content type when
|
||||
* {@linkplain #write writing} the multipart data. Note that {@code "multipart/mixed"}
|
||||
* is registered as a supported media type by default.
|
||||
*
|
||||
* <p>When writing multipart data, this converter uses other
|
||||
* {@link HttpMessageConverter HttpMessageConverters} to write the respective
|
||||
* MIME parts. By default, basic converters are registered for byte array,
|
||||
* {@code String}, and {@code Resource}. These can be overridden via
|
||||
* {@link #setPartConverters} or augmented via {@link #addPartConverter}.
|
||||
* {@link MultiValueMap MultiValueMap<String, String>}.
|
||||
*
|
||||
* <h3>Examples</h3>
|
||||
*
|
||||
* <p>The following snippet shows how to submit an HTML form using the
|
||||
* {@code "multipart/form-data"} content type.
|
||||
* {@code "application/x-www-form-urlencoded"} content type.
|
||||
*
|
||||
* <pre class="code">
|
||||
* RestClient restClient = RestClient.create();
|
||||
* // AllEncompassingFormHttpMessageConverter is configured by default
|
||||
*
|
||||
* MultiValueMap<String, Object> form = new LinkedMultiValueMap<>();
|
||||
* MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
|
||||
* form.add("field 1", "value 1");
|
||||
* form.add("field 2", "value 2");
|
||||
* form.add("field 2", "value 3");
|
||||
* form.add("field 3", 4); // non-String form values supported as of 5.1.4
|
||||
* form.add("field 3", 4);
|
||||
*
|
||||
* ResponseEntity<Void> response = restClient.post()
|
||||
* .uri("https://example.com/myForm")
|
||||
* .contentType(MULTIPART_FORM_DATA)
|
||||
* .contentType(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
* .body(form)
|
||||
* .retrieve()
|
||||
* .toBodilessEntity();</pre>
|
||||
*
|
||||
* <p>The following snippet shows how to do a file upload using the
|
||||
* {@code "multipart/form-data"} content type.
|
||||
*
|
||||
* <pre class="code">
|
||||
* MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
|
||||
* parts.add("field 1", "value 1");
|
||||
* parts.add("file", new ClassPathResource("myFile.jpg"));
|
||||
*
|
||||
* ResponseEntity<Void> response = restClient.post()
|
||||
* .uri("https://example.com/myForm")
|
||||
* .contentType(MULTIPART_FORM_DATA)
|
||||
* .body(parts)
|
||||
* .retrieve()
|
||||
* .toBodilessEntity();</pre>
|
||||
*
|
||||
* <p>The following snippet shows how to do a file upload using the
|
||||
* {@code "multipart/mixed"} content type.
|
||||
*
|
||||
* <pre class="code">
|
||||
* MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
|
||||
* parts.add("field 1", "value 1");
|
||||
* parts.add("file", new ClassPathResource("myFile.jpg"));
|
||||
*
|
||||
* ResponseEntity<Void> response = restClient.post()
|
||||
* .uri("https://example.com/myForm")
|
||||
* .contentType(MULTIPART_MIXED)
|
||||
* .body(form)
|
||||
* .retrieve()
|
||||
* .toBodilessEntity();</pre>
|
||||
*
|
||||
* <p>The following snippet shows how to do a file upload using the
|
||||
* {@code "multipart/related"} content type.
|
||||
*
|
||||
* <pre class="code">
|
||||
* restClient = restClient.mutate()
|
||||
* .messageConverters(l -> l.stream()
|
||||
* .filter(FormHttpMessageConverter.class::isInstance)
|
||||
* .map(FormHttpMessageConverter.class::cast)
|
||||
* .findFirst()
|
||||
* .orElseThrow(() -> new IllegalStateException("Failed to find FormHttpMessageConverter"))
|
||||
* .addSupportedMediaTypes(MULTIPART_RELATED);
|
||||
*
|
||||
* MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
|
||||
* parts.add("field 1", "value 1");
|
||||
* parts.add("file", new ClassPathResource("myFile.jpg"));
|
||||
*
|
||||
* ResponseEntity<Void> response = restClient.post()
|
||||
* .uri("https://example.com/myForm")
|
||||
* .contentType(MULTIPART_RELATED)
|
||||
* .body(form)
|
||||
* .retrieve()
|
||||
* .toBodilessEntity();</pre>
|
||||
*
|
||||
* <h3>Miscellaneous</h3>
|
||||
*
|
||||
* <p>Some methods in this class were inspired by
|
||||
* {@code org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity}.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Juergen Hoeller
|
||||
* @author Sam Brannen
|
||||
* @author Brian Clozel
|
||||
* @since 3.0
|
||||
* @see org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter
|
||||
* @see org.springframework.util.MultiValueMap
|
||||
*/
|
||||
public class FormHttpMessageConverter implements HttpMessageConverter<MultiValueMap<String, ?>> {
|
||||
public class FormHttpMessageConverter implements SmartHttpMessageConverter<MultiValueMap<String, String>> {
|
||||
|
||||
/** The default charset used by the converter. */
|
||||
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
|
||||
|
||||
|
||||
private List<MediaType> supportedMediaTypes = new ArrayList<>();
|
||||
|
||||
private List<HttpMessageConverter<?>> partConverters = new ArrayList<>();
|
||||
|
||||
private Charset charset = DEFAULT_CHARSET;
|
||||
|
||||
private @Nullable Charset multipartCharset;
|
||||
|
||||
|
||||
public FormHttpMessageConverter() {
|
||||
this.supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
|
||||
this.supportedMediaTypes.add(MediaType.MULTIPART_FORM_DATA);
|
||||
this.supportedMediaTypes.add(MediaType.MULTIPART_MIXED);
|
||||
this.supportedMediaTypes.add(MediaType.MULTIPART_RELATED);
|
||||
|
||||
this.partConverters.add(new ByteArrayHttpMessageConverter());
|
||||
this.partConverters.add(new StringHttpMessageConverter());
|
||||
this.partConverters.add(new ResourceHttpMessageConverter());
|
||||
|
||||
applyDefaultCharset();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the list of {@link MediaType} objects supported by this converter.
|
||||
* @see #addSupportedMediaTypes(MediaType...)
|
||||
* @see #getSupportedMediaTypes()
|
||||
*/
|
||||
public void setSupportedMediaTypes(List<MediaType> supportedMediaTypes) {
|
||||
Assert.notNull(supportedMediaTypes, "'supportedMediaTypes' must not be null");
|
||||
// Ensure internal list is mutable.
|
||||
this.supportedMediaTypes = new ArrayList<>(supportedMediaTypes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add {@link MediaType} objects to be supported by this converter.
|
||||
* <p>The supplied {@code MediaType} objects will be appended to the list
|
||||
* of {@linkplain #getSupportedMediaTypes() supported MediaType objects}.
|
||||
* @param supportedMediaTypes a var-args list of {@code MediaType} objects to add
|
||||
* @since 5.2
|
||||
* @see #setSupportedMediaTypes(List)
|
||||
*/
|
||||
public void addSupportedMediaTypes(MediaType... supportedMediaTypes) {
|
||||
Assert.notNull(supportedMediaTypes, "'supportedMediaTypes' must not be null");
|
||||
Assert.noNullElements(supportedMediaTypes, "'supportedMediaTypes' must not contain null elements");
|
||||
Collections.addAll(this.supportedMediaTypes, supportedMediaTypes);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
* @see #setSupportedMediaTypes(List)
|
||||
* @see #addSupportedMediaTypes(MediaType...)
|
||||
*/
|
||||
@Override
|
||||
public List<MediaType> getSupportedMediaTypes() {
|
||||
return Collections.unmodifiableList(this.supportedMediaTypes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the message body converters to use. These converters are used to
|
||||
* convert objects to MIME parts.
|
||||
*/
|
||||
public void setPartConverters(List<HttpMessageConverter<?>> partConverters) {
|
||||
Assert.notEmpty(partConverters, "'partConverters' must not be empty");
|
||||
this.partConverters = partConverters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the {@linkplain #setPartConverters configured converters} for MIME
|
||||
* parts.
|
||||
* @since 5.3
|
||||
*/
|
||||
public List<HttpMessageConverter<?>> getPartConverters() {
|
||||
return Collections.unmodifiableList(this.partConverters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a message body converter. Such a converter is used to convert objects
|
||||
* to MIME parts.
|
||||
*/
|
||||
public void addPartConverter(HttpMessageConverter<?> partConverter) {
|
||||
Assert.notNull(partConverter, "'partConverter' must not be null");
|
||||
this.partConverters.add(partConverter);
|
||||
return List.of(MediaType.APPLICATION_FORM_URLENCODED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default character set to use for reading and writing form data when
|
||||
* the request or response {@code Content-Type} header does not explicitly
|
||||
* specify it.
|
||||
* <p>As of 4.3, this is also used as the default charset for the conversion
|
||||
* of text bodies in a multipart request.
|
||||
* <p>As of 5.0, this is also used for part headers including
|
||||
* {@code Content-Disposition} (and its filename parameter) unless (the mutually
|
||||
* exclusive) {@link #setMultipartCharset multipartCharset} is also set, in
|
||||
* which case part headers are encoded as ASCII and <i>filename</i> is encoded
|
||||
* with the {@code encoded-word} syntax from RFC 2047.
|
||||
* <p>By default this is set to "UTF-8".
|
||||
* <p>By default, this is set to "UTF-8".
|
||||
*/
|
||||
public void setCharset(@Nullable Charset charset) {
|
||||
if (charset != this.charset) {
|
||||
this.charset = (charset != null ? charset : DEFAULT_CHARSET);
|
||||
applyDefaultCharset();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the configured charset as a default to registered part converters.
|
||||
*/
|
||||
private void applyDefaultCharset() {
|
||||
for (HttpMessageConverter<?> candidate : this.partConverters) {
|
||||
if (candidate instanceof AbstractHttpMessageConverter<?> converter) {
|
||||
// Only override default charset if the converter operates with a charset to begin with...
|
||||
if (converter.getDefaultCharset() != null) {
|
||||
converter.setDefaultCharset(this.charset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the character set to use when writing multipart data to encode file
|
||||
* names. Encoding is based on the {@code encoded-word} syntax defined in
|
||||
* RFC 2047 and relies on {@code MimeUtility} from {@code jakarta.mail}.
|
||||
* <p>As of 5.0 by default part headers, including {@code Content-Disposition}
|
||||
* (and its filename parameter) will be encoded based on the setting of
|
||||
* {@link #setCharset(Charset)} or {@code UTF-8} by default.
|
||||
* @since 4.1.1
|
||||
* @see <a href="https://en.wikipedia.org/wiki/MIME#Encoded-Word">Encoded-Word</a>
|
||||
*/
|
||||
public void setMultipartCharset(Charset charset) {
|
||||
this.multipartCharset = charset;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean canRead(Class<?> clazz, @Nullable MediaType mediaType) {
|
||||
if (!MultiValueMap.class.isAssignableFrom(clazz)) {
|
||||
public boolean canRead(ResolvableType type, @Nullable MediaType mediaType) {
|
||||
if (!MultiValueMap.class.isAssignableFrom(type.toClass()) ||
|
||||
(!type.hasUnresolvableGenerics() &&
|
||||
!String.class.isAssignableFrom(type.getGeneric(1).toClass()))) {
|
||||
return false;
|
||||
}
|
||||
if (mediaType == null) {
|
||||
return true;
|
||||
}
|
||||
for (MediaType supportedMediaType : getSupportedMediaTypes()) {
|
||||
if (supportedMediaType.getType().equalsIgnoreCase("multipart")) {
|
||||
// We can't read multipart, so skip this supported media type.
|
||||
continue;
|
||||
}
|
||||
if (supportedMediaType.includes(mediaType)) {
|
||||
return true;
|
||||
}
|
||||
@@ -325,13 +120,12 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) {
|
||||
if (!MultiValueMap.class.isAssignableFrom(clazz)) {
|
||||
public boolean canWrite(ResolvableType targetType, Class<?> valueClass, @Nullable MediaType mediaType) {
|
||||
if (!MultiValueMap.class.isAssignableFrom(targetType.toClass()) ||
|
||||
(!targetType.hasUnresolvableGenerics() &&
|
||||
!String.class.isAssignableFrom(targetType.getGeneric(1).toClass()))) {
|
||||
return false;
|
||||
}
|
||||
if (mediaType == null || MediaType.ALL.equals(mediaType)) {
|
||||
return true;
|
||||
}
|
||||
for (MediaType supportedMediaType : getSupportedMediaTypes()) {
|
||||
if (supportedMediaType.isCompatibleWith(mediaType)) {
|
||||
return true;
|
||||
@@ -341,8 +135,8 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
|
||||
}
|
||||
|
||||
@Override
|
||||
public MultiValueMap<String, String> read(@Nullable Class<? extends MultiValueMap<String, ?>> clazz,
|
||||
HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
|
||||
public MultiValueMap<String, String> read(ResolvableType type, HttpInputMessage inputMessage, @Nullable Map<String, Object> hints)
|
||||
throws IOException, HttpMessageNotReadableException {
|
||||
|
||||
MediaType contentType = inputMessage.getHeaders().getContentType();
|
||||
Charset charset = (contentType != null && contentType.getCharset() != null ?
|
||||
@@ -372,39 +166,13 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public void write(MultiValueMap<String, ?> map, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
|
||||
throws IOException, HttpMessageNotWritableException {
|
||||
public void write(MultiValueMap<String, String> formData, ResolvableType type, @Nullable MediaType contentType,
|
||||
HttpOutputMessage outputMessage, @Nullable Map<String, Object> hints) throws IOException, HttpMessageNotWritableException {
|
||||
|
||||
if (isMultipart(map, contentType)) {
|
||||
writeMultipart((MultiValueMap<String, Object>) map, contentType, outputMessage);
|
||||
}
|
||||
else {
|
||||
writeForm((MultiValueMap<String, Object>) map, contentType, outputMessage);
|
||||
}
|
||||
}
|
||||
contentType = getFormContentType(contentType);
|
||||
outputMessage.getHeaders().setContentType(contentType);
|
||||
|
||||
|
||||
private boolean isMultipart(MultiValueMap<String, ?> map, @Nullable MediaType contentType) {
|
||||
if (contentType != null) {
|
||||
return contentType.getType().equalsIgnoreCase("multipart");
|
||||
}
|
||||
for (List<?> values : map.values()) {
|
||||
for (Object value : values) {
|
||||
if (value != null && !(value instanceof String)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void writeForm(MultiValueMap<String, Object> formData, @Nullable MediaType mediaType,
|
||||
HttpOutputMessage outputMessage) throws IOException {
|
||||
|
||||
mediaType = getFormContentType(mediaType);
|
||||
outputMessage.getHeaders().setContentType(mediaType);
|
||||
|
||||
Charset charset = (mediaType.getCharset() != null ? mediaType.getCharset() : this.charset);
|
||||
Charset charset = (contentType.getCharset() != null ? contentType.getCharset() : this.charset);
|
||||
|
||||
byte[] bytes = serializeForm(formData, charset).getBytes(charset);
|
||||
outputMessage.getHeaders().setContentLength(bytes.length);
|
||||
@@ -436,7 +204,7 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
|
||||
return contentType;
|
||||
}
|
||||
|
||||
protected String serializeForm(MultiValueMap<String, Object> formData, Charset charset) {
|
||||
protected String serializeForm(MultiValueMap<String, String> formData, Charset charset) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
formData.forEach((name, values) -> {
|
||||
if (name == null) {
|
||||
@@ -450,7 +218,7 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
|
||||
builder.append(URLEncoder.encode(name, charset));
|
||||
if (value != null) {
|
||||
builder.append('=');
|
||||
builder.append(URLEncoder.encode(String.valueOf(value), charset));
|
||||
builder.append(URLEncoder.encode(value, charset));
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -458,265 +226,4 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private void writeMultipart(
|
||||
MultiValueMap<String, Object> parts, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
|
||||
throws IOException {
|
||||
|
||||
// If the supplied content type is null, fall back to multipart/form-data.
|
||||
// Otherwise rely on the fact that isMultipart() already verified the
|
||||
// supplied content type is multipart.
|
||||
if (contentType == null) {
|
||||
contentType = MediaType.MULTIPART_FORM_DATA;
|
||||
}
|
||||
|
||||
Map<String, String> parameters = new LinkedHashMap<>(contentType.getParameters().size() + 2);
|
||||
parameters.putAll(contentType.getParameters());
|
||||
|
||||
byte[] boundary = generateMultipartBoundary();
|
||||
if (!isFilenameCharsetSet()) {
|
||||
if (!this.charset.equals(StandardCharsets.UTF_8) &&
|
||||
!this.charset.equals(StandardCharsets.US_ASCII)) {
|
||||
parameters.put("charset", this.charset.name());
|
||||
}
|
||||
}
|
||||
parameters.put("boundary", new String(boundary, StandardCharsets.US_ASCII));
|
||||
|
||||
// Add parameters to output content type
|
||||
contentType = new MediaType(contentType, parameters);
|
||||
outputMessage.getHeaders().setContentType(contentType);
|
||||
|
||||
if (outputMessage instanceof StreamingHttpOutputMessage streamingOutputMessage) {
|
||||
boolean repeatable = checkPartsRepeatable(parts, contentType);
|
||||
streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
|
||||
@Override
|
||||
public void writeTo(OutputStream outputStream) throws IOException {
|
||||
FormHttpMessageConverter.this.writeParts(outputStream, parts, boundary);
|
||||
writeEnd(outputStream, boundary);
|
||||
}
|
||||
@Override
|
||||
public boolean repeatable() {
|
||||
return repeatable;
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
writeParts(outputMessage.getBody(), parts, boundary);
|
||||
writeEnd(outputMessage.getBody(), boundary);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked", "ConstantValue"})
|
||||
private <T> boolean checkPartsRepeatable(MultiValueMap<String, Object> map, MediaType contentType) {
|
||||
return map.entrySet().stream().allMatch(e -> e.getValue().stream().filter(Objects::nonNull).allMatch(part -> {
|
||||
HttpHeaders headers = null;
|
||||
Object body = part;
|
||||
if (part instanceof HttpEntity<?> entity) {
|
||||
headers = entity.getHeaders();
|
||||
body = entity.getBody();
|
||||
Assert.state(body != null, "Empty body for part '" + e.getKey() + "': " + part);
|
||||
}
|
||||
HttpMessageConverter<T> converter = (HttpMessageConverter<T>) findConverterFor(e.getKey(), headers, body);
|
||||
return converter != null && converter.canWriteRepeatedly((T) body, contentType);
|
||||
}));
|
||||
}
|
||||
|
||||
private @Nullable HttpMessageConverter<?> findConverterFor(
|
||||
String name, @Nullable HttpHeaders headers, Object body) {
|
||||
|
||||
Class<?> partType = body.getClass();
|
||||
MediaType contentType = (headers != null ? headers.getContentType() : null);
|
||||
for (HttpMessageConverter<?> converter : this.partConverters) {
|
||||
if (converter.canWrite(partType, contentType)) {
|
||||
return converter;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* When {@link #setMultipartCharset(Charset)} is configured (i.e. RFC 2047,
|
||||
* {@code encoded-word} syntax) we need to use ASCII for part headers, or
|
||||
* otherwise we encode directly using the configured {@link #setCharset(Charset)}.
|
||||
*/
|
||||
private boolean isFilenameCharsetSet() {
|
||||
return (this.multipartCharset != null);
|
||||
}
|
||||
|
||||
private void writeParts(OutputStream os, MultiValueMap<String, Object> parts, byte[] boundary) throws IOException {
|
||||
for (Map.Entry<String, List<Object>> entry : parts.entrySet()) {
|
||||
String name = entry.getKey();
|
||||
for (Object part : entry.getValue()) {
|
||||
if (part != null) {
|
||||
writeBoundary(os, boundary);
|
||||
writePart(name, getHttpEntity(part), os);
|
||||
writeNewLine(os);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void writePart(String name, HttpEntity<?> partEntity, OutputStream os) throws IOException {
|
||||
Object partBody = partEntity.getBody();
|
||||
Assert.state(partBody != null, "Empty body for part '" + name + "': " + partEntity);
|
||||
HttpHeaders partHeaders = partEntity.getHeaders();
|
||||
MediaType partContentType = partHeaders.getContentType();
|
||||
HttpMessageConverter<?> converter = findConverterFor(name, partHeaders, partBody);
|
||||
if (converter != null) {
|
||||
Charset charset = isFilenameCharsetSet() ? StandardCharsets.US_ASCII : this.charset;
|
||||
HttpOutputMessage multipartMessage = new MultipartHttpOutputMessage(os, charset);
|
||||
String filename = getFilename(partBody);
|
||||
ContentDisposition.Builder cd = ContentDisposition.formData().name(name);
|
||||
if (filename != null) {
|
||||
cd.filename(filename, this.multipartCharset);
|
||||
}
|
||||
multipartMessage.getHeaders().setContentDisposition(cd.build());
|
||||
if (!partHeaders.isEmpty()) {
|
||||
multipartMessage.getHeaders().putAll(partHeaders);
|
||||
}
|
||||
((HttpMessageConverter<Object>) converter).write(partBody, partContentType, multipartMessage);
|
||||
return;
|
||||
}
|
||||
throw new HttpMessageNotWritableException("Could not write request: " +
|
||||
"no suitable HttpMessageConverter found for request type [" + partBody.getClass().getName() + "]");
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a multipart boundary.
|
||||
* <p>This implementation delegates to
|
||||
* {@link MimeTypeUtils#generateMultipartBoundary()}.
|
||||
*/
|
||||
protected byte[] generateMultipartBoundary() {
|
||||
return MimeTypeUtils.generateMultipartBoundary();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an {@link HttpEntity} for the given part Object.
|
||||
* @param part the part to return an {@link HttpEntity} for
|
||||
* @return the part Object itself it is an {@link HttpEntity},
|
||||
* or a newly built {@link HttpEntity} wrapper for that part
|
||||
*/
|
||||
protected HttpEntity<?> getHttpEntity(Object part) {
|
||||
return (part instanceof HttpEntity<?> httpEntity ? httpEntity : new HttpEntity<>(part));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the filename of the given multipart part. This value will be used for the
|
||||
* {@code Content-Disposition} header.
|
||||
* <p>The default implementation returns {@link Resource#getFilename()} if the part is a
|
||||
* {@code Resource}, and {@code null} in other cases. Can be overridden in subclasses.
|
||||
* @param part the part to determine the file name for
|
||||
* @return the filename, or {@code null} if not known
|
||||
*/
|
||||
protected @Nullable String getFilename(Object part) {
|
||||
if (part instanceof Resource resource) {
|
||||
return resource.getFilename();
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void writeBoundary(OutputStream os, byte[] boundary) throws IOException {
|
||||
os.write('-');
|
||||
os.write('-');
|
||||
os.write(boundary);
|
||||
writeNewLine(os);
|
||||
}
|
||||
|
||||
private static void writeEnd(OutputStream os, byte[] boundary) throws IOException {
|
||||
os.write('-');
|
||||
os.write('-');
|
||||
os.write(boundary);
|
||||
os.write('-');
|
||||
os.write('-');
|
||||
writeNewLine(os);
|
||||
}
|
||||
|
||||
private static void writeNewLine(OutputStream os) throws IOException {
|
||||
os.write('\r');
|
||||
os.write('\n');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Implementation of {@link org.springframework.http.HttpOutputMessage} used
|
||||
* to write a MIME multipart.
|
||||
*/
|
||||
private static class MultipartHttpOutputMessage implements HttpOutputMessage {
|
||||
|
||||
private final OutputStream outputStream;
|
||||
|
||||
private final Charset charset;
|
||||
|
||||
private final HttpHeaders headers = new HttpHeaders();
|
||||
|
||||
private boolean headersWritten = false;
|
||||
|
||||
public MultipartHttpOutputMessage(OutputStream outputStream, Charset charset) {
|
||||
this.outputStream = new MultipartOutputStream(outputStream);
|
||||
this.charset = charset;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpHeaders getHeaders() {
|
||||
return (this.headersWritten ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputStream getBody() throws IOException {
|
||||
writeHeaders();
|
||||
return this.outputStream;
|
||||
}
|
||||
|
||||
private void writeHeaders() throws IOException {
|
||||
if (!this.headersWritten) {
|
||||
for (Map.Entry<String, List<String>> entry : this.headers.headerSet()) {
|
||||
byte[] headerName = getBytes(entry.getKey());
|
||||
for (String headerValueString : entry.getValue()) {
|
||||
byte[] headerValue = getBytes(headerValueString);
|
||||
this.outputStream.write(headerName);
|
||||
this.outputStream.write(':');
|
||||
this.outputStream.write(' ');
|
||||
this.outputStream.write(headerValue);
|
||||
writeNewLine(this.outputStream);
|
||||
}
|
||||
}
|
||||
writeNewLine(this.outputStream);
|
||||
this.headersWritten = true;
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] getBytes(String name) {
|
||||
return name.getBytes(this.charset);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* OutputStream that neither flushes nor closes.
|
||||
*/
|
||||
private static class MultipartOutputStream extends FilterOutputStream {
|
||||
|
||||
public MultipartOutputStream(OutputStream out) {
|
||||
super(out);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] b, int off, int let) throws IOException {
|
||||
this.out.write(b, off, let);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -114,6 +114,14 @@ public interface HttpMessageConverters extends Iterable<HttpMessageConverter<?>>
|
||||
*/
|
||||
T withStringConverter(HttpMessageConverter<?> stringMessageConverter);
|
||||
|
||||
/**
|
||||
* Override the default {@code HttpMessageConverter} for URL encoded forms.
|
||||
* @param formMessageConverter the converter instance to use
|
||||
* @since 7.1
|
||||
* @see FormHttpMessageConverter
|
||||
*/
|
||||
T withFormConverter(HttpMessageConverter<?> formMessageConverter);
|
||||
|
||||
/**
|
||||
* Override the default String {@code HttpMessageConverter}
|
||||
* with any converter supporting the Kotlin Serialization conversion for JSON.
|
||||
|
||||
+1
@@ -170,6 +170,7 @@ public class MultipartHttpMessageConverter implements SmartHttpMessageConverter<
|
||||
|
||||
this.partConverters = new ArrayList<>();
|
||||
converters.forEach(this.partConverters::add);
|
||||
applyDefaultCharset();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+7
-6
@@ -16,12 +16,12 @@
|
||||
|
||||
package org.springframework.http.converter.support;
|
||||
|
||||
import org.springframework.http.converter.FormHttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageConverters;
|
||||
import org.springframework.http.converter.multipart.MultipartHttpMessageConverter;
|
||||
|
||||
/**
|
||||
* Extension of {@link org.springframework.http.converter.FormHttpMessageConverter},
|
||||
* Extension of {@link MultipartHttpMessageConverter},
|
||||
* adding support for XML, JSON, Smile, CBOR, Protobuf and Yaml based parts when
|
||||
* related libraries are present in the classpath.
|
||||
*
|
||||
@@ -29,17 +29,18 @@ import org.springframework.http.converter.HttpMessageConverters;
|
||||
* @author Juergen Hoeller
|
||||
* @author Sebastien Deleuze
|
||||
* @since 3.2
|
||||
* @deprecated since 7.1 in favor of {@link MultipartHttpMessageConverter}.
|
||||
*/
|
||||
public class AllEncompassingFormHttpMessageConverter extends FormHttpMessageConverter {
|
||||
@Deprecated(since = "7.1", forRemoval = true)
|
||||
public class AllEncompassingFormHttpMessageConverter extends MultipartHttpMessageConverter {
|
||||
|
||||
|
||||
/**
|
||||
* Create a new {@link AllEncompassingFormHttpMessageConverter} instance
|
||||
* that will auto-detect part converters.
|
||||
*/
|
||||
@SuppressWarnings("removal")
|
||||
public AllEncompassingFormHttpMessageConverter() {
|
||||
HttpMessageConverters.forClient().registerDefaults().build().forEach(this::addPartConverter);
|
||||
super(HttpMessageConverters.forClient().registerDefaults().build());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,7 +50,7 @@ public class AllEncompassingFormHttpMessageConverter extends FormHttpMessageConv
|
||||
* @since 7.0
|
||||
*/
|
||||
public AllEncompassingFormHttpMessageConverter(Iterable<HttpMessageConverter<?>> converters) {
|
||||
converters.forEach(this::addPartConverter);
|
||||
super(converters);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -36,10 +36,10 @@ import jakarta.servlet.http.HttpServletRequestWrapper;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.http.HttpInputMessage;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.FormHttpMessageConverter;
|
||||
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
|
||||
import org.springframework.http.server.ServletServerHttpRequest;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
@@ -48,7 +48,7 @@ import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* {@code Filter} that parses form data for HTTP PUT, PATCH, and DELETE requests
|
||||
* and exposes it as Servlet request parameters. By default the Servlet spec
|
||||
* and exposes it as Servlet request parameters. By default, the Servlet spec
|
||||
* only requires this for HTTP POST.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
@@ -58,12 +58,12 @@ public class FormContentFilter extends OncePerRequestFilter {
|
||||
|
||||
private static final List<String> HTTP_METHODS = Arrays.asList("PUT", "PATCH", "DELETE");
|
||||
|
||||
private FormHttpMessageConverter formConverter = new AllEncompassingFormHttpMessageConverter();
|
||||
private FormHttpMessageConverter formConverter = new FormHttpMessageConverter();
|
||||
|
||||
|
||||
/**
|
||||
* Set the converter to use for parsing form content.
|
||||
* <p>By default this is an instance of {@link AllEncompassingFormHttpMessageConverter}.
|
||||
* <p>By default, this is an instance of {@link FormHttpMessageConverter}.
|
||||
*/
|
||||
public void setFormConverter(FormHttpMessageConverter converter) {
|
||||
Assert.notNull(converter, "FormHttpMessageConverter is required");
|
||||
@@ -105,7 +105,7 @@ public class FormContentFilter extends OncePerRequestFilter {
|
||||
return request.getInputStream();
|
||||
}
|
||||
};
|
||||
return this.formConverter.read(null, inputMessage);
|
||||
return this.formConverter.read(ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class), inputMessage, null);
|
||||
}
|
||||
|
||||
private boolean shouldParse(HttpServletRequest request) {
|
||||
|
||||
+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);
|
||||
|
||||
+6
-2
@@ -36,9 +36,11 @@ import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
|
||||
import org.springframework.http.converter.FormHttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageConverters;
|
||||
import org.springframework.http.converter.StringHttpMessageConverter;
|
||||
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
|
||||
import org.springframework.http.converter.multipart.MultipartHttpMessageConverter;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.web.ErrorResponse;
|
||||
import org.springframework.web.HttpMediaTypeNotAcceptableException;
|
||||
@@ -293,7 +295,9 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce
|
||||
}
|
||||
this.messageConverters.add(new ByteArrayHttpMessageConverter());
|
||||
this.messageConverters.add(new StringHttpMessageConverter());
|
||||
this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());
|
||||
this.messageConverters.add(new FormHttpMessageConverter());
|
||||
this.messageConverters.add(new MultipartHttpMessageConverter(HttpMessageConverters.forServer()
|
||||
.registerDefaults().build()));
|
||||
}
|
||||
|
||||
private void initExceptionHandlerAdviceCache() {
|
||||
|
||||
+6
-2
@@ -51,9 +51,11 @@ import org.springframework.core.log.LogFormatUtils;
|
||||
import org.springframework.core.task.AsyncTaskExecutor;
|
||||
import org.springframework.core.task.SimpleAsyncTaskExecutor;
|
||||
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
|
||||
import org.springframework.http.converter.FormHttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageConverters;
|
||||
import org.springframework.http.converter.StringHttpMessageConverter;
|
||||
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
|
||||
import org.springframework.http.converter.multipart.MultipartHttpMessageConverter;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
@@ -579,7 +581,9 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
|
||||
this.messageConverters.add(new ByteArrayHttpMessageConverter());
|
||||
this.messageConverters.add(new StringHttpMessageConverter());
|
||||
|
||||
this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());
|
||||
this.messageConverters.add(new FormHttpMessageConverter());
|
||||
this.messageConverters.add(new MultipartHttpMessageConverter(HttpMessageConverters.forServer()
|
||||
.registerDefaults().build()));
|
||||
}
|
||||
|
||||
private void initControllerAdviceCache() {
|
||||
|
||||
+2
-2
@@ -39,7 +39,7 @@ import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageConverters;
|
||||
import org.springframework.http.converter.StringHttpMessageConverter;
|
||||
import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter;
|
||||
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
|
||||
import org.springframework.http.converter.multipart.MultipartHttpMessageConverter;
|
||||
import org.springframework.scheduling.concurrent.ConcurrentTaskExecutor;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
@@ -213,7 +213,7 @@ class WebMvcConfigurationSupportExtensionTests {
|
||||
assertThat(converters).hasSize(3);
|
||||
assertThat(converters.get(0).getClass()).isEqualTo(StringHttpMessageConverter.class);
|
||||
assertThat(converters.get(1).getClass()).isEqualTo(JacksonJsonHttpMessageConverter.class);
|
||||
assertThat(converters.get(2).getClass()).isEqualTo(AllEncompassingFormHttpMessageConverter.class);
|
||||
assertThat(converters.get(2).getClass()).isEqualTo(MultipartHttpMessageConverter.class);
|
||||
JsonMapper jsonMapper = ((JacksonJsonHttpMessageConverter) converters.get(1)).getMapper();
|
||||
assertThat(jsonMapper.deserializationConfig().isEnabled(MapperFeature.DEFAULT_VIEW_INCLUSION)).isFalse();
|
||||
assertThat(jsonMapper.deserializationConfig().isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)).isFalse();
|
||||
|
||||
+2
-3
@@ -51,7 +51,7 @@ import org.springframework.http.converter.ByteArrayHttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.ResourceHttpMessageConverter;
|
||||
import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter;
|
||||
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
|
||||
import org.springframework.http.converter.multipart.MultipartHttpMessageConverter;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.util.FileSystemUtils;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
@@ -135,8 +135,7 @@ class RequestPartIntegrationTests {
|
||||
converters.add(new ResourceHttpMessageConverter());
|
||||
converters.add(new JacksonJsonHttpMessageConverter());
|
||||
|
||||
AllEncompassingFormHttpMessageConverter converter = new AllEncompassingFormHttpMessageConverter();
|
||||
converter.setPartConverters(converters);
|
||||
MultipartHttpMessageConverter converter = new MultipartHttpMessageConverter(converters);
|
||||
|
||||
restTemplate = new RestTemplate(new HttpComponentsClientHttpRequestFactory());
|
||||
restTemplate.setMessageConverters(Collections.singletonList(converter));
|
||||
|
||||
+2
-2
@@ -49,13 +49,13 @@ import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ProblemDetail;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
|
||||
import org.springframework.http.converter.FormHttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.http.converter.ResourceHttpMessageConverter;
|
||||
import org.springframework.http.converter.StringHttpMessageConverter;
|
||||
import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
|
||||
import org.springframework.http.converter.xml.JacksonXmlHttpMessageConverter;
|
||||
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
@@ -135,7 +135,7 @@ class RequestResponseBodyMethodProcessorTests {
|
||||
this.servletRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
|
||||
this.servletRequest.setContentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE);
|
||||
|
||||
List<HttpMessageConverter<?>> converters = List.of(new AllEncompassingFormHttpMessageConverter());
|
||||
List<HttpMessageConverter<?>> converters = List.of(new FormHttpMessageConverter());
|
||||
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
|
||||
Reference in New Issue
Block a user