Skip to content

Commit 1486102

Browse files
committed
WIP
1 parent fa275f9 commit 1486102

File tree

5 files changed

+111
-40
lines changed

5 files changed

+111
-40
lines changed

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

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

177+
@SuppressWarnings("unchecked")
177178
private RequestMatcher formData(MultiValueMap<String, String> expectedMap, boolean containsExactly) {
178179
return request -> {
179180
MockClientHttpRequest mockRequest = (MockClientHttpRequest) request;
180181
MockHttpInputMessage message = new MockHttpInputMessage(mockRequest.getBodyAsBytes());
181182
message.getHeaders().putAll(mockRequest.getHeaders());
182-
MultiValueMap<String, String> actualMap = new FormHttpMessageConverter().read(null, message);
183+
MultiValueMap<String, String> actualMap = (MultiValueMap<String, String>) new FormHttpMessageConverter().read(null, message);
183184
if (containsExactly) {
184185
assertEquals("Form data", expectedMap, actualMap);
185186
}

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -899,6 +899,7 @@ public HttpHeaders getHeaders() {
899899
}
900900
}
901901

902+
@SuppressWarnings("unchecked")
902903
private MultiValueMap<String, String> parseFormData(MediaType mediaType) {
903904
HttpInputMessage message = new HttpInputMessage() {
904905
@Override
@@ -914,7 +915,7 @@ public HttpHeaders getHeaders() {
914915
};
915916

916917
try {
917-
return new FormHttpMessageConverter().read(null, message);
918+
return (MultiValueMap<String, String>) new FormHttpMessageConverter().read(null, message);
918919
}
919920
catch (IOException ex) {
920921
throw new IllegalStateException("Failed to parse form data in request body", ex);

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

+86-34
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@
152152
* @see org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter
153153
* @see org.springframework.util.MultiValueMap
154154
*/
155-
public class FormHttpMessageConverter implements HttpMessageConverter<MultiValueMap<String, ?>> {
155+
public class FormHttpMessageConverter implements HttpMessageConverter<Map<String, ?>> {
156156

157157
/** The default charset used by the converter. */
158158
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
@@ -295,7 +295,7 @@ public void setMultipartCharset(Charset charset) {
295295

296296
@Override
297297
public boolean canRead(Class<?> clazz, @Nullable MediaType mediaType) {
298-
if (!MultiValueMap.class.isAssignableFrom(clazz)) {
298+
if (!Map.class.isAssignableFrom(clazz)) {
299299
return false;
300300
}
301301
if (mediaType == null) {
@@ -330,7 +330,7 @@ public boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) {
330330
}
331331

332332
@Override
333-
public MultiValueMap<String, String> read(@Nullable Class<? extends MultiValueMap<String, ?>> clazz,
333+
public Map<String, ?> read(@Nullable Class<? extends Map<String, ?>> clazz,
334334
HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
335335

336336
MediaType contentType = inputMessage.getHeaders().getContentType();
@@ -339,41 +339,76 @@ public MultiValueMap<String, String> read(@Nullable Class<? extends MultiValueMa
339339
String body = StreamUtils.copyToString(inputMessage.getBody(), charset);
340340

341341
String[] pairs = StringUtils.tokenizeToStringArray(body, "&");
342-
MultiValueMap<String, String> result = new LinkedMultiValueMap<>(pairs.length);
342+
Map<String, String> svResult = null;
343+
MultiValueMap<String, String> mvResult = null;
344+
if (clazz == null || MultiValueMap.class.isAssignableFrom(clazz)) {
345+
mvResult = new LinkedMultiValueMap<>(pairs.length);
346+
}
347+
else {
348+
svResult = CollectionUtils.newLinkedHashMap(pairs.length);
349+
}
343350
for (String pair : pairs) {
344351
int idx = pair.indexOf('=');
345352
if (idx == -1) {
346-
result.add(URLDecoder.decode(pair, charset), null);
353+
String name = URLDecoder.decode(pair, charset);
354+
if (mvResult != null) {
355+
mvResult.add(name, null);
356+
}
357+
else if (svResult != null) {
358+
svResult.put(name, null);
359+
}
347360
}
348361
else {
349362
String name = URLDecoder.decode(pair.substring(0, idx), charset);
350363
String value = URLDecoder.decode(pair.substring(idx + 1), charset);
351-
result.add(name, value);
364+
if (mvResult != null) {
365+
mvResult.add(name, value);
366+
}
367+
else if (svResult != null) {
368+
svResult.put(name, value);
369+
}
352370
}
353371
}
354-
return result;
372+
if (mvResult != null) {
373+
return mvResult;
374+
}
375+
else if (svResult != null) {
376+
return svResult;
377+
}
378+
else {
379+
throw new IllegalStateException();
380+
}
355381
}
356382

357383
@Override
358384
@SuppressWarnings("unchecked")
359-
public void write(MultiValueMap<String, ?> map, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
385+
public void write(Map<String, ?> map, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
360386
throws IOException, HttpMessageNotWritableException {
361387

362388
if (isMultipart(map, contentType)) {
363-
writeMultipart((MultiValueMap<String, Object>) map, contentType, outputMessage);
389+
writeMultipart((Map<String, Object>) map, contentType, outputMessage);
364390
}
365391
else {
366-
writeForm((MultiValueMap<String, Object>) map, contentType, outputMessage);
392+
writeForm((Map<String, Object>) map, contentType, outputMessage);
367393
}
368394
}
369395

370396

371-
private boolean isMultipart(MultiValueMap<String, ?> map, @Nullable MediaType contentType) {
397+
private boolean isMultipart(Map<String, ?> map, @Nullable MediaType contentType) {
372398
if (contentType != null) {
373399
return contentType.getType().equalsIgnoreCase("multipart");
374400
}
375-
for (List<?> values : map.values()) {
376-
for (Object value : values) {
401+
if (map instanceof MultiValueMap<?, ?> mvMap) {
402+
for (List<?> values : mvMap.values()) {
403+
for (Object value : values) {
404+
if (value != null && !(value instanceof String)) {
405+
return true;
406+
}
407+
}
408+
}
409+
}
410+
else {
411+
for (Object value : map.values()) {
377412
if (value != null && !(value instanceof String)) {
378413
return true;
379414
}
@@ -382,7 +417,7 @@ private boolean isMultipart(MultiValueMap<String, ?> map, @Nullable MediaType co
382417
return false;
383418
}
384419

385-
private void writeForm(MultiValueMap<String, Object> formData, @Nullable MediaType mediaType,
420+
private void writeForm(Map<String, Object> formData, @Nullable MediaType mediaType,
386421
HttpOutputMessage outputMessage) throws IOException {
387422

388423
mediaType = getFormContentType(mediaType);
@@ -430,30 +465,37 @@ protected MediaType getFormContentType(@Nullable MediaType contentType) {
430465
return contentType;
431466
}
432467

433-
protected String serializeForm(MultiValueMap<String, Object> formData, Charset charset) {
468+
protected String serializeForm(Map<String, Object> formData, Charset charset) {
434469
StringBuilder builder = new StringBuilder();
435-
formData.forEach((name, values) -> {
470+
formData.forEach((name, value) -> {
436471
if (name == null) {
437-
Assert.isTrue(CollectionUtils.isEmpty(values), () -> "Null name in form data: " + formData);
472+
Assert.isTrue(value == null || value instanceof List<?> values && values.isEmpty(),
473+
() -> "Null name in form data: " + formData);
438474
return;
439475
}
440-
values.forEach(value -> {
441-
if (builder.length() != 0) {
442-
builder.append('&');
443-
}
444-
builder.append(URLEncoder.encode(name, charset));
445-
if (value != null) {
446-
builder.append('=');
447-
builder.append(URLEncoder.encode(String.valueOf(value), charset));
448-
}
449-
});
476+
if (value instanceof List<?> values) {
477+
values.forEach(v -> serializeFormValue(name, value, charset, builder));
478+
}
479+
else {
480+
serializeFormValue(name, value, charset, builder);
481+
}
450482
});
451-
452483
return builder.toString();
453484
}
454485

486+
private static void serializeFormValue(String name, @Nullable Object value, Charset charset, StringBuilder builder) {
487+
if (!builder.isEmpty()) {
488+
builder.append('&');
489+
}
490+
builder.append(URLEncoder.encode(name, charset));
491+
if (value != null) {
492+
builder.append('=');
493+
builder.append(URLEncoder.encode(String.valueOf(value), charset));
494+
}
495+
}
496+
455497
private void writeMultipart(
456-
MultiValueMap<String, Object> parts, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
498+
Map<String, Object> parts, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
457499
throws IOException {
458500

459501
// If the supplied content type is null, fall back to multipart/form-data.
@@ -500,13 +542,23 @@ private boolean isFilenameCharsetSet() {
500542
return (this.multipartCharset != null);
501543
}
502544

503-
private void writeParts(OutputStream os, MultiValueMap<String, Object> parts, byte[] boundary) throws IOException {
504-
for (Map.Entry<String, List<Object>> entry : parts.entrySet()) {
545+
private void writeParts(OutputStream os, Map<String, Object> parts, byte[] boundary) throws IOException {
546+
for (Map.Entry<String, Object> entry : parts.entrySet()) {
505547
String name = entry.getKey();
506-
for (Object part : entry.getValue()) {
507-
if (part != null) {
548+
Object value = entry.getValue();
549+
if (value instanceof List<?> values) {
550+
for (Object part : values) {
551+
if (part != null) {
552+
writeBoundary(os, boundary);
553+
writePart(name, getHttpEntity(part), os);
554+
writeNewLine(os);
555+
}
556+
}
557+
}
558+
else {
559+
if (value != null) {
508560
writeBoundary(os, boundary);
509-
writePart(name, getHttpEntity(part), os);
561+
writePart(name, getHttpEntity(value), os);
510562
writeNewLine(os);
511563
}
512564
}

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

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2024 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.
@@ -94,6 +94,7 @@ protected void doFilterInternal(
9494
}
9595
}
9696

97+
@SuppressWarnings("unchecked")
9798
@Nullable
9899
private MultiValueMap<String, String> parseIfNecessary(HttpServletRequest request) throws IOException {
99100
if (!shouldParse(request)) {
@@ -106,7 +107,7 @@ public InputStream getBody() throws IOException {
106107
return request.getInputStream();
107108
}
108109
};
109-
return this.formConverter.read(null, inputMessage);
110+
return (MultiValueMap<String, String>) this.formConverter.read(null, inputMessage);
110111
}
111112

112113
private boolean shouldParse(HttpServletRequest request) {

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

+18-2
Original file line numberDiff line numberDiff line change
@@ -117,13 +117,14 @@ void addSupportedMediaTypes() {
117117
assertCanWrite(MULTIPART_RELATED);
118118
}
119119

120+
@SuppressWarnings("unchecked")
120121
@Test
121-
void readForm() throws Exception {
122+
void readMultiValueForm() throws Exception {
122123
String body = "name+1=value+1&name+2=value+2%2B1&name+2=value+2%2B2&name+3";
123124
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.ISO_8859_1));
124125
inputMessage.getHeaders().setContentType(
125126
new MediaType("application", "x-www-form-urlencoded", StandardCharsets.ISO_8859_1));
126-
MultiValueMap<String, String> result = this.converter.read(null, inputMessage);
127+
MultiValueMap<String, String> result = (MultiValueMap<String, String>) this.converter.read(null, inputMessage);
127128

128129
assertThat(result).as("Invalid result").hasSize(3);
129130
assertThat(result.getFirst("name 1")).as("Invalid result").isEqualTo("value 1");
@@ -132,6 +133,21 @@ void readForm() throws Exception {
132133
assertThat(result.getFirst("name 3")).as("Invalid result").isNull();
133134
}
134135

136+
@SuppressWarnings("unchecked")
137+
@Test
138+
void readSingleValueForm() throws Exception {
139+
String body = "name+1=value+1&name+2=value+2&name+3";
140+
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.ISO_8859_1));
141+
inputMessage.getHeaders().setContentType(
142+
new MediaType("application", "x-www-form-urlencoded", StandardCharsets.ISO_8859_1));
143+
Map<String, String> result = (Map<String, String>) this.converter.read((Class<? extends Map<String, ?>>) Map.class, inputMessage);
144+
145+
assertThat(result).as("Invalid result").hasSize(3);
146+
assertThat(result.get("name 1")).as("Invalid result").isEqualTo("value 1");
147+
assertThat(result.get("name 2")).as("Invalid result").isEqualTo("value 2");
148+
assertThat(result.get("name 3")).as("Invalid result").isNull();
149+
}
150+
135151
@Test
136152
void writeForm() throws IOException {
137153
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();

0 commit comments

Comments
 (0)