Skip to content

Commit 726ac91

Browse files
committed
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
1 parent fc54cf4 commit 726ac91

File tree

2 files changed

+28
-48
lines changed

2 files changed

+28
-48
lines changed

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

+26-27
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
import java.util.LinkedHashMap;
2929
import java.util.List;
3030
import java.util.Map;
31-
import java.util.function.BiConsumer;
3231

3332
import org.springframework.core.io.Resource;
3433
import org.springframework.http.ContentDisposition;
@@ -51,13 +50,24 @@
5150
* Implementation of {@link HttpMessageConverter} to read and write 'normal' HTML
5251
* forms and also to write (but not read) multipart data (e.g. file uploads).
5352
*
54-
* <p>In other words, this converter can read and write the
55-
* {@code "application/x-www-form-urlencoded"} media type as
56-
* {@code Map<String, String>} or as
57-
* {@link MultiValueMap MultiValueMap&lt;String, String&gt;}, and it can also
58-
* write (but not read) the {@code "multipart/form-data"} and
59-
* {@code "multipart/mixed"} media types as {@code Map<String, Object>} or as
60-
* {@link MultiValueMap MultiValueMap&lt;String, Object&gt;}.
53+
* <p>
54+
* The following table shows an overview of the supported media and class types.
55+
* <table border="1">
56+
* <tr><th>Media type</th><th>Read</th><th>Write</th></tr>
57+
* <tr>
58+
* <td>{@code "application/x-www-form-urlencoded"}</td>
59+
* <td>{@link MultiValueMap MultiValueMap&lt;String, String&gt;}</td>
60+
* <td>{@link Map Map&lt;String, String&gt;}<br>
61+
* {@link MultiValueMap MultiValueMap&lt;String, String&gt;}</td>
62+
* </tr>
63+
* <tr>
64+
* <td>{@code "multipart/form-data"}<br>
65+
* {@code "multipart/mixed"}</td>
66+
* <td>Unsupported</td>
67+
* <td>{@link Map Map&lt;String, Object&gt;}<br>
68+
* {@link MultiValueMap MultiValueMap&lt;String, Object&gt;}</td>
69+
* </tr>
70+
* </table>
6171
*
6272
* <h3>Multipart Data</h3>
6373
*
@@ -158,8 +168,8 @@
158168
* {@code org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity}.
159169
*
160170
* <p>As of 6.2, the {@code FormHttpMessageConverter} is parameterized over
161-
* {@code Map<String, ?>}, whereas before it was {@code MultiValueMap<String, ?>},
162-
* in order to support single-value maps.
171+
* {@code Map<String, ?>} in order to support writing single-value maps.
172+
* Before 6.2, this class was parameterized over {@code MultiValueMap<String, ?>}.
163173
*
164174
* @author Arjen Poutsma
165175
* @author Rossen Stoyanchev
@@ -312,7 +322,7 @@ public void setMultipartCharset(Charset charset) {
312322

313323
@Override
314324
public boolean canRead(Class<?> clazz, @Nullable MediaType mediaType) {
315-
if (!Map.class.isAssignableFrom(clazz)) {
325+
if (!MultiValueMap.class.isAssignableFrom(clazz)) {
316326
return false;
317327
}
318328
if (mediaType == null) {
@@ -354,32 +364,21 @@ public boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) {
354364
Charset charset = (contentType != null && contentType.getCharset() != null ?
355365
contentType.getCharset() : this.charset);
356366
String body = StreamUtils.copyToString(inputMessage.getBody(), charset);
357-
String[] pairs = StringUtils.tokenizeToStringArray(body, "&");
358-
359-
if (clazz == null || MultiValueMap.class.isAssignableFrom(clazz)) {
360-
MultiValueMap<String, String> result = new LinkedMultiValueMap<>(pairs.length);
361-
readToMap(pairs, charset, result::add);
362-
return result;
363-
}
364-
else {
365-
Map<String, String> result = CollectionUtils.newLinkedHashMap(pairs.length);
366-
readToMap(pairs, charset, result::putIfAbsent);
367-
return result;
368-
}
369-
}
370367

371-
private static void readToMap(String[] pairs, Charset charset, BiConsumer<String, String> addFunction) {
368+
String[] pairs = StringUtils.tokenizeToStringArray(body, "&");
369+
MultiValueMap<String, String> result = new LinkedMultiValueMap<>(pairs.length);
372370
for (String pair : pairs) {
373371
int idx = pair.indexOf('=');
374372
if (idx == -1) {
375-
addFunction.accept(URLDecoder.decode(pair, charset), null);
373+
result.add(URLDecoder.decode(pair, charset), null);
376374
}
377375
else {
378376
String name = URLDecoder.decode(pair.substring(0, idx), charset);
379377
String value = URLDecoder.decode(pair.substring(idx + 1), charset);
380-
addFunction.accept(name, value);
378+
result.add(name, value);
381379
}
382380
}
381+
return result;
383382
}
384383

385384
@Override

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

+2-21
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,6 @@ class FormHttpMessageConverterTests {
7373
@Test
7474
void canRead() {
7575
assertCanRead(MultiValueMap.class, null);
76-
assertCanRead(Map.class, null);
7776
assertCanRead(APPLICATION_FORM_URLENCODED);
7877

7978
assertCannotRead(String.class, null);
@@ -120,7 +119,7 @@ void addSupportedMediaTypes() {
120119

121120
@Test
122121
@SuppressWarnings("unchecked")
123-
void readFormMultiValue() throws Exception {
122+
void readForm() throws Exception {
124123
String body = "name+1=value+1&name+2=value+2%2B1&name+2=value+2%2B2&name+3";
125124
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.ISO_8859_1));
126125
inputMessage.getHeaders().setContentType(
@@ -135,25 +134,7 @@ void readFormMultiValue() throws Exception {
135134
}
136135

137136
@Test
138-
@SuppressWarnings({ "rawtypes", "unchecked" })
139-
void readFormSingleValue() throws Exception {
140-
String body = "name+1=value+1&name+2=value+2%2B1&name+2=value+2%2B2&name+3";
141-
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.ISO_8859_1));
142-
inputMessage.getHeaders().setContentType(
143-
new MediaType("application", "x-www-form-urlencoded", StandardCharsets.ISO_8859_1));
144-
Object result = ((HttpMessageConverter) this.converter).read(Map.class, inputMessage);
145-
146-
assertThat(result).isInstanceOf(Map.class);
147-
assertThat(result).isNotInstanceOf(MultiValueMap.class);
148-
Map<String, String> map = (Map<String, String>) result;
149-
assertThat(map).as("Invalid result").hasSize(3);
150-
assertThat(map.get("name 1")).as("Invalid result").isEqualTo("value 1");
151-
assertThat(map.get("name 2")).as("Invalid result").isEqualTo("value 2+1");
152-
assertThat(map.get("name 3")).as("Invalid result").isNull();
153-
}
154-
155-
@Test
156-
void writeFormMultiValue() throws IOException {
137+
void writeForm() throws IOException {
157138
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
158139
body.set("name 1", "value 1");
159140
body.add("name 2", "value 2+1");

0 commit comments

Comments
 (0)