From 2afae430eba0f2e3c0cae50d727b3d79bf0cf02a Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 3 Mar 2020 15:37:31 +0000 Subject: [PATCH] Update list of support multipart media types See gh-24582 --- .../org/springframework/http/MediaType.java | 15 +++++- .../multipart/MultipartHttpMessageReader.java | 21 +++++++-- .../multipart/MultipartHttpMessageWriter.java | 23 ++++++---- .../SynchronossPartHttpMessageReader.java | 18 ++++++-- .../converter/FormHttpMessageConverter.java | 8 ++-- .../MultipartHttpMessageWriterTests.java | 46 ++++++++++++++++--- ...SynchronossPartHttpMessageReaderTests.java | 10 ++-- .../FormHttpMessageConverterTests.java | 5 +- 8 files changed, 109 insertions(+), 37 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 f44b3148220..9fd8e2ff87e 100644 --- a/spring-web/src/main/java/org/springframework/http/MediaType.java +++ b/spring-web/src/main/java/org/springframework/http/MediaType.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -301,6 +301,18 @@ public class MediaType extends MimeType implements Serializable { */ public static final String MULTIPART_MIXED_VALUE = "multipart/mixed"; + /** + * Public constant media type for {@code multipart/related}. + * @since 5.2.5 + */ + public static final MediaType MULTIPART_RELATED; + + /** + * A String equivalent of {@link MediaType#MULTIPART_RELATED}. + * @since 5.2.5 + */ + public static final String MULTIPART_RELATED_VALUE = "multipart/related"; + /** * Public constant media type for {@code text/event-stream}. * @since 4.3.6 @@ -381,6 +393,7 @@ public class MediaType extends MimeType implements Serializable { IMAGE_PNG = new MediaType("image", "png"); MULTIPART_FORM_DATA = new MediaType("multipart", "form-data"); MULTIPART_MIXED = new MediaType("multipart", "mixed"); + MULTIPART_RELATED = new MediaType("multipart", "related"); 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/codec/multipart/MultipartHttpMessageReader.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartHttpMessageReader.java index 0d47dd6fcef..2866d7ed02f 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartHttpMessageReader.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartHttpMessageReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.http.codec.multipart; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -55,6 +56,9 @@ public class MultipartHttpMessageReader extends LoggingCodecSupport private static final ResolvableType MULTIPART_VALUE_TYPE = ResolvableType.forClassWithGenerics( MultiValueMap.class, String.class, Part.class); + static final List MIME_TYPES = Collections.unmodifiableList(Arrays.asList( + MediaType.MULTIPART_FORM_DATA, MediaType.MULTIPART_MIXED, MediaType.MULTIPART_RELATED)); + private final HttpMessageReader partReader; @@ -75,13 +79,22 @@ public class MultipartHttpMessageReader extends LoggingCodecSupport @Override public List getReadableMediaTypes() { - return Collections.singletonList(MediaType.MULTIPART_FORM_DATA); + return MIME_TYPES; } @Override public boolean canRead(ResolvableType elementType, @Nullable MediaType mediaType) { - return MULTIPART_VALUE_TYPE.isAssignableFrom(elementType) && - (mediaType == null || MediaType.MULTIPART_FORM_DATA.isCompatibleWith(mediaType)); + if (MULTIPART_VALUE_TYPE.isAssignableFrom(elementType)) { + if (mediaType == null) { + return true; + } + for (MediaType supportedMediaType : MIME_TYPES) { + if (supportedMediaType.isCompatibleWith(mediaType)) { + return true; + } + } + } + return false; } diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartHttpMessageWriter.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartHttpMessageWriter.java index 13a1dfdc684..350d693cd31 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartHttpMessageWriter.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartHttpMessageWriter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -132,8 +132,7 @@ public class MultipartHttpMessageWriter extends LoggingCodecSupport } private static List initMediaTypes(@Nullable HttpMessageWriter formWriter) { - List result = new ArrayList<>(); - result.add(MediaType.MULTIPART_FORM_DATA); + List result = new ArrayList<>(MultipartHttpMessageReader.MIME_TYPES); if (formWriter != null) { result.addAll(formWriter.getWritableMediaTypes()); } @@ -197,7 +196,7 @@ public class MultipartHttpMessageWriter extends LoggingCodecSupport return Mono.from(inputStream) .flatMap(map -> { if (this.formWriter == null || isMultipart(map, mediaType)) { - return writeMultipart(map, outputMessage, hints); + return writeMultipart(map, outputMessage, mediaType, hints); } else { @SuppressWarnings("unchecked") @@ -209,7 +208,7 @@ public class MultipartHttpMessageWriter extends LoggingCodecSupport private boolean isMultipart(MultiValueMap map, @Nullable MediaType contentType) { if (contentType != null) { - return MediaType.MULTIPART_FORM_DATA.includes(contentType); + return contentType.getType().equalsIgnoreCase("multipart"); } for (List values : map.values()) { for (Object value : values) { @@ -221,16 +220,22 @@ public class MultipartHttpMessageWriter extends LoggingCodecSupport return false; } - private Mono writeMultipart( - MultiValueMap map, ReactiveHttpOutputMessage outputMessage, Map hints) { + private Mono writeMultipart(MultiValueMap map, + ReactiveHttpOutputMessage outputMessage, @Nullable MediaType mediaType, Map hints) { byte[] boundary = generateMultipartBoundary(); - Map params = new HashMap<>(2); + Map params = new HashMap<>(); + if (mediaType != null) { + params.putAll(mediaType.getParameters()); + } params.put("boundary", new String(boundary, StandardCharsets.US_ASCII)); params.put("charset", getCharset().name()); - outputMessage.getHeaders().setContentType(new MediaType(MediaType.MULTIPART_FORM_DATA, params)); + mediaType = (mediaType != null ? mediaType : MediaType.MULTIPART_FORM_DATA); + mediaType = new MediaType(mediaType, params); + + outputMessage.getHeaders().setContentType(mediaType); LogFormatUtils.traceDebug(logger, traceOn -> Hints.getLogPrefix(hints) + "Encoding " + (isEnableLoggingRequestDetails() ? diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java index b6537a64160..30dce891b49 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,6 @@ import java.nio.charset.StandardCharsets; import java.nio.file.OpenOption; import java.nio.file.Path; import java.nio.file.StandardOpenOption; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; @@ -153,13 +152,22 @@ public class SynchronossPartHttpMessageReader extends LoggingCodecSupport implem @Override public List getReadableMediaTypes() { - return Collections.singletonList(MediaType.MULTIPART_FORM_DATA); + return MultipartHttpMessageReader.MIME_TYPES; } @Override public boolean canRead(ResolvableType elementType, @Nullable MediaType mediaType) { - return Part.class.equals(elementType.toClass()) && - (mediaType == null || MediaType.MULTIPART_FORM_DATA.isCompatibleWith(mediaType)); + if (Part.class.equals(elementType.toClass())) { + if (mediaType == null) { + return true; + } + for (MediaType supportedMediaType : getReadableMediaTypes()) { + if (supportedMediaType.isCompatibleWith(mediaType)) { + return true; + } + } + } + return false; } @Override 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 5c2594fe47e..945a37f1bf6 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -160,8 +160,6 @@ public class FormHttpMessageConverter implements HttpMessageConverter map, @Nullable MediaType contentType) { if (contentType != null) { - return MULTIPART_ALL.includes(contentType); + return contentType.getType().equalsIgnoreCase("multipart"); } for (List values : map.values()) { for (Object value : values) { diff --git a/spring-web/src/test/java/org/springframework/http/codec/multipart/MultipartHttpMessageWriterTests.java b/spring-web/src/test/java/org/springframework/http/codec/multipart/MultipartHttpMessageWriterTests.java index 497ad9559f3..7a282bfdd44 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/multipart/MultipartHttpMessageWriterTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/multipart/MultipartHttpMessageWriterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -68,17 +68,23 @@ public class MultipartHttpMessageWriterTests extends AbstractLeakCheckingTests { assertThat(this.writer.canWrite( ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class), MediaType.MULTIPART_FORM_DATA)).isTrue(); + assertThat(this.writer.canWrite( + ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, Object.class), + MediaType.MULTIPART_MIXED)).isTrue(); + assertThat(this.writer.canWrite( + ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, Object.class), + MediaType.MULTIPART_RELATED)).isTrue(); + assertThat(this.writer.canWrite( + ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, Object.class), + MediaType.APPLICATION_FORM_URLENCODED)).isTrue(); assertThat(this.writer.canWrite( ResolvableType.forClassWithGenerics(Map.class, String.class, Object.class), MediaType.MULTIPART_FORM_DATA)).isFalse(); - assertThat(this.writer.canWrite( - ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, Object.class), - MediaType.APPLICATION_FORM_URLENCODED)).isTrue(); } @Test - public void writeMultipart() throws Exception { + public void writeMultipartFormData() throws Exception { Resource logo = new ClassPathResource("/org/springframework/http/converter/logo.jpg"); Resource utf8 = new ClassPathResource("/org/springframework/http/converter/logo.jpg") { @Override @@ -109,7 +115,8 @@ public class MultipartHttpMessageWriterTests extends AbstractLeakCheckingTests { Mono>> result = Mono.just(bodyBuilder.build()); Map hints = Collections.emptyMap(); - this.writer.write(result, null, MediaType.MULTIPART_FORM_DATA, this.response, hints).block(Duration.ofSeconds(5)); + this.writer.write(result, null, MediaType.MULTIPART_FORM_DATA, this.response, hints) + .block(Duration.ofSeconds(5)); MultiValueMap requestParts = parse(hints); assertThat(requestParts.size()).isEqualTo(7); @@ -167,6 +174,33 @@ public class MultipartHttpMessageWriterTests extends AbstractLeakCheckingTests { assertThat(value).isEqualTo("AaBbCc"); } + @Test // gh-24582 + public void writeMultipartRelated() { + + MediaType mediaType = MediaType.parseMediaType("multipart/related;type=foo"); + + MultipartBodyBuilder bodyBuilder = new MultipartBodyBuilder(); + bodyBuilder.part("name 1", "value 1"); + bodyBuilder.part("name 2", "value 2"); + Mono>> result = Mono.just(bodyBuilder.build()); + + Map hints = Collections.emptyMap(); + this.writer.write(result, null, mediaType, this.response, hints) + .block(Duration.ofSeconds(5)); + + MediaType contentType = this.response.getHeaders().getContentType(); + assertThat(contentType).isNotNull(); + assertThat(contentType.isCompatibleWith(mediaType)).isTrue(); + assertThat(contentType.getParameter("type")).isEqualTo("foo"); + assertThat(contentType.getParameter("boundary")).isNotEmpty(); + assertThat(contentType.getParameter("charset")).isEqualTo("UTF-8"); + + MultiValueMap requestParts = parse(hints); + assertThat(requestParts.size()).isEqualTo(2); + assertThat(requestParts.getFirst("name 1").name()).isEqualTo("name 1"); + assertThat(requestParts.getFirst("name 2").name()).isEqualTo("name 2"); + } + @SuppressWarnings("ConstantConditions") private String decodeToString(Part part) { return StringDecoder.textPlainOnly().decodeToMono(part.content(), diff --git a/spring-web/src/test/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReaderTests.java b/spring-web/src/test/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReaderTests.java index bf60a6c7911..7e12e1c29a6 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReaderTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReaderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -68,11 +68,13 @@ public class SynchronossPartHttpMessageReaderTests extends AbstractLeakCheckingT private static final ResolvableType PARTS_ELEMENT_TYPE = forClassWithGenerics(MultiValueMap.class, String.class, Part.class); + @Test void canRead() { - assertThat(this.reader.canRead( - PARTS_ELEMENT_TYPE, - MediaType.MULTIPART_FORM_DATA)).isTrue(); + assertThat(this.reader.canRead(PARTS_ELEMENT_TYPE, MediaType.MULTIPART_FORM_DATA)).isTrue(); + assertThat(this.reader.canRead(PARTS_ELEMENT_TYPE, MediaType.MULTIPART_MIXED)).isTrue(); + assertThat(this.reader.canRead(PARTS_ELEMENT_TYPE, MediaType.MULTIPART_RELATED)).isTrue(); + assertThat(this.reader.canRead(PARTS_ELEMENT_TYPE, null)).isTrue(); assertThat(this.reader.canRead( forClassWithGenerics(MultiValueMap.class, String.class, Object.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 91250150546..cad2852f389 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -52,7 +52,6 @@ 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_MIXED; import static org.springframework.http.MediaType.TEXT_XML; -import static org.springframework.http.converter.FormHttpMessageConverter.MULTIPART_ALL; /** * Unit tests for {@link FormHttpMessageConverter} and @@ -278,7 +277,7 @@ public class FormHttpMessageConverterTests { } private void asssertCannotReadMultipart() { - assertCannotRead(MULTIPART_ALL); + assertCannotRead(new MediaType("multipart", "*")); assertCannotRead(MULTIPART_FORM_DATA); assertCannotRead(MULTIPART_MIXED); assertCannotRead(MULTIPART_RELATED);