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
This commit is contained in:
Arjen Poutsma
2024-05-28 15:25:08 +02:00
parent fc54cf4430
commit 726ac9110c
2 changed files with 28 additions and 48 deletions

View File

@@ -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;
* 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;
* {@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
@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
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) {
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

View File

@@ -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 {
@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 {
}
@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");