diff --git a/framework-docs/modules/ROOT/pages/integration/rest-clients.adoc b/framework-docs/modules/ROOT/pages/integration/rest-clients.adoc index 4dd3ce2daed..52f1e25aa6f 100644 --- a/framework-docs/modules/ROOT/pages/integration/rest-clients.adoc +++ b/framework-docs/modules/ROOT/pages/integration/rest-clients.adoc @@ -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` as source instance +or a target type. + +For example: + +[source,java,indent=0,subs="verbatim"] +---- + MultiValueMap form = new LinkedMultiValueMap<>(); + form.add("project", "Spring Framework"); + form.add("module", "spring-web"); + ResponseEntity response = this.restClient.post() + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .body(form) + .retrieve() + .toBodilessEntity(); +---- + + ==== Multipart To send multipart data, you need to provide a `MultiValueMap` 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 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 diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/message-converters.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/message-converters.adoc index d65c68aab7f..72726b105bb 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/message-converters.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/message-converters.adoc @@ -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`. -The converter can also write (but not read) multipart data read from a `MultiValueMap`. -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` can be written to multipart messages, converting each part independently using +the configured message converters. Multipart messages can be read into `MultiValueMap`, 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. diff --git a/spring-test/src/main/java/org/springframework/test/web/client/match/ContentRequestMatchers.java b/spring-test/src/main/java/org/springframework/test/web/client/match/ContentRequestMatchers.java index 58dbbfb4b57..0213190dfac 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/match/ContentRequestMatchers.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/match/ContentRequestMatchers.java @@ -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 actualMap = new FormHttpMessageConverter().read(null, message); + MultiValueMap actualMap = new FormHttpMessageConverter() + .read(ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class), message, null); if (containsExactly) { assertEquals("Form data", expectedMap, actualMap); } diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/request/AbstractMockHttpServletRequestBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/request/AbstractMockHttpServletRequestBuilder.java index ac46d9b660c..e5ba86d373c 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/request/AbstractMockHttpServletRequestBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/request/AbstractMockHttpServletRequestBuilder.java @@ -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 parseFormData(MediaType mediaType) { HttpInputMessage message = new HttpInputMessage() { @Override @@ -1038,7 +1038,8 @@ public abstract class AbstractMockHttpServletRequestBuilder resourceRegionConverter; + @Nullable HttpMessageConverter formConverter; + @Nullable Consumer> configurer; @Nullable Consumer>> 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> partConverters = new ArrayList<>(this.getCustomConverters()); List> 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> partConverters = new ArrayList<>(this.getCustomConverters()); List> 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()); diff --git a/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java index 193a5429949..039de5a0823 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java @@ -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}. * - *

In other words, this converter can read and write the + *

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>}. - * - *

Multipart Data

- * - *

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} and 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. - * - *

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>}. * *

Examples

* *

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. * *

  * 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
- *
- * ResponseEntity<Void> response = restClient.post()
- *   .uri("https://example.com/myForm")
- *   .contentType(MULTIPART_FORM_DATA)
- *   .body(form)
- *   .retrieve()
- *   .toBodilessEntity();
- * - *

The following snippet shows how to do a file upload using the - * {@code "multipart/form-data"} content type. - * - *

- * 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();
- * - *

The following snippet shows how to do a file upload using the - * {@code "multipart/mixed"} content type. - * - *

- * 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();
- * - *

The following snippet shows how to do a file upload using the - * {@code "multipart/related"} content type. - * - *

- * 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"));
+ * form.add("field 3", 4);
  *
  * ResponseEntity<Void> response = restClient.post()
  *   .uri("https://example.com/myForm")
- *   .contentType(MULTIPART_RELATED)
+ *   .contentType(MediaType.APPLICATION_FORM_URLENCODED)
  *   .body(form)
  *   .retrieve()
  *   .toBodilessEntity();
* - *

Miscellaneous

- * - *

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> { +public class FormHttpMessageConverter implements SmartHttpMessageConverter> { /** The default charset used by the converter. */ public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; - - private List supportedMediaTypes = new ArrayList<>(); - - private List> 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 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. - *

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 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> 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> 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. - *

As of 4.3, this is also used as the default charset for the conversion - * of text bodies in a multipart request. - *

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 filename is encoded - * with the {@code encoded-word} syntax from RFC 2047. - *

By default this is set to "UTF-8". + *

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}. - *

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 Encoded-Word - */ - 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 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 read(@Nullable Class> clazz, - HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { + public MultiValueMap read(ResolvableType type, HttpInputMessage inputMessage, @Nullable Map 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 map, @Nullable MediaType contentType, HttpOutputMessage outputMessage) - throws IOException, HttpMessageNotWritableException { - - if (isMultipart(map, contentType)) { - writeMultipart((MultiValueMap) map, contentType, outputMessage); - } - else { - writeForm((MultiValueMap) map, contentType, outputMessage); - } - } - - - private boolean isMultipart(MultiValueMap 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 formData, @Nullable MediaType mediaType, - HttpOutputMessage outputMessage) throws IOException { + public void write(MultiValueMap formData, ResolvableType type, @Nullable MediaType contentType, + HttpOutputMessage outputMessage, @Nullable Map hints) throws IOException, HttpMessageNotWritableException { - mediaType = getFormContentType(mediaType); - outputMessage.getHeaders().setContentType(mediaType); + contentType = getFormContentType(contentType); + outputMessage.getHeaders().setContentType(contentType); - 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 formData, Charset charset) { + protected String serializeForm(MultiValueMap formData, Charset charset) { StringBuilder builder = new StringBuilder(); formData.forEach((name, values) -> { if (name == null) { @@ -450,7 +218,7 @@ public class FormHttpMessageConverter implements HttpMessageConverter converter = (HttpMessageConverter) 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 parts, byte[] boundary) throws IOException { - for (Map.Entry> 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) 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. - *

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. - *

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> 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() { - } - } - - } diff --git a/spring-web/src/main/java/org/springframework/http/converter/HttpMessageConverters.java b/spring-web/src/main/java/org/springframework/http/converter/HttpMessageConverters.java index c7b96340f0e..7b6104f35a2 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/HttpMessageConverters.java +++ b/spring-web/src/main/java/org/springframework/http/converter/HttpMessageConverters.java @@ -114,6 +114,14 @@ public interface HttpMessageConverters extends Iterable> */ 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. diff --git a/spring-web/src/main/java/org/springframework/http/converter/multipart/MultipartHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/multipart/MultipartHttpMessageConverter.java index 5526bdbcbf5..56c704b611b 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/multipart/MultipartHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/multipart/MultipartHttpMessageConverter.java @@ -170,6 +170,7 @@ public class MultipartHttpMessageConverter implements SmartHttpMessageConverter< this.partConverters = new ArrayList<>(); converters.forEach(this.partConverters::add); + applyDefaultCharset(); } /** diff --git a/spring-web/src/main/java/org/springframework/http/converter/support/AllEncompassingFormHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/support/AllEncompassingFormHttpMessageConverter.java index 24a6ad19044..7ab5491afaf 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/support/AllEncompassingFormHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/support/AllEncompassingFormHttpMessageConverter.java @@ -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> converters) { - converters.forEach(this::addPartConverter); + super(converters); } } diff --git a/spring-web/src/main/java/org/springframework/web/filter/FormContentFilter.java b/spring-web/src/main/java/org/springframework/web/filter/FormContentFilter.java index ea27c055563..1d4abc3d52e 100644 --- a/spring-web/src/main/java/org/springframework/web/filter/FormContentFilter.java +++ b/spring-web/src/main/java/org/springframework/web/filter/FormContentFilter.java @@ -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 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. - *

By default this is an instance of {@link AllEncompassingFormHttpMessageConverter}. + *

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) { diff --git a/spring-web/src/test/java/org/springframework/http/converter/DefaultHttpMessageConvertersTests.java b/spring-web/src/test/java/org/springframework/http/converter/DefaultHttpMessageConvertersTests.java index dcef8369b35..0a387f18d01 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/DefaultHttpMessageConvertersTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/DefaultHttpMessageConvertersTests.java @@ -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); } diff --git a/spring-web/src/test/java/org/springframework/http/converter/FormHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/FormHttpMessageConverterTests.java index 049340968a6..030fd7a810b 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/FormHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/FormHttpMessageConverterTests.java @@ -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); - } - - @Test - void setSupportedMediaTypes() { - this.converter.setSupportedMediaTypes(List.of(MULTIPART_FORM_DATA)); - assertCannotWrite(MULTIPART_MIXED); - - this.converter.setSupportedMediaTypes(List.of(MULTIPART_MIXED)); - assertCanWrite(MULTIPART_MIXED); + assertCanWrite(LINKED_MULTI_VALUE_MAP, APPLICATION_FORM_URLENCODED); + assertCanWrite(ResolvableType.forClass(LinkedMultiValueMap.class), APPLICATION_FORM_URLENCODED); } @Test - void addSupportedMediaTypes() { - this.converter.setSupportedMediaTypes(List.of(MULTIPART_FORM_DATA)); + void cannotWriteMultipart() { + assertCannotWrite(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 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 entity = new HttpEntity<>(myBean, entityHeaders); - parts.add("json", entity); - - Map 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 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 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("")); - HttpHeaders entityHeaders = new HttpHeaders(); - entityHeaders.setContentType(TEXT_XML); - HttpEntity entity = new HttpEntity<>(xml, entityHeaders); - parts.add("xml", entity); - - Map 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 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 parts = new LinkedMultiValueMap<>(); - parts.add("part1", myBean); - - HttpHeaders entityHeaders = new HttpHeaders(); - entityHeaders.setContentType(TEXT_XML); - HttpEntity 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 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: foo - // But on CI server we get: foo - // So... we make a compromise: - assertThat(item.getString()) - .startsWith("foo"); - } - - @Test - void writeMultipartCharset() throws Exception { - MultiValueMap 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 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; - } - } - } diff --git a/spring-web/src/test/java/org/springframework/http/converter/multipart/MultipartHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/multipart/MultipartHttpMessageConverterTests.java index f29024d6c78..8cbc5988ad7 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/multipart/MultipartHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/multipart/MultipartHttpMessageConverterTests.java @@ -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); diff --git a/spring-web/src/test/java/org/springframework/web/client/RestClientBuilderTests.java b/spring-web/src/test/java/org/springframework/web/client/RestClientBuilderTests.java index 6f5550ce6a8..041351c5311 100644 --- a/spring-web/src/test/java/org/springframework/web/client/RestClientBuilderTests.java +++ b/spring-web/src/test/java/org/springframework/web/client/RestClientBuilderTests.java @@ -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 diff --git a/spring-web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java b/spring-web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java index 82d4ea0ed03..7a9c98567ad 100644 --- a/spring-web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java +++ b/spring-web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java @@ -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 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 diff --git a/spring-web/src/test/java/org/springframework/web/multipart/support/StandardMultipartHttpServletRequestTests.java b/spring-web/src/test/java/org/springframework/web/multipart/support/StandardMultipartHttpServletRequestTests.java index 7f5eeee513b..e84789af192 100644 --- a/spring-web/src/test/java/org/springframework/web/multipart/support/StandardMultipartHttpServletRequestTests.java +++ b/spring-web/src/test/java/org/springframework/web/multipart/support/StandardMultipartHttpServletRequestTests.java @@ -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); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java index 879cc78b6ac..2dcb8775066 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java @@ -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() { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java index a48695012f0..7de750f3ed5 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java @@ -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() { diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportExtensionTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportExtensionTests.java index 24f5f5c99ba..4388a0a08a7 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportExtensionTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportExtensionTests.java @@ -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(); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartIntegrationTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartIntegrationTests.java index 0a679f81d52..c203ff49a2d 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartIntegrationTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartIntegrationTests.java @@ -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)); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessorTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessorTests.java index 0b321d5a6aa..fc0c968df0c 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessorTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessorTests.java @@ -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> converters = List.of(new AllEncompassingFormHttpMessageConverter()); + List> converters = List.of(new FormHttpMessageConverter()); RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters); @SuppressWarnings("unchecked")