Add explicit support for multipart/mixed in FormHttpMessageConverter

Commit 5008423408 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
This commit is contained in:
Sam Brannen 2019-06-29 11:40:49 +03:00
parent 75d1428e24
commit 896496341a
5 changed files with 65 additions and 45 deletions

View File

@ -44,6 +44,7 @@ import org.springframework.util.StringUtils;
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @author Sebastien Deleuze * @author Sebastien Deleuze
* @author Kazuki Shimizu * @author Kazuki Shimizu
* @author Sam Brannen
* @since 3.0 * @since 3.0
* @see <a href="https://tools.ietf.org/html/rfc7231#section-3.1.1.1"> * @see <a href="https://tools.ietf.org/html/rfc7231#section-3.1.1.1">
* HTTP 1.1: Semantics and Content, section 3.1.1.1</a> * HTTP 1.1: Semantics and Content, section 3.1.1.1</a>
@ -288,6 +289,18 @@ public class MediaType extends MimeType implements Serializable {
*/ */
public static final String MULTIPART_FORM_DATA_VALUE = "multipart/form-data"; 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}. * Public constant media type for {@code text/event-stream}.
* @since 4.3.6 * @since 4.3.6
@ -367,6 +380,7 @@ public class MediaType extends MimeType implements Serializable {
IMAGE_JPEG = new MediaType("image", "jpeg"); IMAGE_JPEG = new MediaType("image", "jpeg");
IMAGE_PNG = new MediaType("image", "png"); IMAGE_PNG = new MediaType("image", "png");
MULTIPART_FORM_DATA = new MediaType("multipart", "form-data"); MULTIPART_FORM_DATA = new MediaType("multipart", "form-data");
MULTIPART_MIXED = new MediaType("multipart", "mixed");
TEXT_EVENT_STREAM = new MediaType("text", "event-stream"); TEXT_EVENT_STREAM = new MediaType("text", "event-stream");
TEXT_HTML = new MediaType("text", "html"); TEXT_HTML = new MediaType("text", "html");
TEXT_MARKDOWN = new MediaType("text", "markdown"); TEXT_MARKDOWN = new MediaType("text", "markdown");

View File

@ -52,7 +52,8 @@ import org.springframework.util.StringUtils;
* <p>In other words, this converter can read and write the * <p>In other words, this converter can read and write the
* {@code "application/x-www-form-urlencoded"} media type as * {@code "application/x-www-form-urlencoded"} media type as
* {@link MultiValueMap MultiValueMap&lt;String, String&gt;}, and it can also * {@link MultiValueMap MultiValueMap&lt;String, String&gt;}, 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&lt;String, Object&gt;}. * {@link MultiValueMap MultiValueMap&lt;String, Object&gt;}.
* *
* <h3>Multipart Data</h3> * <h3>Multipart Data</h3>
@ -63,7 +64,9 @@ import org.springframework.util.StringUtils;
* {@code "multipart/mixed"} and {@code "multipart/related"}, as long as the * {@code "multipart/mixed"} and {@code "multipart/related"}, as long as the
* multipart subtype is registered as a {@linkplain #getSupportedMediaTypes * multipart subtype is registered as a {@linkplain #getSupportedMediaTypes
* supported media type} <em>and</em> the desired multipart subtype is specified * supported media type} <em>and</em> 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.
* *
* <p>When writing multipart data, this converter uses other * <p>When writing multipart data, this converter uses other
* {@link HttpMessageConverter HttpMessageConverters} to write the respective * {@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 2");
* form.add("field 2", "value 3"); * form.add("field 2", "value 3");
* form.add("field 3", 4); // non-String form values supported as of 5.1.4 * form.add("field 3", 4); // non-String form values supported as of 5.1.4
* restTemplate.postForLocation("https://example.com/myForm", form); *
* </pre> * restTemplate.postForLocation("https://example.com/myForm", form);</pre>
* *
* <p>The following snippet shows how to do a file upload using the * <p>The following snippet shows how to do a file upload using the
* {@code "multipart/form-data"} content type. * {@code "multipart/form-data"} content type.
@ -95,33 +98,45 @@ import org.springframework.util.StringUtils;
* MultiValueMap&lt;String, Object&gt; parts = new LinkedMultiValueMap&lt;&gt;(); * MultiValueMap&lt;String, Object&gt; parts = new LinkedMultiValueMap&lt;&gt;();
* parts.add("field 1", "value 1"); * parts.add("field 1", "value 1");
* parts.add("file", new ClassPathResource("myFile.jpg")); * parts.add("file", new ClassPathResource("myFile.jpg"));
* restTemplate.postForLocation("https://example.com/myFileUpload", parts); *
* </pre> * restTemplate.postForLocation("https://example.com/myFileUpload", parts);</pre>
* *
* <p>The following snippet shows how to do a file upload using the * <p>The following snippet shows how to do a file upload using the
* {@code "multipart/mixed"} content type. * {@code "multipart/mixed"} content type.
* *
* <pre class="code"> * <pre class="code">
* MediaType multipartMixed = new MediaType("multipart", "mixed"); * MultiValueMap&lt;String, Object&gt; parts = new LinkedMultiValueMap&lt;&gt;();
* 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&lt;&gt;(parts, requestHeaders));</pre>
*
* <p>The following snippet shows how to do a file upload using the
* {@code "multipart/related"} content type.
*
* <pre class="code">
* MediaType multipartRelated = new MediaType("multipart", "related");
* *
* restTemplate.getMessageConverters().stream() * restTemplate.getMessageConverters().stream()
* .filter(FormHttpMessageConverter.class::isInstance) * .filter(FormHttpMessageConverter.class::isInstance)
* .map(FormHttpMessageConverter.class::cast) * .map(FormHttpMessageConverter.class::cast)
* .findFirst() * .findFirst()
* .orElseThrow(() -&gt; new IllegalStateException("Failed to find FormHttpMessageConverter")) * .orElseThrow(() -&gt; new IllegalStateException("Failed to find FormHttpMessageConverter"))
* .addSupportedMediaTypes(multipartMixed); * .addSupportedMediaTypes(multipartRelated);
* *
* MultiValueMap&lt;String, Object&gt; parts = new LinkedMultiValueMap&lt;&gt;(); * MultiValueMap&lt;String, Object&gt; parts = new LinkedMultiValueMap&lt;&gt;();
* parts.add("field 1", "value 1"); * parts.add("field 1", "value 1");
* parts.add("file", new ClassPathResource("myFile.jpg")); * parts.add("file", new ClassPathResource("myFile.jpg"));
* *
* HttpHeaders requestHeaders = new HttpHeaders(); * HttpHeaders requestHeaders = new HttpHeaders();
* requestHeaders.setContentType(multipartMixed); * requestHeaders.setContentType(multipartRelated);
* HttpEntity&lt;MultiValueMap&lt;String, Object&gt;&gt; requestEntity =
* new HttpEntity&lt;&gt;(parts, requestHeaders);
* *
* restTemplate.postForLocation("https://example.com/myFileUpload", requestEntity); * restTemplate.postForLocation("https://example.com/myFileUpload",
* </pre> * new HttpEntity&lt;&gt;(parts, requestHeaders));</pre>
* *
* <h3>Miscellaneous</h3> * <h3>Miscellaneous</h3>
* *
@ -138,13 +153,13 @@ import org.springframework.util.StringUtils;
*/ */
public class FormHttpMessageConverter implements HttpMessageConverter<MultiValueMap<String, ?>> { public class FormHttpMessageConverter implements HttpMessageConverter<MultiValueMap<String, ?>> {
private static final MediaType MULTIPART_ALL = new MediaType("multipart", "*");
/** /**
* The default charset used by the converter. * The default charset used by the converter.
*/ */
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; 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 = private static final MediaType DEFAULT_FORM_DATA_MEDIA_TYPE =
new MediaType(MediaType.APPLICATION_FORM_URLENCODED, DEFAULT_CHARSET); new MediaType(MediaType.APPLICATION_FORM_URLENCODED, DEFAULT_CHARSET);
@ -162,6 +177,7 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
public FormHttpMessageConverter() { public FormHttpMessageConverter() {
this.supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED); this.supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
this.supportedMediaTypes.add(MediaType.MULTIPART_FORM_DATA); this.supportedMediaTypes.add(MediaType.MULTIPART_FORM_DATA);
this.supportedMediaTypes.add(MediaType.MULTIPART_MIXED);
this.partConverters.add(new ByteArrayHttpMessageConverter()); this.partConverters.add(new ByteArrayHttpMessageConverter());
this.partConverters.add(new StringHttpMessageConverter()); this.partConverters.add(new StringHttpMessageConverter());

View File

@ -49,7 +49,9 @@ import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED; import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED;
import static org.springframework.http.MediaType.MULTIPART_FORM_DATA; import static org.springframework.http.MediaType.MULTIPART_FORM_DATA;
import static org.springframework.http.MediaType.MULTIPART_MIXED;
import static org.springframework.http.MediaType.TEXT_XML; import static org.springframework.http.MediaType.TEXT_XML;
import static org.springframework.http.converter.FormHttpMessageConverter.MULTIPART_ALL;
/** /**
* Unit tests for {@link FormHttpMessageConverter} and * Unit tests for {@link FormHttpMessageConverter} and
@ -61,8 +63,6 @@ import static org.springframework.http.MediaType.TEXT_XML;
*/ */
public class FormHttpMessageConverterTests { public class FormHttpMessageConverterTests {
private static final MediaType MULTIPART_ALL = new MediaType("multipart", "*");
private static final MediaType MULTIPART_MIXED = new MediaType("multipart", "mixed");
private static final MediaType MULTIPART_RELATED = new MediaType("multipart", "related"); private static final MediaType MULTIPART_RELATED = new MediaType("multipart", "related");
private final FormHttpMessageConverter converter = new AllEncompassingFormHttpMessageConverter(); private final FormHttpMessageConverter converter = new AllEncompassingFormHttpMessageConverter();
@ -80,24 +80,19 @@ public class FormHttpMessageConverterTests {
@Test @Test
public void cannotReadMultipart() { public void cannotReadMultipart() {
// Without custom multipart types supported // Without custom multipart types supported
assertCannotRead(MULTIPART_ALL); asssertCannotReadMultipart();
assertCannotRead(MULTIPART_FORM_DATA);
assertCannotRead(MULTIPART_MIXED);
assertCannotRead(MULTIPART_RELATED);
this.converter.addSupportedMediaTypes(MULTIPART_MIXED, MULTIPART_RELATED); this.converter.addSupportedMediaTypes(MULTIPART_RELATED);
// With custom multipart types supported // Should still be the case with custom multipart types supported
assertCannotRead(MULTIPART_ALL); asssertCannotReadMultipart();
assertCannotRead(MULTIPART_FORM_DATA);
assertCannotRead(MULTIPART_MIXED);
assertCannotRead(MULTIPART_RELATED);
} }
@Test @Test
public void canWrite() { public void canWrite() {
assertCanWrite(APPLICATION_FORM_URLENCODED); assertCanWrite(APPLICATION_FORM_URLENCODED);
assertCanWrite(MULTIPART_FORM_DATA); assertCanWrite(MULTIPART_FORM_DATA);
assertCanWrite(MULTIPART_MIXED);
assertCanWrite(new MediaType("multipart", "form-data", StandardCharsets.UTF_8)); assertCanWrite(new MediaType("multipart", "form-data", StandardCharsets.UTF_8));
assertCanWrite(MediaType.ALL); assertCanWrite(MediaType.ALL);
assertCanWrite(null); assertCanWrite(null);
@ -105,26 +100,21 @@ public class FormHttpMessageConverterTests {
@Test @Test
public void setSupportedMediaTypes() { public void setSupportedMediaTypes() {
assertCannotWrite(MULTIPART_MIXED);
assertCannotWrite(MULTIPART_RELATED); assertCannotWrite(MULTIPART_RELATED);
List<MediaType> supportedMediaTypes = new ArrayList<>(this.converter.getSupportedMediaTypes()); List<MediaType> supportedMediaTypes = new ArrayList<>(this.converter.getSupportedMediaTypes());
supportedMediaTypes.add(MULTIPART_MIXED);
supportedMediaTypes.add(MULTIPART_RELATED); supportedMediaTypes.add(MULTIPART_RELATED);
this.converter.setSupportedMediaTypes(supportedMediaTypes); this.converter.setSupportedMediaTypes(supportedMediaTypes);
assertCanWrite(MULTIPART_MIXED);
assertCanWrite(MULTIPART_RELATED); assertCanWrite(MULTIPART_RELATED);
} }
@Test @Test
public void addSupportedMediaTypes() { public void addSupportedMediaTypes() {
assertCannotWrite(MULTIPART_MIXED);
assertCannotWrite(MULTIPART_RELATED); assertCannotWrite(MULTIPART_RELATED);
this.converter.addSupportedMediaTypes(MULTIPART_MIXED, MULTIPART_RELATED); this.converter.addSupportedMediaTypes(MULTIPART_RELATED);
assertCanWrite(MULTIPART_MIXED);
assertCanWrite(MULTIPART_RELATED); assertCanWrite(MULTIPART_RELATED);
} }
@ -286,6 +276,13 @@ public class FormHttpMessageConverterTests {
assertThat(this.converter.canRead(clazz, mediaType)).as(clazz.getSimpleName() + " : " + mediaType).isTrue(); 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) { private void assertCannotRead(MediaType mediaType) {
assertCannotRead(MultiValueMap.class, mediaType); assertCannotRead(MultiValueMap.class, mediaType);
} }

View File

@ -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.CONTENT_TYPE;
import static org.springframework.http.HttpHeaders.LOCATION; import static org.springframework.http.HttpHeaders.LOCATION;
import static org.springframework.http.MediaType.MULTIPART_FORM_DATA; import static org.springframework.http.MediaType.MULTIPART_FORM_DATA;
import static org.springframework.http.MediaType.MULTIPART_MIXED;
/** /**
* @author Brian Clozel * @author Brian Clozel
@ -43,7 +44,6 @@ import static org.springframework.http.MediaType.MULTIPART_FORM_DATA;
*/ */
public class AbstractMockWebServerTestCase { 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 MULTIPART_RELATED = new MediaType("multipart", "related");
protected static final MediaType textContentType = protected static final MediaType textContentType =

View File

@ -63,6 +63,7 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.junit.Assume.assumeFalse; import static org.junit.Assume.assumeFalse;
import static org.springframework.http.HttpMethod.POST; import static org.springframework.http.HttpMethod.POST;
import static org.springframework.http.MediaType.MULTIPART_MIXED;
/** /**
* Integration tests for {@link RestTemplate}. * Integration tests for {@link RestTemplate}.
@ -274,15 +275,10 @@ public class RestTemplateIntegrationTests extends AbstractMockWebServerTestCase
} }
@Test @Test
public void multipartMixed() { public void multipartMixed() throws Exception {
addSupportedMediaTypeToFormHttpMessageConverter(MULTIPART_MIXED);
HttpHeaders requestHeaders = new HttpHeaders(); HttpHeaders requestHeaders = new HttpHeaders();
requestHeaders.setContentType(MULTIPART_MIXED); requestHeaders.setContentType(MULTIPART_MIXED);
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(createMultipartParts(), template.postForLocation(baseUrl + "/multipartMixed", new HttpEntity<>(createMultipartParts(), requestHeaders));
requestHeaders);
template.postForLocation(baseUrl + "/multipartMixed", requestEntity);
} }
@Test @Test
@ -291,10 +287,7 @@ public class RestTemplateIntegrationTests extends AbstractMockWebServerTestCase
HttpHeaders requestHeaders = new HttpHeaders(); HttpHeaders requestHeaders = new HttpHeaders();
requestHeaders.setContentType(MULTIPART_RELATED); requestHeaders.setContentType(MULTIPART_RELATED);
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(createMultipartParts(), template.postForLocation(baseUrl + "/multipartRelated", new HttpEntity<>(createMultipartParts(), requestHeaders));
requestHeaders);
template.postForLocation(baseUrl + "/multipartRelated", requestEntity);
} }
private MultiValueMap<String, Object> createMultipartParts() { private MultiValueMap<String, Object> createMultipartParts() {