Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
* @since 5.0
*/
public class FormHttpMessageReader extends LoggingCodecSupport
implements HttpMessageReader<MultiValueMap<String, String>> {
implements HttpMessageReader<Map<String, ?>> {

/**
* The default charset used by the reader.
Expand All @@ -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;

Expand Down Expand Up @@ -107,25 +110,29 @@ 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) {
return (mediaType == null || MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(mediaType));
}

@Override
public Flux<MultiValueMap<String, String>> read(ResolvableType elementType,
public Flux<Map<String, ?>> read(ResolvableType elementType,
ReactiveHttpInputMessage message, Map<String, Object> hints) {

return Flux.from(readMono(elementType, message, hints));
}

@Override
public Mono<MultiValueMap<String, String>> readMono(ResolvableType elementType,
public Mono<Map<String, ?>> readMono(ResolvableType elementType,
ReactiveHttpInputMessage message, Map<String, Object> hints) {

MediaType contentType = message.getHeaders().getContentType();
Expand All @@ -137,6 +144,9 @@ public Mono<MultiValueMap<String, String>> readMono(ResolvableType elementType,
DataBufferUtils.release(buffer);
MultiValueMap<String, String> formData = parseFormData(charset, body);
logFormData(formData, hints);
if (!MultiValueMap.class.isAssignableFrom(elementType.toClass())) {
return formData.asSingleValueMap();
}
return formData;
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@

/**
* {@link HttpMessageWriter} for writing a {@code MultiValueMap<String, String>}
* as HTML form data, i.e. {@code "application/x-www-form-urlencoded"}, to the
* body of a request.
* or a {@code Map<String, String>} as HTML form data, i.e.
* {@code "application/x-www-form-urlencoded"}, to the body of a request.
*
* <p>Note that unless the media type is explicitly set to
* {@link MediaType#APPLICATION_FORM_URLENCODED}, the {@link #canWrite} method
Expand All @@ -58,7 +58,7 @@
* @see org.springframework.http.codec.multipart.MultipartHttpMessageWriter
*/
public class FormHttpMessageWriter extends LoggingCodecSupport
implements HttpMessageWriter<MultiValueMap<String, String>> {
implements HttpMessageWriter<Map<String, ?>> {

/** The default charset used by the writer. */
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
Expand All @@ -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;

Expand Down Expand Up @@ -99,22 +102,24 @@ public List<MediaType> 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<Void> write(Publisher<? extends MultiValueMap<String, String>> inputStream,
@SuppressWarnings("unchecked")
public Mono<Void> write(Publisher<? extends Map<String, ?>> inputStream,
ResolvableType elementType, @Nullable MediaType mediaType, ReactiveHttpOutputMessage message,
Map<String, Object> hints) {

Expand All @@ -125,7 +130,13 @@ public Mono<Void> write(Publisher<? extends MultiValueMap<String, String>> 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<String, String>) multiValueMap, charset);
}
else {
value = serializeForm((Map<String, String>) form, charset);
}
ByteBuffer byteBuffer = charset.encode(value);
DataBuffer buffer = message.bufferFactory().wrap(byteBuffer); // wrapping only, no allocation
message.getHeaders().setContentLength(byteBuffer.remaining());
Expand All @@ -151,7 +162,7 @@ protected MediaType getMediaType(@Nullable MediaType mediaType) {
return mediaType;
}

private void logFormData(MultiValueMap<String, String> form, Map<String, Object> hints) {
private void logFormData(Map<String, ?> form, Map<String, Object> hints) {
LogFormatUtils.traceDebug(logger, traceOn -> Hints.getLogPrefix(hints) + "Writing " +
(isEnableLoggingRequestDetails() ?
LogFormatUtils.formatValue(form, !traceOn) :
Expand All @@ -174,4 +185,19 @@ protected String serializeForm(MultiValueMap<String, String> formData, Charset c
return builder.toString();
}

protected String serializeForm(Map<String, String> 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();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public class MultipartHttpMessageWriter extends MultipartWriterSupport

private final Supplier<List<HttpMessageWriter<?>>> partWritersSupplier;

private final @Nullable HttpMessageWriter<MultiValueMap<String, String>> formWriter;
private final @Nullable HttpMessageWriter<? super MultiValueMap<String, String>> formWriter;


/**
Expand Down Expand Up @@ -109,7 +109,7 @@ public MultipartHttpMessageWriter(List<HttpMessageWriter<?>> partWriters) {
* @param formWriter the fallback writer for form data, {@code null} by default
*/
public MultipartHttpMessageWriter(List<HttpMessageWriter<?>> partWriters,
@Nullable HttpMessageWriter<MultiValueMap<String, String>> formWriter) {
@Nullable HttpMessageWriter<? super MultiValueMap<String, String>> formWriter) {

this(() -> partWriters, formWriter);
}
Expand All @@ -124,7 +124,7 @@ public MultipartHttpMessageWriter(List<HttpMessageWriter<?>> partWriters,
* @since 6.0.3
*/
public MultipartHttpMessageWriter(Supplier<List<HttpMessageWriter<?>>> partWritersSupplier,
@Nullable HttpMessageWriter<MultiValueMap<String, String>> formWriter) {
@Nullable HttpMessageWriter<? super MultiValueMap<String, String>> formWriter) {

super(initMediaTypes(formWriter));
this.partWritersSupplier = partWritersSupplier;
Expand Down Expand Up @@ -153,8 +153,9 @@ public List<HttpMessageWriter<?>> getPartWriters() {
* Return the configured form writer.
* @since 5.1.13
*/
@SuppressWarnings("unchecked")
public @Nullable HttpMessageWriter<MultiValueMap<String, String>> getFormWriter() {
return this.formWriter;
return (HttpMessageWriter<MultiValueMap<String, String>>) this.formWriter;
}


Expand Down