diff --git a/spring-web/src/main/java/org/springframework/http/codec/FormHttpMessageReader.java b/spring-web/src/main/java/org/springframework/http/codec/FormHttpMessageReader.java index c9646b299a17..9b6b8e416068 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/FormHttpMessageReader.java +++ b/spring-web/src/main/java/org/springframework/http/codec/FormHttpMessageReader.java @@ -48,7 +48,7 @@ * @since 5.0 */ public class FormHttpMessageReader extends LoggingCodecSupport - implements HttpMessageReader> { + implements HttpMessageReader> { /** * The default charset used by the reader. @@ -58,6 +58,9 @@ public class FormHttpMessageReader extends LoggingCodecSupport private static final ResolvableType MULTIVALUE_STRINGS_TYPE = ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class); + private static final ResolvableType MAP_STRINGS_TYPE = + ResolvableType.forClassWithGenerics(Map.class, String.class, String.class); + private Charset defaultCharset = DEFAULT_CHARSET; @@ -107,10 +110,14 @@ public boolean canRead(ResolvableType elementType, @Nullable MediaType mediaType if (!supportsMediaType(mediaType)) { return false; } + if (!Map.class.isAssignableFrom(elementType.toClass())) { + return false; + } if (MultiValueMap.class.isAssignableFrom(elementType.toClass()) && elementType.hasUnresolvableGenerics()) { return true; } - return MULTIVALUE_STRINGS_TYPE.isAssignableFrom(elementType); + return MULTIVALUE_STRINGS_TYPE.isAssignableFrom(elementType) || + MAP_STRINGS_TYPE.isAssignableFrom(elementType); } private static boolean supportsMediaType(@Nullable MediaType mediaType) { @@ -118,14 +125,14 @@ private static boolean supportsMediaType(@Nullable MediaType mediaType) { } @Override - public Flux> read(ResolvableType elementType, + public Flux> read(ResolvableType elementType, ReactiveHttpInputMessage message, Map hints) { return Flux.from(readMono(elementType, message, hints)); } @Override - public Mono> readMono(ResolvableType elementType, + public Mono> readMono(ResolvableType elementType, ReactiveHttpInputMessage message, Map hints) { MediaType contentType = message.getHeaders().getContentType(); @@ -137,6 +144,9 @@ public Mono> readMono(ResolvableType elementType, DataBufferUtils.release(buffer); MultiValueMap formData = parseFormData(charset, body); logFormData(formData, hints); + if (!MultiValueMap.class.isAssignableFrom(elementType.toClass())) { + return formData.asSingleValueMap(); + } return formData; }); } diff --git a/spring-web/src/main/java/org/springframework/http/codec/FormHttpMessageWriter.java b/spring-web/src/main/java/org/springframework/http/codec/FormHttpMessageWriter.java index 3514e2c82aee..a008c93e68ca 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/FormHttpMessageWriter.java +++ b/spring-web/src/main/java/org/springframework/http/codec/FormHttpMessageWriter.java @@ -39,8 +39,8 @@ /** * {@link HttpMessageWriter} for writing a {@code MultiValueMap} - * as HTML form data, i.e. {@code "application/x-www-form-urlencoded"}, to the - * body of a request. + * or a {@code Map} as HTML form data, i.e. + * {@code "application/x-www-form-urlencoded"}, to the body of a request. * *

Note that unless the media type is explicitly set to * {@link MediaType#APPLICATION_FORM_URLENCODED}, the {@link #canWrite} method @@ -58,7 +58,7 @@ * @see org.springframework.http.codec.multipart.MultipartHttpMessageWriter */ public class FormHttpMessageWriter extends LoggingCodecSupport - implements HttpMessageWriter> { + implements HttpMessageWriter> { /** The default charset used by the writer. */ public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; @@ -69,6 +69,9 @@ public class FormHttpMessageWriter extends LoggingCodecSupport private static final ResolvableType MULTIVALUE_TYPE = ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class); + private static final ResolvableType MAP_TYPE = + ResolvableType.forClassWithGenerics(Map.class, String.class, String.class); + private Charset defaultCharset = DEFAULT_CHARSET; @@ -99,22 +102,24 @@ public List getWritableMediaTypes() { @Override public boolean canWrite(ResolvableType elementType, @Nullable MediaType mediaType) { - if (!MultiValueMap.class.isAssignableFrom(elementType.toClass())) { + if (!Map.class.isAssignableFrom(elementType.toClass())) { return false; } if (MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(mediaType)) { - // Optimistically, any MultiValueMap with or without generics + // Optimistically, any Map with or without generics return true; } if (mediaType == null) { - // Only String-based MultiValueMap - return MULTIVALUE_TYPE.isAssignableFrom(elementType); + // Only String-based Map or MultiValueMap + return MULTIVALUE_TYPE.isAssignableFrom(elementType) || + MAP_TYPE.isAssignableFrom(elementType); } return false; } @Override - public Mono write(Publisher> inputStream, + @SuppressWarnings("unchecked") + public Mono write(Publisher> inputStream, ResolvableType elementType, @Nullable MediaType mediaType, ReactiveHttpOutputMessage message, Map hints) { @@ -125,7 +130,13 @@ public Mono write(Publisher> input return Mono.from(inputStream).flatMap(form -> { logFormData(form, hints); - String value = serializeForm(form, charset); + String value; + if (form instanceof MultiValueMap multiValueMap) { + value = serializeForm((MultiValueMap) multiValueMap, charset); + } + else { + value = serializeForm((Map) form, charset); + } ByteBuffer byteBuffer = charset.encode(value); DataBuffer buffer = message.bufferFactory().wrap(byteBuffer); // wrapping only, no allocation message.getHeaders().setContentLength(byteBuffer.remaining()); @@ -151,7 +162,7 @@ protected MediaType getMediaType(@Nullable MediaType mediaType) { return mediaType; } - private void logFormData(MultiValueMap form, Map hints) { + private void logFormData(Map form, Map hints) { LogFormatUtils.traceDebug(logger, traceOn -> Hints.getLogPrefix(hints) + "Writing " + (isEnableLoggingRequestDetails() ? LogFormatUtils.formatValue(form, !traceOn) : @@ -174,4 +185,19 @@ protected String serializeForm(MultiValueMap formData, Charset c return builder.toString(); } + protected String serializeForm(Map formData, Charset charset) { + StringBuilder builder = new StringBuilder(); + formData.forEach((name, value) -> { + if (builder.length() != 0) { + builder.append('&'); + } + builder.append(URLEncoder.encode(name, charset)); + if (value != null) { + builder.append('='); + builder.append(URLEncoder.encode(value, charset)); + } + }); + return builder.toString(); + } + } 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 944fd70415f1..6a62ccb8f38d 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 @@ -81,7 +81,7 @@ public class MultipartHttpMessageWriter extends MultipartWriterSupport private final Supplier>> partWritersSupplier; - private final @Nullable HttpMessageWriter> formWriter; + private final @Nullable HttpMessageWriter> formWriter; /** @@ -109,7 +109,7 @@ public MultipartHttpMessageWriter(List> partWriters) { * @param formWriter the fallback writer for form data, {@code null} by default */ public MultipartHttpMessageWriter(List> partWriters, - @Nullable HttpMessageWriter> formWriter) { + @Nullable HttpMessageWriter> formWriter) { this(() -> partWriters, formWriter); } @@ -124,7 +124,7 @@ public MultipartHttpMessageWriter(List> partWriters, * @since 6.0.3 */ public MultipartHttpMessageWriter(Supplier>> partWritersSupplier, - @Nullable HttpMessageWriter> formWriter) { + @Nullable HttpMessageWriter> formWriter) { super(initMediaTypes(formWriter)); this.partWritersSupplier = partWritersSupplier; @@ -153,8 +153,9 @@ public List> getPartWriters() { * Return the configured form writer. * @since 5.1.13 */ + @SuppressWarnings("unchecked") public @Nullable HttpMessageWriter> getFormWriter() { - return this.formWriter; + return (HttpMessageWriter>) this.formWriter; }