Browse Source

Do not read Map in FormHttpMessageConverter

This commit ensures that the FormHttpMessageConverter no longer supports
 reading Maps (just MultiValueMaps). Plain maps are often used to
 represent JSON.

See gh-32826
pull/32926/head
Arjen Poutsma 2 years ago
parent
commit
726ac9110c
  1. 53
      spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java
  2. 23
      spring-web/src/test/java/org/springframework/http/converter/FormHttpMessageConverterTests.java

53
spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java

@ -28,7 +28,6 @@ import java.util.Collections; @@ -28,7 +28,6 @@ import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import org.springframework.core.io.Resource;
import org.springframework.http.ContentDisposition;
@ -51,13 +50,24 @@ import org.springframework.util.StringUtils; @@ -51,13 +50,24 @@ import org.springframework.util.StringUtils;
* Implementation of {@link HttpMessageConverter} to read and write 'normal' HTML
* forms and also to write (but not read) multipart data (e.g. file uploads).
*
* <p>In other words, this converter can read and write the
* {@code "application/x-www-form-urlencoded"} media type as
* {@code Map<String, String>} or as
* {@link MultiValueMap MultiValueMap&lt;String, String&gt;}, and it can also
* write (but not read) the {@code "multipart/form-data"} and
* {@code "multipart/mixed"} media types as {@code Map<String, Object>} or as
* {@link MultiValueMap MultiValueMap&lt;String, Object&gt;}.
* <p>
* The following table shows an overview of the supported media and class types.
* <table border="1">
* <tr><th>Media type</th><th>Read</th><th>Write</th></tr>
* <tr>
* <td>{@code "application/x-www-form-urlencoded"}</td>
* <td>{@link MultiValueMap MultiValueMap&lt;String, String&gt;}</td>
* <td>{@link Map Map&lt;String, String&gt;}<br>
* {@link MultiValueMap MultiValueMap&lt;String, String&gt;}</td>
* </tr>
* <tr>
* <td>{@code "multipart/form-data"}<br>
* {@code "multipart/mixed"}</td>
* <td>Unsupported</td>
* <td>{@link Map Map&lt;String, Object&gt;}<br>
* {@link MultiValueMap MultiValueMap&lt;String, Object&gt;}</td>
* </tr>
* </table>
*
* <h3>Multipart Data</h3>
*
@ -158,8 +168,8 @@ import org.springframework.util.StringUtils; @@ -158,8 +168,8 @@ import org.springframework.util.StringUtils;
* {@code org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity}.
*
* <p>As of 6.2, the {@code FormHttpMessageConverter} is parameterized over
* {@code Map<String, ?>}, whereas before it was {@code MultiValueMap<String, ?>},
* in order to support single-value maps.
* {@code Map<String, ?>} in order to support writing single-value maps.
* Before 6.2, this class was parameterized over {@code MultiValueMap<String, ?>}.
*
* @author Arjen Poutsma
* @author Rossen Stoyanchev
@ -312,7 +322,7 @@ public class FormHttpMessageConverter implements HttpMessageConverter<Map<String @@ -312,7 +322,7 @@ public class FormHttpMessageConverter implements HttpMessageConverter<Map<String
@Override
public boolean canRead(Class<?> clazz, @Nullable MediaType mediaType) {
if (!Map.class.isAssignableFrom(clazz)) {
if (!MultiValueMap.class.isAssignableFrom(clazz)) {
return false;
}
if (mediaType == null) {
@ -354,32 +364,21 @@ public class FormHttpMessageConverter implements HttpMessageConverter<Map<String @@ -354,32 +364,21 @@ public class FormHttpMessageConverter implements HttpMessageConverter<Map<String
Charset charset = (contentType != null && contentType.getCharset() != null ?
contentType.getCharset() : this.charset);
String body = StreamUtils.copyToString(inputMessage.getBody(), charset);
String[] pairs = StringUtils.tokenizeToStringArray(body, "&");
if (clazz == null || MultiValueMap.class.isAssignableFrom(clazz)) {
MultiValueMap<String, String> result = new LinkedMultiValueMap<>(pairs.length);
readToMap(pairs, charset, result::add);
return result;
}
else {
Map<String, String> result = CollectionUtils.newLinkedHashMap(pairs.length);
readToMap(pairs, charset, result::putIfAbsent);
return result;
}
}
private static void readToMap(String[] pairs, Charset charset, BiConsumer<String, String> addFunction) {
String[] pairs = StringUtils.tokenizeToStringArray(body, "&");
MultiValueMap<String, String> result = new LinkedMultiValueMap<>(pairs.length);
for (String pair : pairs) {
int idx = pair.indexOf('=');
if (idx == -1) {
addFunction.accept(URLDecoder.decode(pair, charset), null);
result.add(URLDecoder.decode(pair, charset), null);
}
else {
String name = URLDecoder.decode(pair.substring(0, idx), charset);
String value = URLDecoder.decode(pair.substring(idx + 1), charset);
addFunction.accept(name, value);
result.add(name, value);
}
}
return result;
}
@Override

23
spring-web/src/test/java/org/springframework/http/converter/FormHttpMessageConverterTests.java

@ -73,7 +73,6 @@ class FormHttpMessageConverterTests { @@ -73,7 +73,6 @@ class FormHttpMessageConverterTests {
@Test
void canRead() {
assertCanRead(MultiValueMap.class, null);
assertCanRead(Map.class, null);
assertCanRead(APPLICATION_FORM_URLENCODED);
assertCannotRead(String.class, null);
@ -120,7 +119,7 @@ class FormHttpMessageConverterTests { @@ -120,7 +119,7 @@ class FormHttpMessageConverterTests {
@Test
@SuppressWarnings("unchecked")
void readFormMultiValue() throws Exception {
void readForm() throws Exception {
String body = "name+1=value+1&name+2=value+2%2B1&name+2=value+2%2B2&name+3";
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.ISO_8859_1));
inputMessage.getHeaders().setContentType(
@ -135,25 +134,7 @@ class FormHttpMessageConverterTests { @@ -135,25 +134,7 @@ class FormHttpMessageConverterTests {
}
@Test
@SuppressWarnings({ "rawtypes", "unchecked" })
void readFormSingleValue() throws Exception {
String body = "name+1=value+1&name+2=value+2%2B1&name+2=value+2%2B2&name+3";
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.ISO_8859_1));
inputMessage.getHeaders().setContentType(
new MediaType("application", "x-www-form-urlencoded", StandardCharsets.ISO_8859_1));
Object result = ((HttpMessageConverter) this.converter).read(Map.class, inputMessage);
assertThat(result).isInstanceOf(Map.class);
assertThat(result).isNotInstanceOf(MultiValueMap.class);
Map<String, String> map = (Map<String, String>) result;
assertThat(map).as("Invalid result").hasSize(3);
assertThat(map.get("name 1")).as("Invalid result").isEqualTo("value 1");
assertThat(map.get("name 2")).as("Invalid result").isEqualTo("value 2+1");
assertThat(map.get("name 3")).as("Invalid result").isNull();
}
@Test
void writeFormMultiValue() throws IOException {
void writeForm() throws IOException {
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.set("name 1", "value 1");
body.add("name 2", "value 2+1");

Loading…
Cancel
Save