Skip to content

Commit 859b97c

Browse files
committed
Revert "Do not read Map in FormHttpMessageConverter"
This reverts commit 726ac91 and 80faa94. See gh-32826
1 parent c127421 commit 859b97c

File tree

5 files changed

+47
-174
lines changed

5 files changed

+47
-174
lines changed

spring-test/src/main/java/org/springframework/test/web/client/match/ContentRequestMatchers.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,13 +174,12 @@ public RequestMatcher formDataContains(Map<String, String> expected) {
174174
return formData(multiValueMap, false);
175175
}
176176

177-
@SuppressWarnings("unchecked")
178177
private RequestMatcher formData(MultiValueMap<String, String> expectedMap, boolean containsExactly) {
179178
return request -> {
180179
MockClientHttpRequest mockRequest = (MockClientHttpRequest) request;
181180
MockHttpInputMessage message = new MockHttpInputMessage(mockRequest.getBodyAsBytes());
182181
message.getHeaders().putAll(mockRequest.getHeaders());
183-
MultiValueMap<String, String> actualMap = (MultiValueMap<String, String>) new FormHttpMessageConverter().read(null, message);
182+
MultiValueMap<String, String> actualMap = new FormHttpMessageConverter().read(null, message);
184183
if (containsExactly) {
185184
assertEquals("Form data", expectedMap, actualMap);
186185
}

spring-test/src/main/java/org/springframework/test/web/servlet/request/AbstractMockHttpServletRequestBuilder.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -902,7 +902,7 @@ public HttpHeaders getHeaders() {
902902
};
903903

904904
try {
905-
return (MultiValueMap<String, String>) new FormHttpMessageConverter().read(null, message);
905+
return new FormHttpMessageConverter().read(null, message);
906906
}
907907
catch (IOException ex) {
908908
throw new IllegalStateException("Failed to parse form data in request body", ex);

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

Lines changed: 39 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -50,24 +50,12 @@
5050
* Implementation of {@link HttpMessageConverter} to read and write 'normal' HTML
5151
* forms and also to write (but not read) multipart data (e.g. file uploads).
5252
*
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>
53+
* <p>In other words, this converter can read and write the
54+
* {@code "application/x-www-form-urlencoded"} media type as
55+
* {@link MultiValueMap MultiValueMap&lt;String, String&gt;}, and it can also
56+
* write (but not read) the {@code "multipart/form-data"} and
57+
* {@code "multipart/mixed"} media types as
58+
* {@link MultiValueMap MultiValueMap&lt;String, Object&gt;}.
7159
*
7260
* <h3>Multipart Data</h3>
7361
*
@@ -167,10 +155,6 @@
167155
* <p>Some methods in this class were inspired by
168156
* {@code org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity}.
169157
*
170-
* <p>As of 6.2, the {@code FormHttpMessageConverter} is parameterized over
171-
* {@code Map<String, ?>} in order to support writing single-value maps.
172-
* Before 6.2, this class was parameterized over {@code MultiValueMap<String, ?>}.
173-
*
174158
* @author Arjen Poutsma
175159
* @author Rossen Stoyanchev
176160
* @author Juergen Hoeller
@@ -179,7 +163,7 @@
179163
* @see org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter
180164
* @see org.springframework.util.MultiValueMap
181165
*/
182-
public class FormHttpMessageConverter implements HttpMessageConverter<Map<String, ?>> {
166+
public class FormHttpMessageConverter implements HttpMessageConverter<MultiValueMap<String, ?>> {
183167

184168
/** The default charset used by the converter. */
185169
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
@@ -342,7 +326,7 @@ public boolean canRead(Class<?> clazz, @Nullable MediaType mediaType) {
342326

343327
@Override
344328
public boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) {
345-
if (!Map.class.isAssignableFrom(clazz)) {
329+
if (!MultiValueMap.class.isAssignableFrom(clazz)) {
346330
return false;
347331
}
348332
if (mediaType == null || MediaType.ALL.equals(mediaType)) {
@@ -357,7 +341,7 @@ public boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) {
357341
}
358342

359343
@Override
360-
public Map<String, ?> read(@Nullable Class<? extends Map<String, ?>> clazz,
344+
public MultiValueMap<String, String> read(@Nullable Class<? extends MultiValueMap<String, ?>> clazz,
361345
HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
362346

363347
MediaType contentType = inputMessage.getHeaders().getContentType();
@@ -383,38 +367,33 @@ public boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) {
383367

384368
@Override
385369
@SuppressWarnings("unchecked")
386-
public void write(Map<String, ?> map, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
370+
public void write(MultiValueMap<String, ?> map, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
387371
throws IOException, HttpMessageNotWritableException {
388372

389373
if (isMultipart(map, contentType)) {
390-
writeMultipart((Map<String, Object>) map, contentType, outputMessage);
374+
writeMultipart((MultiValueMap<String, Object>) map, contentType, outputMessage);
391375
}
392376
else {
393-
writeForm((Map<String, Object>) map, contentType, outputMessage);
377+
writeForm((MultiValueMap<String, Object>) map, contentType, outputMessage);
394378
}
395379
}
396380

397381

398-
private boolean isMultipart(Map<String, ?> map, @Nullable MediaType contentType) {
382+
private boolean isMultipart(MultiValueMap<String, ?> map, @Nullable MediaType contentType) {
399383
if (contentType != null) {
400384
return contentType.getType().equalsIgnoreCase("multipart");
401385
}
402-
for (Object value : map.values()) {
403-
if (value instanceof List<?> values) {
404-
for (Object v : values) {
405-
if (v != null && !(v instanceof String)) {
406-
return true;
407-
}
386+
for (List<?> values : map.values()) {
387+
for (Object value : values) {
388+
if (value != null && !(value instanceof String)) {
389+
return true;
408390
}
409391
}
410-
else if (value != null && !(value instanceof String)) {
411-
return true;
412-
}
413392
}
414393
return false;
415394
}
416395

417-
private void writeForm(Map<String, Object> formData, @Nullable MediaType mediaType,
396+
private void writeForm(MultiValueMap<String, Object> formData, @Nullable MediaType mediaType,
418397
HttpOutputMessage outputMessage) throws IOException {
419398

420399
mediaType = getFormContentType(mediaType);
@@ -462,36 +441,30 @@ protected MediaType getFormContentType(@Nullable MediaType contentType) {
462441
return contentType;
463442
}
464443

465-
protected String serializeForm(Map<String, Object> formData, Charset charset) {
444+
protected String serializeForm(MultiValueMap<String, Object> formData, Charset charset) {
466445
StringBuilder builder = new StringBuilder();
467-
formData.forEach((name, value) -> {
468-
if (value instanceof List<?> values) {
446+
formData.forEach((name, values) -> {
469447
if (name == null) {
470448
Assert.isTrue(CollectionUtils.isEmpty(values), () -> "Null name in form data: " + formData);
471449
return;
472450
}
473-
values.forEach(v -> appendFormValue(builder, name, v, charset));
474-
}
475-
else {
476-
appendFormValue(builder, name, value, charset);
477-
}
451+
values.forEach(value -> {
452+
if (builder.length() != 0) {
453+
builder.append('&');
454+
}
455+
builder.append(URLEncoder.encode(name, charset));
456+
if (value != null) {
457+
builder.append('=');
458+
builder.append(URLEncoder.encode(String.valueOf(value), charset));
459+
}
460+
});
478461
});
479-
return builder.toString();
480-
}
481462

482-
private static void appendFormValue(StringBuilder builder, String name, @Nullable Object value, Charset charset) {
483-
if (!builder.isEmpty()) {
484-
builder.append('&');
485-
}
486-
builder.append(URLEncoder.encode(name, charset));
487-
if (value != null) {
488-
builder.append('=');
489-
builder.append(URLEncoder.encode(String.valueOf(value), charset));
490-
}
463+
return builder.toString();
491464
}
492465

493466
private void writeMultipart(
494-
Map<String, Object> parts, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
467+
MultiValueMap<String, Object> parts, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
495468
throws IOException {
496469

497470
// If the supplied content type is null, fall back to multipart/form-data.
@@ -538,24 +511,16 @@ private boolean isFilenameCharsetSet() {
538511
return (this.multipartCharset != null);
539512
}
540513

541-
private void writeParts(OutputStream os, Map<String, Object> parts, byte[] boundary) throws IOException {
542-
for (Map.Entry<String, Object> entry : parts.entrySet()) {
514+
private void writeParts(OutputStream os, MultiValueMap<String, Object> parts, byte[] boundary) throws IOException {
515+
for (Map.Entry<String, List<Object>> entry : parts.entrySet()) {
543516
String name = entry.getKey();
544-
Object value = entry.getValue();
545-
if (value instanceof List<?> values) {
546-
for (Object part : values) {
547-
if (part != null) {
548-
writeBoundary(os, boundary);
549-
writePart(name, getHttpEntity(part), os);
550-
writeNewLine(os);
551-
}
517+
for (Object part : entry.getValue()) {
518+
if (part != null) {
519+
writeBoundary(os, boundary);
520+
writePart(name, getHttpEntity(part), os);
521+
writeNewLine(os);
552522
}
553523
}
554-
else if (value != null) {
555-
writeBoundary(os, boundary);
556-
writePart(name, getHttpEntity(value), os);
557-
writeNewLine(os);
558-
}
559524
}
560525
}
561526

spring-web/src/main/java/org/springframework/web/filter/FormContentFilter.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -95,7 +95,6 @@ protected void doFilterInternal(
9595
}
9696

9797
@Nullable
98-
@SuppressWarnings("unchecked")
9998
private MultiValueMap<String, String> parseIfNecessary(HttpServletRequest request) throws IOException {
10099
if (!shouldParse(request)) {
101100
return null;
@@ -107,7 +106,7 @@ public InputStream getBody() throws IOException {
107106
return request.getInputStream();
108107
}
109108
};
110-
return (MultiValueMap<String, String>) this.formConverter.read(null, inputMessage);
109+
return this.formConverter.read(null, inputMessage);
111110
}
112111

113112
private boolean shouldParse(HttpServletRequest request) {

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

Lines changed: 4 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -118,13 +118,12 @@ void addSupportedMediaTypes() {
118118
}
119119

120120
@Test
121-
@SuppressWarnings("unchecked")
122121
void readForm() throws Exception {
123122
String body = "name+1=value+1&name+2=value+2%2B1&name+2=value+2%2B2&name+3";
124123
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.ISO_8859_1));
125124
inputMessage.getHeaders().setContentType(
126125
new MediaType("application", "x-www-form-urlencoded", StandardCharsets.ISO_8859_1));
127-
MultiValueMap<String, String> result = (MultiValueMap<String, String>) this.converter.read(null, inputMessage);
126+
MultiValueMap<String, String> result = this.converter.read(null, inputMessage);
128127

129128
assertThat(result).as("Invalid result").hasSize(3);
130129
assertThat(result.getFirst("name 1")).as("Invalid result").isEqualTo("value 1");
@@ -152,24 +151,7 @@ void writeForm() throws IOException {
152151
}
153152

154153
@Test
155-
void writeFormSingleValue() throws IOException {
156-
Map<String, String> body = new LinkedHashMap<>();
157-
body.put("name 1", "value 1");
158-
body.put("name 2", "value 2");
159-
body.put("name 3", null);
160-
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
161-
this.converter.write(body, APPLICATION_FORM_URLENCODED, outputMessage);
162-
163-
assertThat(outputMessage.getBodyAsString(UTF_8))
164-
.as("Invalid result").isEqualTo("name+1=value+1&name+2=value+2&name+3");
165-
assertThat(outputMessage.getHeaders().getContentType())
166-
.as("Invalid content-type").isEqualTo(APPLICATION_FORM_URLENCODED);
167-
assertThat(outputMessage.getHeaders().getContentLength())
168-
.as("Invalid content-length").isEqualTo(outputMessage.getBodyAsBytes().length);
169-
}
170-
171-
@Test
172-
void writeMultipartMultiValue() throws Exception {
154+
void writeMultipart() throws Exception {
173155

174156
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
175157
parts.add("name 1", "value 1");
@@ -246,78 +228,6 @@ public String getFilename() {
246228
assertThat(item.getContentType()).isEqualTo("application/json");
247229
}
248230

249-
@Test
250-
void writeMultipartSingleValue() throws Exception {
251-
252-
Map<String, Object> parts = new LinkedHashMap<>();
253-
parts.put("name 1", "value 1");
254-
parts.put("name 2", "value 2");
255-
parts.put("name 3", null);
256-
257-
Resource logo = new ClassPathResource("/org/springframework/http/converter/logo.jpg");
258-
parts.put("logo", logo);
259-
260-
// SPR-12108
261-
Resource utf8 = new ClassPathResource("/org/springframework/http/converter/logo.jpg") {
262-
@Override
263-
public String getFilename() {
264-
return "Hall\u00F6le.jpg";
265-
}
266-
};
267-
parts.put("utf8", utf8);
268-
269-
MyBean myBean = new MyBean();
270-
myBean.setString("foo");
271-
HttpHeaders entityHeaders = new HttpHeaders();
272-
entityHeaders.setContentType(APPLICATION_JSON);
273-
HttpEntity<MyBean> entity = new HttpEntity<>(myBean, entityHeaders);
274-
parts.put("json", entity);
275-
276-
Map<String, String> parameters = new LinkedHashMap<>(2);
277-
parameters.put("charset", UTF_8.name());
278-
parameters.put("foo", "bar");
279-
280-
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
281-
this.converter.write(parts, new MediaType("multipart", "form-data", parameters), outputMessage);
282-
283-
final MediaType contentType = outputMessage.getHeaders().getContentType();
284-
assertThat(contentType.getParameters()).containsKeys("charset", "boundary", "foo"); // gh-21568, gh-25839
285-
286-
// see if Commons FileUpload can read what we wrote
287-
FileUpload fileUpload = new FileUpload();
288-
fileUpload.setFileItemFactory(new DiskFileItemFactory());
289-
RequestContext requestContext = new MockHttpOutputMessageRequestContext(outputMessage);
290-
List<FileItem> items = fileUpload.parseRequest(requestContext);
291-
assertThat(items).hasSize(5);
292-
FileItem item = items.get(0);
293-
assertThat(item.isFormField()).isTrue();
294-
assertThat(item.getFieldName()).isEqualTo("name 1");
295-
assertThat(item.getString()).isEqualTo("value 1");
296-
297-
item = items.get(1);
298-
assertThat(item.isFormField()).isTrue();
299-
assertThat(item.getFieldName()).isEqualTo("name 2");
300-
assertThat(item.getString()).isEqualTo("value 2");
301-
302-
item = items.get(2);
303-
assertThat(item.isFormField()).isFalse();
304-
assertThat(item.getFieldName()).isEqualTo("logo");
305-
assertThat(item.getName()).isEqualTo("logo.jpg");
306-
assertThat(item.getContentType()).isEqualTo("image/jpeg");
307-
assertThat(item.getSize()).isEqualTo(logo.getFile().length());
308-
309-
item = items.get(3);
310-
assertThat(item.isFormField()).isFalse();
311-
assertThat(item.getFieldName()).isEqualTo("utf8");
312-
assertThat(item.getName()).isEqualTo("Hall\u00F6le.jpg");
313-
assertThat(item.getContentType()).isEqualTo("image/jpeg");
314-
assertThat(item.getSize()).isEqualTo(logo.getFile().length());
315-
316-
item = items.get(4);
317-
assertThat(item.getFieldName()).isEqualTo("json");
318-
assertThat(item.getContentType()).isEqualTo("application/json");
319-
}
320-
321231
@Test
322232
void writeMultipartWithSourceHttpMessageConverter() throws Exception {
323233

@@ -491,8 +401,8 @@ private void assertCannotRead(Class<?> clazz, MediaType mediaType) {
491401
}
492402

493403
private void assertCanWrite(MediaType mediaType) {
494-
assertThat(this.converter.canWrite(MultiValueMap.class, mediaType)).as("MultiValueMap : " + mediaType).isTrue();
495-
assertThat(this.converter.canWrite(Map.class, mediaType)).as("Map : " + mediaType).isTrue();
404+
Class<?> clazz = MultiValueMap.class;
405+
assertThat(this.converter.canWrite(clazz, mediaType)).as(clazz.getSimpleName() + " : " + mediaType).isTrue();
496406
}
497407

498408
private void assertCannotWrite(MediaType mediaType) {

0 commit comments

Comments
 (0)