Revert "Do not read Map in FormHttpMessageConverter"

This reverts commit 726ac9110c and
80faa94afc.

See gh-32826
This commit is contained in:
Arjen Poutsma
2024-06-06 12:25:20 +02:00
parent c127421e38
commit 859b97ce05
5 changed files with 47 additions and 174 deletions

View File

@@ -174,13 +174,12 @@ public class ContentRequestMatchers {
return formData(multiValueMap, false);
}
@SuppressWarnings("unchecked")
private RequestMatcher formData(MultiValueMap<String, String> expectedMap, boolean containsExactly) {
return request -> {
MockClientHttpRequest mockRequest = (MockClientHttpRequest) request;
MockHttpInputMessage message = new MockHttpInputMessage(mockRequest.getBodyAsBytes());
message.getHeaders().putAll(mockRequest.getHeaders());
MultiValueMap<String, String> actualMap = (MultiValueMap<String, String>) new FormHttpMessageConverter().read(null, message);
MultiValueMap<String, String> actualMap = new FormHttpMessageConverter().read(null, message);
if (containsExactly) {
assertEquals("Form data", expectedMap, actualMap);
}

View File

@@ -902,7 +902,7 @@ public abstract class AbstractMockHttpServletRequestBuilder<B extends AbstractMo
};
try {
return (MultiValueMap<String, String>) new FormHttpMessageConverter().read(null, message);
return new FormHttpMessageConverter().read(null, message);
}
catch (IOException ex) {
throw new IllegalStateException("Failed to parse form data in request body", ex);

View File

@@ -50,24 +50,12 @@ 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>
* 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>
* <p>In other words, this converter can read and write the
* {@code "application/x-www-form-urlencoded"} media type 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
* {@link MultiValueMap MultiValueMap&lt;String, Object&gt;}.
*
* <h3>Multipart Data</h3>
*
@@ -167,10 +155,6 @@ import org.springframework.util.StringUtils;
* <p>Some methods in this class were inspired by
* {@code org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity}.
*
* <p>As of 6.2, the {@code FormHttpMessageConverter} is parameterized over
* {@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
* @author Juergen Hoeller
@@ -179,7 +163,7 @@ import org.springframework.util.StringUtils;
* @see org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter
* @see org.springframework.util.MultiValueMap
*/
public class FormHttpMessageConverter implements HttpMessageConverter<Map<String, ?>> {
public class FormHttpMessageConverter implements HttpMessageConverter<MultiValueMap<String, ?>> {
/** The default charset used by the converter. */
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
@@ -342,7 +326,7 @@ public class FormHttpMessageConverter implements HttpMessageConverter<Map<String
@Override
public boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) {
if (!Map.class.isAssignableFrom(clazz)) {
if (!MultiValueMap.class.isAssignableFrom(clazz)) {
return false;
}
if (mediaType == null || MediaType.ALL.equals(mediaType)) {
@@ -357,7 +341,7 @@ public class FormHttpMessageConverter implements HttpMessageConverter<Map<String
}
@Override
public Map<String, ?> read(@Nullable Class<? extends Map<String, ?>> clazz,
public MultiValueMap<String, String> read(@Nullable Class<? extends MultiValueMap<String, ?>> clazz,
HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
MediaType contentType = inputMessage.getHeaders().getContentType();
@@ -383,38 +367,33 @@ public class FormHttpMessageConverter implements HttpMessageConverter<Map<String
@Override
@SuppressWarnings("unchecked")
public void write(Map<String, ?> map, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
public void write(MultiValueMap<String, ?> map, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
if (isMultipart(map, contentType)) {
writeMultipart((Map<String, Object>) map, contentType, outputMessage);
writeMultipart((MultiValueMap<String, Object>) map, contentType, outputMessage);
}
else {
writeForm((Map<String, Object>) map, contentType, outputMessage);
writeForm((MultiValueMap<String, Object>) map, contentType, outputMessage);
}
}
private boolean isMultipart(Map<String, ?> map, @Nullable MediaType contentType) {
private boolean isMultipart(MultiValueMap<String, ?> map, @Nullable MediaType contentType) {
if (contentType != null) {
return contentType.getType().equalsIgnoreCase("multipart");
}
for (Object value : map.values()) {
if (value instanceof List<?> values) {
for (Object v : values) {
if (v != null && !(v instanceof String)) {
return true;
}
for (List<?> values : map.values()) {
for (Object value : values) {
if (value != null && !(value instanceof String)) {
return true;
}
}
else if (value != null && !(value instanceof String)) {
return true;
}
}
return false;
}
private void writeForm(Map<String, Object> formData, @Nullable MediaType mediaType,
private void writeForm(MultiValueMap<String, Object> formData, @Nullable MediaType mediaType,
HttpOutputMessage outputMessage) throws IOException {
mediaType = getFormContentType(mediaType);
@@ -462,36 +441,30 @@ public class FormHttpMessageConverter implements HttpMessageConverter<Map<String
return contentType;
}
protected String serializeForm(Map<String, Object> formData, Charset charset) {
protected String serializeForm(MultiValueMap<String, Object> formData, Charset charset) {
StringBuilder builder = new StringBuilder();
formData.forEach((name, value) -> {
if (value instanceof List<?> values) {
formData.forEach((name, values) -> {
if (name == null) {
Assert.isTrue(CollectionUtils.isEmpty(values), () -> "Null name in form data: " + formData);
return;
}
values.forEach(v -> appendFormValue(builder, name, v, charset));
}
else {
appendFormValue(builder, name, value, charset);
}
values.forEach(value -> {
if (builder.length() != 0) {
builder.append('&');
}
builder.append(URLEncoder.encode(name, charset));
if (value != null) {
builder.append('=');
builder.append(URLEncoder.encode(String.valueOf(value), charset));
}
});
});
return builder.toString();
}
private static void appendFormValue(StringBuilder builder, String name, @Nullable Object value, Charset charset) {
if (!builder.isEmpty()) {
builder.append('&');
}
builder.append(URLEncoder.encode(name, charset));
if (value != null) {
builder.append('=');
builder.append(URLEncoder.encode(String.valueOf(value), charset));
}
}
private void writeMultipart(
Map<String, Object> parts, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
MultiValueMap<String, Object> parts, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
throws IOException {
// If the supplied content type is null, fall back to multipart/form-data.
@@ -538,24 +511,16 @@ public class FormHttpMessageConverter implements HttpMessageConverter<Map<String
return (this.multipartCharset != null);
}
private void writeParts(OutputStream os, Map<String, Object> parts, byte[] boundary) throws IOException {
for (Map.Entry<String, Object> entry : parts.entrySet()) {
private void writeParts(OutputStream os, MultiValueMap<String, Object> parts, byte[] boundary) throws IOException {
for (Map.Entry<String, List<Object>> entry : parts.entrySet()) {
String name = entry.getKey();
Object value = entry.getValue();
if (value instanceof List<?> values) {
for (Object part : values) {
if (part != null) {
writeBoundary(os, boundary);
writePart(name, getHttpEntity(part), os);
writeNewLine(os);
}
for (Object part : entry.getValue()) {
if (part != null) {
writeBoundary(os, boundary);
writePart(name, getHttpEntity(part), os);
writeNewLine(os);
}
}
else if (value != null) {
writeBoundary(os, boundary);
writePart(name, getHttpEntity(value), os);
writeNewLine(os);
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -95,7 +95,6 @@ public class FormContentFilter extends OncePerRequestFilter {
}
@Nullable
@SuppressWarnings("unchecked")
private MultiValueMap<String, String> parseIfNecessary(HttpServletRequest request) throws IOException {
if (!shouldParse(request)) {
return null;
@@ -107,7 +106,7 @@ public class FormContentFilter extends OncePerRequestFilter {
return request.getInputStream();
}
};
return (MultiValueMap<String, String>) this.formConverter.read(null, inputMessage);
return this.formConverter.read(null, inputMessage);
}
private boolean shouldParse(HttpServletRequest request) {

View File

@@ -118,13 +118,12 @@ class FormHttpMessageConverterTests {
}
@Test
@SuppressWarnings("unchecked")
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(
new MediaType("application", "x-www-form-urlencoded", StandardCharsets.ISO_8859_1));
MultiValueMap<String, String> result = (MultiValueMap<String, String>) this.converter.read(null, inputMessage);
MultiValueMap<String, String> result = this.converter.read(null, inputMessage);
assertThat(result).as("Invalid result").hasSize(3);
assertThat(result.getFirst("name 1")).as("Invalid result").isEqualTo("value 1");
@@ -152,24 +151,7 @@ class FormHttpMessageConverterTests {
}
@Test
void writeFormSingleValue() throws IOException {
Map<String, String> body = new LinkedHashMap<>();
body.put("name 1", "value 1");
body.put("name 2", "value 2");
body.put("name 3", null);
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
this.converter.write(body, APPLICATION_FORM_URLENCODED, outputMessage);
assertThat(outputMessage.getBodyAsString(UTF_8))
.as("Invalid result").isEqualTo("name+1=value+1&name+2=value+2&name+3");
assertThat(outputMessage.getHeaders().getContentType())
.as("Invalid content-type").isEqualTo(APPLICATION_FORM_URLENCODED);
assertThat(outputMessage.getHeaders().getContentLength())
.as("Invalid content-length").isEqualTo(outputMessage.getBodyAsBytes().length);
}
@Test
void writeMultipartMultiValue() throws Exception {
void writeMultipart() throws Exception {
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
parts.add("name 1", "value 1");
@@ -246,78 +228,6 @@ class FormHttpMessageConverterTests {
assertThat(item.getContentType()).isEqualTo("application/json");
}
@Test
void writeMultipartSingleValue() throws Exception {
Map<String, Object> parts = new LinkedHashMap<>();
parts.put("name 1", "value 1");
parts.put("name 2", "value 2");
parts.put("name 3", null);
Resource logo = new ClassPathResource("/org/springframework/http/converter/logo.jpg");
parts.put("logo", logo);
// SPR-12108
Resource utf8 = new ClassPathResource("/org/springframework/http/converter/logo.jpg") {
@Override
public String getFilename() {
return "Hall\u00F6le.jpg";
}
};
parts.put("utf8", utf8);
MyBean myBean = new MyBean();
myBean.setString("foo");
HttpHeaders entityHeaders = new HttpHeaders();
entityHeaders.setContentType(APPLICATION_JSON);
HttpEntity<MyBean> entity = new HttpEntity<>(myBean, entityHeaders);
parts.put("json", entity);
Map<String, String> parameters = new LinkedHashMap<>(2);
parameters.put("charset", UTF_8.name());
parameters.put("foo", "bar");
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
this.converter.write(parts, new MediaType("multipart", "form-data", parameters), outputMessage);
final MediaType contentType = outputMessage.getHeaders().getContentType();
assertThat(contentType.getParameters()).containsKeys("charset", "boundary", "foo"); // gh-21568, gh-25839
// see if Commons FileUpload can read what we wrote
FileUpload fileUpload = new FileUpload();
fileUpload.setFileItemFactory(new DiskFileItemFactory());
RequestContext requestContext = new MockHttpOutputMessageRequestContext(outputMessage);
List<FileItem> items = fileUpload.parseRequest(requestContext);
assertThat(items).hasSize(5);
FileItem item = items.get(0);
assertThat(item.isFormField()).isTrue();
assertThat(item.getFieldName()).isEqualTo("name 1");
assertThat(item.getString()).isEqualTo("value 1");
item = items.get(1);
assertThat(item.isFormField()).isTrue();
assertThat(item.getFieldName()).isEqualTo("name 2");
assertThat(item.getString()).isEqualTo("value 2");
item = items.get(2);
assertThat(item.isFormField()).isFalse();
assertThat(item.getFieldName()).isEqualTo("logo");
assertThat(item.getName()).isEqualTo("logo.jpg");
assertThat(item.getContentType()).isEqualTo("image/jpeg");
assertThat(item.getSize()).isEqualTo(logo.getFile().length());
item = items.get(3);
assertThat(item.isFormField()).isFalse();
assertThat(item.getFieldName()).isEqualTo("utf8");
assertThat(item.getName()).isEqualTo("Hall\u00F6le.jpg");
assertThat(item.getContentType()).isEqualTo("image/jpeg");
assertThat(item.getSize()).isEqualTo(logo.getFile().length());
item = items.get(4);
assertThat(item.getFieldName()).isEqualTo("json");
assertThat(item.getContentType()).isEqualTo("application/json");
}
@Test
void writeMultipartWithSourceHttpMessageConverter() throws Exception {
@@ -491,8 +401,8 @@ class FormHttpMessageConverterTests {
}
private void assertCanWrite(MediaType mediaType) {
assertThat(this.converter.canWrite(MultiValueMap.class, mediaType)).as("MultiValueMap : " + mediaType).isTrue();
assertThat(this.converter.canWrite(Map.class, mediaType)).as("Map : " + mediaType).isTrue();
Class<?> clazz = MultiValueMap.class;
assertThat(this.converter.canWrite(clazz, mediaType)).as(clazz.getSimpleName() + " : " + mediaType).isTrue();
}
private void assertCannotWrite(MediaType mediaType) {