From 896496341afaa8501ecc8491e8aa8936fea6d80c Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Sat, 29 Jun 2019 11:40:49 +0300 Subject: [PATCH] Add explicit support for multipart/mixed in FormHttpMessageConverter Commit 5008423408dfd474a49240a5728c7790b8c24bff added support for multipart/* media types in FormHttpMessageConverter, but users still had to manually register multipart/mixed as a supported media type in order to POST multipart data with that content type. This commit removes the need to manually register multipart/mixed as a supported media type by registering it automatically in FormHttpMessageConverter. In addition, this commit introduces MULTIPART_MIXED and MULTIPART_MIXED_VALUE constants in MediaType. Closes gh-23209 --- .../org/springframework/http/MediaType.java | 14 ++++++ .../converter/FormHttpMessageConverter.java | 46 +++++++++++++------ .../FormHttpMessageConverterTests.java | 33 ++++++------- .../client/AbstractMockWebServerTestCase.java | 2 +- .../client/RestTemplateIntegrationTests.java | 15 ++---- 5 files changed, 65 insertions(+), 45 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/MediaType.java b/spring-web/src/main/java/org/springframework/http/MediaType.java index 72a5e06320..ad37e19851 100644 --- a/spring-web/src/main/java/org/springframework/http/MediaType.java +++ b/spring-web/src/main/java/org/springframework/http/MediaType.java @@ -44,6 +44,7 @@ import org.springframework.util.StringUtils; * @author Rossen Stoyanchev * @author Sebastien Deleuze * @author Kazuki Shimizu + * @author Sam Brannen * @since 3.0 * @see * HTTP 1.1: Semantics and Content, section 3.1.1.1 @@ -288,6 +289,18 @@ public class MediaType extends MimeType implements Serializable { */ public static final String MULTIPART_FORM_DATA_VALUE = "multipart/form-data"; + /** + * Public constant media type for {@code multipart/mixed}. + * @since 5.2 + */ + public static final MediaType MULTIPART_MIXED; + + /** + * A String equivalent of {@link MediaType#MULTIPART_MIXED}. + * @since 5.2 + */ + public static final String MULTIPART_MIXED_VALUE = "multipart/mixed"; + /** * Public constant media type for {@code text/event-stream}. * @since 4.3.6 @@ -367,6 +380,7 @@ public class MediaType extends MimeType implements Serializable { IMAGE_JPEG = new MediaType("image", "jpeg"); IMAGE_PNG = new MediaType("image", "png"); MULTIPART_FORM_DATA = new MediaType("multipart", "form-data"); + MULTIPART_MIXED = new MediaType("multipart", "mixed"); TEXT_EVENT_STREAM = new MediaType("text", "event-stream"); TEXT_HTML = new MediaType("text", "html"); TEXT_MARKDOWN = new MediaType("text", "markdown"); 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 35015da28f..cec5388f7e 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 @@ -52,7 +52,8 @@ import org.springframework.util.StringUtils; *

In other words, 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"} media type as + * write (but not read) the {@code "multipart/form-data"} and + * {@code "multipart/mixed"} media types as * {@link MultiValueMap MultiValueMap<String, Object>}. * *

Multipart Data

@@ -63,7 +64,9 @@ import org.springframework.util.StringUtils; * {@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. + * 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 @@ -85,8 +88,8 @@ import org.springframework.util.StringUtils; * 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 - * restTemplate.postForLocation("https://example.com/myForm", form); - * + * + * restTemplate.postForLocation("https://example.com/myForm", form); * *

The following snippet shows how to do a file upload using the * {@code "multipart/form-data"} content type. @@ -95,33 +98,45 @@ import org.springframework.util.StringUtils; * MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>(); * parts.add("field 1", "value 1"); * parts.add("file", new ClassPathResource("myFile.jpg")); - * restTemplate.postForLocation("https://example.com/myFileUpload", parts); - * + * + * restTemplate.postForLocation("https://example.com/myFileUpload", parts); * *

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

- * MediaType multipartMixed = new MediaType("multipart", "mixed");
+ * MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
+ * parts.add("field 1", "value 1");
+ * parts.add("file", new ClassPathResource("myFile.jpg"));
+ *
+ * HttpHeaders requestHeaders = new HttpHeaders();
+ * requestHeaders.setContentType(MediaType.MULTIPART_MIXED);
+ *
+ * restTemplate.postForLocation("https://example.com/myFileUpload",
+ *     new HttpEntity<>(parts, requestHeaders));
+ * + *

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

+ * MediaType multipartRelated = new MediaType("multipart", "related");
  *
  * restTemplate.getMessageConverters().stream()
  *     .filter(FormHttpMessageConverter.class::isInstance)
  *     .map(FormHttpMessageConverter.class::cast)
  *     .findFirst()
  *     .orElseThrow(() -> new IllegalStateException("Failed to find FormHttpMessageConverter"))
- *     .addSupportedMediaTypes(multipartMixed);
+ *     .addSupportedMediaTypes(multipartRelated);
  *
  * MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
  * parts.add("field 1", "value 1");
  * parts.add("file", new ClassPathResource("myFile.jpg"));
  *
  * HttpHeaders requestHeaders = new HttpHeaders();
- * requestHeaders.setContentType(multipartMixed);
- * HttpEntity<MultiValueMap<String, Object>> requestEntity =
- *     new HttpEntity<>(parts, requestHeaders);
+ * requestHeaders.setContentType(multipartRelated);
  *
- * restTemplate.postForLocation("https://example.com/myFileUpload", requestEntity);
- * 
+ * restTemplate.postForLocation("https://example.com/myFileUpload", + * new HttpEntity<>(parts, requestHeaders)); * *

Miscellaneous

* @@ -138,13 +153,13 @@ import org.springframework.util.StringUtils; */ public class FormHttpMessageConverter implements HttpMessageConverter> { - private static final MediaType MULTIPART_ALL = new MediaType("multipart", "*"); - /** * The default charset used by the converter. */ public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; + static final MediaType MULTIPART_ALL = new MediaType("multipart", "*"); + private static final MediaType DEFAULT_FORM_DATA_MEDIA_TYPE = new MediaType(MediaType.APPLICATION_FORM_URLENCODED, DEFAULT_CHARSET); @@ -162,6 +177,7 @@ public class FormHttpMessageConverter implements HttpMessageConverter supportedMediaTypes = new ArrayList<>(this.converter.getSupportedMediaTypes()); - supportedMediaTypes.add(MULTIPART_MIXED); supportedMediaTypes.add(MULTIPART_RELATED); this.converter.setSupportedMediaTypes(supportedMediaTypes); - assertCanWrite(MULTIPART_MIXED); assertCanWrite(MULTIPART_RELATED); } @Test public void addSupportedMediaTypes() { - assertCannotWrite(MULTIPART_MIXED); assertCannotWrite(MULTIPART_RELATED); - this.converter.addSupportedMediaTypes(MULTIPART_MIXED, MULTIPART_RELATED); + this.converter.addSupportedMediaTypes(MULTIPART_RELATED); - assertCanWrite(MULTIPART_MIXED); assertCanWrite(MULTIPART_RELATED); } @@ -286,6 +276,13 @@ public class FormHttpMessageConverterTests { assertThat(this.converter.canRead(clazz, mediaType)).as(clazz.getSimpleName() + " : " + mediaType).isTrue(); } + private void asssertCannotReadMultipart() { + assertCannotRead(MULTIPART_ALL); + assertCannotRead(MULTIPART_FORM_DATA); + assertCannotRead(MULTIPART_MIXED); + assertCannotRead(MULTIPART_RELATED); + } + private void assertCannotRead(MediaType mediaType) { assertCannotRead(MultiValueMap.class, mediaType); } diff --git a/spring-web/src/test/java/org/springframework/web/client/AbstractMockWebServerTestCase.java b/spring-web/src/test/java/org/springframework/web/client/AbstractMockWebServerTestCase.java index 8039ccd31c..bfcf7653af 100644 --- a/spring-web/src/test/java/org/springframework/web/client/AbstractMockWebServerTestCase.java +++ b/spring-web/src/test/java/org/springframework/web/client/AbstractMockWebServerTestCase.java @@ -36,6 +36,7 @@ import static org.springframework.http.HttpHeaders.CONTENT_LENGTH; import static org.springframework.http.HttpHeaders.CONTENT_TYPE; import static org.springframework.http.HttpHeaders.LOCATION; import static org.springframework.http.MediaType.MULTIPART_FORM_DATA; +import static org.springframework.http.MediaType.MULTIPART_MIXED; /** * @author Brian Clozel @@ -43,7 +44,6 @@ import static org.springframework.http.MediaType.MULTIPART_FORM_DATA; */ public class AbstractMockWebServerTestCase { - protected static final MediaType MULTIPART_MIXED = new MediaType("multipart", "mixed"); protected static final MediaType MULTIPART_RELATED = new MediaType("multipart", "related"); protected static final MediaType textContentType = 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 db84403712..666cc27ede 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 @@ -63,6 +63,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.junit.Assume.assumeFalse; import static org.springframework.http.HttpMethod.POST; +import static org.springframework.http.MediaType.MULTIPART_MIXED; /** * Integration tests for {@link RestTemplate}. @@ -274,15 +275,10 @@ public class RestTemplateIntegrationTests extends AbstractMockWebServerTestCase } @Test - public void multipartMixed() { - addSupportedMediaTypeToFormHttpMessageConverter(MULTIPART_MIXED); - + public void multipartMixed() throws Exception { HttpHeaders requestHeaders = new HttpHeaders(); requestHeaders.setContentType(MULTIPART_MIXED); - HttpEntity> requestEntity = new HttpEntity<>(createMultipartParts(), - requestHeaders); - - template.postForLocation(baseUrl + "/multipartMixed", requestEntity); + template.postForLocation(baseUrl + "/multipartMixed", new HttpEntity<>(createMultipartParts(), requestHeaders)); } @Test @@ -291,10 +287,7 @@ public class RestTemplateIntegrationTests extends AbstractMockWebServerTestCase HttpHeaders requestHeaders = new HttpHeaders(); requestHeaders.setContentType(MULTIPART_RELATED); - HttpEntity> requestEntity = new HttpEntity<>(createMultipartParts(), - requestHeaders); - - template.postForLocation(baseUrl + "/multipartRelated", requestEntity); + template.postForLocation(baseUrl + "/multipartRelated", new HttpEntity<>(createMultipartParts(), requestHeaders)); } private MultiValueMap createMultipartParts() {