Support Map in FormHttpMessageConverter
This commit changes the FormHttpMessageConverter from a HttpMessageConverter<MultiValueMap<String, ?>> to a HttpMessageConverter<Map<String, ?>>, so that normal, single-value maps can also be used as form representation, both for reading and writing. Closes gh-32826
This commit is contained in:
@@ -174,12 +174,13 @@ public class ContentRequestMatchers {
|
|||||||
return formData(multiValueMap, false);
|
return formData(multiValueMap, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
private RequestMatcher formData(MultiValueMap<String, String> expectedMap, boolean containsExactly) {
|
private RequestMatcher formData(MultiValueMap<String, String> expectedMap, boolean containsExactly) {
|
||||||
return request -> {
|
return request -> {
|
||||||
MockClientHttpRequest mockRequest = (MockClientHttpRequest) request;
|
MockClientHttpRequest mockRequest = (MockClientHttpRequest) request;
|
||||||
MockHttpInputMessage message = new MockHttpInputMessage(mockRequest.getBodyAsBytes());
|
MockHttpInputMessage message = new MockHttpInputMessage(mockRequest.getBodyAsBytes());
|
||||||
message.getHeaders().putAll(mockRequest.getHeaders());
|
message.getHeaders().putAll(mockRequest.getHeaders());
|
||||||
MultiValueMap<String, String> actualMap = new FormHttpMessageConverter().read(null, message);
|
MultiValueMap<String, String> actualMap = (MultiValueMap<String, String>) new FormHttpMessageConverter().read(null, message);
|
||||||
if (containsExactly) {
|
if (containsExactly) {
|
||||||
assertEquals("Form data", expectedMap, actualMap);
|
assertEquals("Form data", expectedMap, actualMap);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -899,6 +899,7 @@ public class MockHttpServletRequestBuilder
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
private MultiValueMap<String, String> parseFormData(MediaType mediaType) {
|
private MultiValueMap<String, String> parseFormData(MediaType mediaType) {
|
||||||
HttpInputMessage message = new HttpInputMessage() {
|
HttpInputMessage message = new HttpInputMessage() {
|
||||||
@Override
|
@Override
|
||||||
@@ -914,7 +915,7 @@ public class MockHttpServletRequestBuilder
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return new FormHttpMessageConverter().read(null, message);
|
return (MultiValueMap<String, String>) new FormHttpMessageConverter().read(null, message);
|
||||||
}
|
}
|
||||||
catch (IOException ex) {
|
catch (IOException ex) {
|
||||||
throw new IllegalStateException("Failed to parse form data in request body", ex);
|
throw new IllegalStateException("Failed to parse form data in request body", ex);
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import java.util.Collections;
|
|||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
|
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.http.ContentDisposition;
|
import org.springframework.http.ContentDisposition;
|
||||||
@@ -52,9 +53,10 @@ import org.springframework.util.StringUtils;
|
|||||||
*
|
*
|
||||||
* <p>In other words, this converter can read and write the
|
* <p>In other words, this converter can read and write the
|
||||||
* {@code "application/x-www-form-urlencoded"} media type as
|
* {@code "application/x-www-form-urlencoded"} media type as
|
||||||
|
* {@code Map<String, String>} or as
|
||||||
* {@link MultiValueMap MultiValueMap<String, String>}, and it can also
|
* {@link MultiValueMap MultiValueMap<String, String>}, and it can also
|
||||||
* write (but not read) the {@code "multipart/form-data"} and
|
* write (but not read) the {@code "multipart/form-data"} and
|
||||||
* {@code "multipart/mixed"} media types as
|
* {@code "multipart/mixed"} media types as {@code Map<String, Object>} or as
|
||||||
* {@link MultiValueMap MultiValueMap<String, Object>}.
|
* {@link MultiValueMap MultiValueMap<String, Object>}.
|
||||||
*
|
*
|
||||||
* <h3>Multipart Data</h3>
|
* <h3>Multipart Data</h3>
|
||||||
@@ -81,7 +83,7 @@ import org.springframework.util.StringUtils;
|
|||||||
* {@code "multipart/form-data"} content type.
|
* {@code "multipart/form-data"} content type.
|
||||||
*
|
*
|
||||||
* <pre class="code">
|
* <pre class="code">
|
||||||
* RestTemplate restTemplate = new RestTemplate();
|
* RestClient restClient = RestClient.create();
|
||||||
* // AllEncompassingFormHttpMessageConverter is configured by default
|
* // AllEncompassingFormHttpMessageConverter is configured by default
|
||||||
*
|
*
|
||||||
* MultiValueMap<String, Object> form = new LinkedMultiValueMap<>();
|
* MultiValueMap<String, Object> form = new LinkedMultiValueMap<>();
|
||||||
@@ -90,7 +92,12 @@ import org.springframework.util.StringUtils;
|
|||||||
* form.add("field 2", "value 3");
|
* form.add("field 2", "value 3");
|
||||||
* form.add("field 3", 4); // non-String form values supported as of 5.1.4
|
* form.add("field 3", 4); // non-String form values supported as of 5.1.4
|
||||||
*
|
*
|
||||||
* restTemplate.postForLocation("https://example.com/myForm", form);</pre>
|
* ResponseEntity<Void> response = restClient.post()
|
||||||
|
* .uri("https://example.com/myForm")
|
||||||
|
* .contentType(MULTIPART_FORM_DATA)
|
||||||
|
* .body(form)
|
||||||
|
* .retrieve()
|
||||||
|
* .toBodilessEntity();</pre>
|
||||||
*
|
*
|
||||||
* <p>The following snippet shows how to do a file upload using the
|
* <p>The following snippet shows how to do a file upload using the
|
||||||
* {@code "multipart/form-data"} content type.
|
* {@code "multipart/form-data"} content type.
|
||||||
@@ -100,7 +107,12 @@ import org.springframework.util.StringUtils;
|
|||||||
* parts.add("field 1", "value 1");
|
* parts.add("field 1", "value 1");
|
||||||
* parts.add("file", new ClassPathResource("myFile.jpg"));
|
* parts.add("file", new ClassPathResource("myFile.jpg"));
|
||||||
*
|
*
|
||||||
* restTemplate.postForLocation("https://example.com/myFileUpload", parts);</pre>
|
* ResponseEntity<Void> response = restClient.post()
|
||||||
|
* .uri("https://example.com/myForm")
|
||||||
|
* .contentType(MULTIPART_FORM_DATA)
|
||||||
|
* .body(parts)
|
||||||
|
* .retrieve()
|
||||||
|
* .toBodilessEntity();</pre>
|
||||||
*
|
*
|
||||||
* <p>The following snippet shows how to do a file upload using the
|
* <p>The following snippet shows how to do a file upload using the
|
||||||
* {@code "multipart/mixed"} content type.
|
* {@code "multipart/mixed"} content type.
|
||||||
@@ -110,40 +122,45 @@ import org.springframework.util.StringUtils;
|
|||||||
* parts.add("field 1", "value 1");
|
* parts.add("field 1", "value 1");
|
||||||
* parts.add("file", new ClassPathResource("myFile.jpg"));
|
* parts.add("file", new ClassPathResource("myFile.jpg"));
|
||||||
*
|
*
|
||||||
* HttpHeaders requestHeaders = new HttpHeaders();
|
* ResponseEntity<Void> response = restClient.post()
|
||||||
* requestHeaders.setContentType(MediaType.MULTIPART_MIXED);
|
* .uri("https://example.com/myForm")
|
||||||
*
|
* .contentType(MULTIPART_MIXED)
|
||||||
* restTemplate.postForLocation("https://example.com/myFileUpload",
|
* .body(form)
|
||||||
* new HttpEntity<>(parts, requestHeaders));</pre>
|
* .retrieve()
|
||||||
|
* .toBodilessEntity();</pre>
|
||||||
*
|
*
|
||||||
* <p>The following snippet shows how to do a file upload using the
|
* <p>The following snippet shows how to do a file upload using the
|
||||||
* {@code "multipart/related"} content type.
|
* {@code "multipart/related"} content type.
|
||||||
*
|
*
|
||||||
* <pre class="code">
|
* <pre class="code">
|
||||||
* MediaType multipartRelated = new MediaType("multipart", "related");
|
* restClient = restClient.mutate()
|
||||||
*
|
* .messageConverters(l -> l.stream()
|
||||||
* restTemplate.getMessageConverters().stream()
|
* .filter(FormHttpMessageConverter.class::isInstance)
|
||||||
* .filter(FormHttpMessageConverter.class::isInstance)
|
|
||||||
* .map(FormHttpMessageConverter.class::cast)
|
* .map(FormHttpMessageConverter.class::cast)
|
||||||
* .findFirst()
|
* .findFirst()
|
||||||
* .orElseThrow(() -> new IllegalStateException("Failed to find FormHttpMessageConverter"))
|
* .orElseThrow(() -> new IllegalStateException("Failed to find FormHttpMessageConverter"))
|
||||||
* .addSupportedMediaTypes(multipartRelated);
|
* .addSupportedMediaTypes(MULTIPART_RELATED);
|
||||||
*
|
*
|
||||||
* MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
|
* MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
|
||||||
* parts.add("field 1", "value 1");
|
* parts.add("field 1", "value 1");
|
||||||
* parts.add("file", new ClassPathResource("myFile.jpg"));
|
* parts.add("file", new ClassPathResource("myFile.jpg"));
|
||||||
*
|
*
|
||||||
* HttpHeaders requestHeaders = new HttpHeaders();
|
* ResponseEntity<Void> response = restClient.post()
|
||||||
* requestHeaders.setContentType(multipartRelated);
|
* .uri("https://example.com/myForm")
|
||||||
*
|
* .contentType(MULTIPART_RELATED)
|
||||||
* restTemplate.postForLocation("https://example.com/myFileUpload",
|
* .body(form)
|
||||||
* new HttpEntity<>(parts, requestHeaders));</pre>
|
* .retrieve()
|
||||||
|
* .toBodilessEntity();</pre>
|
||||||
*
|
*
|
||||||
* <h3>Miscellaneous</h3>
|
* <h3>Miscellaneous</h3>
|
||||||
*
|
*
|
||||||
* <p>Some methods in this class were inspired by
|
* <p>Some methods in this class were inspired by
|
||||||
* {@code org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity}.
|
* {@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.
|
||||||
|
*
|
||||||
* @author Arjen Poutsma
|
* @author Arjen Poutsma
|
||||||
* @author Rossen Stoyanchev
|
* @author Rossen Stoyanchev
|
||||||
* @author Juergen Hoeller
|
* @author Juergen Hoeller
|
||||||
@@ -152,7 +169,7 @@ import org.springframework.util.StringUtils;
|
|||||||
* @see org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter
|
* @see org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter
|
||||||
* @see org.springframework.util.MultiValueMap
|
* @see org.springframework.util.MultiValueMap
|
||||||
*/
|
*/
|
||||||
public class FormHttpMessageConverter implements HttpMessageConverter<MultiValueMap<String, ?>> {
|
public class FormHttpMessageConverter implements HttpMessageConverter<Map<String, ?>> {
|
||||||
|
|
||||||
/** The default charset used by the converter. */
|
/** The default charset used by the converter. */
|
||||||
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
|
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
|
||||||
@@ -295,7 +312,7 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean canRead(Class<?> clazz, @Nullable MediaType mediaType) {
|
public boolean canRead(Class<?> clazz, @Nullable MediaType mediaType) {
|
||||||
if (!MultiValueMap.class.isAssignableFrom(clazz)) {
|
if (!Map.class.isAssignableFrom(clazz)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (mediaType == null) {
|
if (mediaType == null) {
|
||||||
@@ -315,7 +332,7 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) {
|
public boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) {
|
||||||
if (!MultiValueMap.class.isAssignableFrom(clazz)) {
|
if (!Map.class.isAssignableFrom(clazz)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (mediaType == null || MediaType.ALL.equals(mediaType)) {
|
if (mediaType == null || MediaType.ALL.equals(mediaType)) {
|
||||||
@@ -330,59 +347,75 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MultiValueMap<String, String> read(@Nullable Class<? extends MultiValueMap<String, ?>> clazz,
|
public Map<String, ?> read(@Nullable Class<? extends Map<String, ?>> clazz,
|
||||||
HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
|
HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
|
||||||
|
|
||||||
MediaType contentType = inputMessage.getHeaders().getContentType();
|
MediaType contentType = inputMessage.getHeaders().getContentType();
|
||||||
Charset charset = (contentType != null && contentType.getCharset() != null ?
|
Charset charset = (contentType != null && contentType.getCharset() != null ?
|
||||||
contentType.getCharset() : this.charset);
|
contentType.getCharset() : this.charset);
|
||||||
String body = StreamUtils.copyToString(inputMessage.getBody(), charset);
|
String body = StreamUtils.copyToString(inputMessage.getBody(), charset);
|
||||||
|
|
||||||
String[] pairs = StringUtils.tokenizeToStringArray(body, "&");
|
String[] pairs = StringUtils.tokenizeToStringArray(body, "&");
|
||||||
MultiValueMap<String, String> result = new LinkedMultiValueMap<>(pairs.length);
|
|
||||||
|
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) {
|
||||||
for (String pair : pairs) {
|
for (String pair : pairs) {
|
||||||
int idx = pair.indexOf('=');
|
int idx = pair.indexOf('=');
|
||||||
if (idx == -1) {
|
if (idx == -1) {
|
||||||
result.add(URLDecoder.decode(pair, charset), null);
|
addFunction.accept(URLDecoder.decode(pair, charset), null);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
String name = URLDecoder.decode(pair.substring(0, idx), charset);
|
String name = URLDecoder.decode(pair.substring(0, idx), charset);
|
||||||
String value = URLDecoder.decode(pair.substring(idx + 1), charset);
|
String value = URLDecoder.decode(pair.substring(idx + 1), charset);
|
||||||
result.add(name, value);
|
addFunction.accept(name, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public void write(MultiValueMap<String, ?> map, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
|
public void write(Map<String, ?> map, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
|
||||||
throws IOException, HttpMessageNotWritableException {
|
throws IOException, HttpMessageNotWritableException {
|
||||||
|
|
||||||
if (isMultipart(map, contentType)) {
|
if (isMultipart(map, contentType)) {
|
||||||
writeMultipart((MultiValueMap<String, Object>) map, contentType, outputMessage);
|
writeMultipart((Map<String, Object>) map, contentType, outputMessage);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
writeForm((MultiValueMap<String, Object>) map, contentType, outputMessage);
|
writeForm((Map<String, Object>) map, contentType, outputMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private boolean isMultipart(MultiValueMap<String, ?> map, @Nullable MediaType contentType) {
|
private boolean isMultipart(Map<String, ?> map, @Nullable MediaType contentType) {
|
||||||
if (contentType != null) {
|
if (contentType != null) {
|
||||||
return contentType.getType().equalsIgnoreCase("multipart");
|
return contentType.getType().equalsIgnoreCase("multipart");
|
||||||
}
|
}
|
||||||
for (List<?> values : map.values()) {
|
for (Object value : map.values()) {
|
||||||
for (Object value : values) {
|
if (value instanceof List<?> values) {
|
||||||
if (value != null && !(value instanceof String)) {
|
for (Object v : values) {
|
||||||
return true;
|
if (v != null && !(v instanceof String)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (value != null && !(value instanceof String)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void writeForm(MultiValueMap<String, Object> formData, @Nullable MediaType mediaType,
|
private void writeForm(Map<String, Object> formData, @Nullable MediaType mediaType,
|
||||||
HttpOutputMessage outputMessage) throws IOException {
|
HttpOutputMessage outputMessage) throws IOException {
|
||||||
|
|
||||||
mediaType = getFormContentType(mediaType);
|
mediaType = getFormContentType(mediaType);
|
||||||
@@ -430,30 +463,36 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
|
|||||||
return contentType;
|
return contentType;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected String serializeForm(MultiValueMap<String, Object> formData, Charset charset) {
|
protected String serializeForm(Map<String, Object> formData, Charset charset) {
|
||||||
StringBuilder builder = new StringBuilder();
|
StringBuilder builder = new StringBuilder();
|
||||||
formData.forEach((name, values) -> {
|
formData.forEach((name, value) -> {
|
||||||
|
if (value instanceof List<?> values) {
|
||||||
if (name == null) {
|
if (name == null) {
|
||||||
Assert.isTrue(CollectionUtils.isEmpty(values), () -> "Null name in form data: " + formData);
|
Assert.isTrue(CollectionUtils.isEmpty(values), () -> "Null name in form data: " + formData);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
values.forEach(value -> {
|
values.forEach(v -> appendFormValue(builder, name, v, charset));
|
||||||
if (builder.length() != 0) {
|
}
|
||||||
builder.append('&');
|
else {
|
||||||
}
|
appendFormValue(builder, name, value, charset);
|
||||||
builder.append(URLEncoder.encode(name, charset));
|
}
|
||||||
if (value != null) {
|
|
||||||
builder.append('=');
|
|
||||||
builder.append(URLEncoder.encode(String.valueOf(value), charset));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return builder.toString();
|
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(
|
private void writeMultipart(
|
||||||
MultiValueMap<String, Object> parts, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
|
Map<String, Object> parts, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
|
|
||||||
// If the supplied content type is null, fall back to multipart/form-data.
|
// If the supplied content type is null, fall back to multipart/form-data.
|
||||||
@@ -500,16 +539,24 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
|
|||||||
return (this.multipartCharset != null);
|
return (this.multipartCharset != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void writeParts(OutputStream os, MultiValueMap<String, Object> parts, byte[] boundary) throws IOException {
|
private void writeParts(OutputStream os, Map<String, Object> parts, byte[] boundary) throws IOException {
|
||||||
for (Map.Entry<String, List<Object>> entry : parts.entrySet()) {
|
for (Map.Entry<String, Object> entry : parts.entrySet()) {
|
||||||
String name = entry.getKey();
|
String name = entry.getKey();
|
||||||
for (Object part : entry.getValue()) {
|
Object value = entry.getValue();
|
||||||
if (part != null) {
|
if (value instanceof List<?> values) {
|
||||||
writeBoundary(os, boundary);
|
for (Object part : values) {
|
||||||
writePart(name, getHttpEntity(part), os);
|
if (part != null) {
|
||||||
writeNewLine(os);
|
writeBoundary(os, boundary);
|
||||||
|
writePart(name, getHttpEntity(part), os);
|
||||||
|
writeNewLine(os);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (value != null) {
|
||||||
|
writeBoundary(os, boundary);
|
||||||
|
writePart(name, getHttpEntity(value), os);
|
||||||
|
writeNewLine(os);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2002-2021 the original author or authors.
|
* Copyright 2002-2024 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -95,6 +95,7 @@ public class FormContentFilter extends OncePerRequestFilter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
private MultiValueMap<String, String> parseIfNecessary(HttpServletRequest request) throws IOException {
|
private MultiValueMap<String, String> parseIfNecessary(HttpServletRequest request) throws IOException {
|
||||||
if (!shouldParse(request)) {
|
if (!shouldParse(request)) {
|
||||||
return null;
|
return null;
|
||||||
@@ -106,7 +107,7 @@ public class FormContentFilter extends OncePerRequestFilter {
|
|||||||
return request.getInputStream();
|
return request.getInputStream();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return this.formConverter.read(null, inputMessage);
|
return (MultiValueMap<String, String>) this.formConverter.read(null, inputMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean shouldParse(HttpServletRequest request) {
|
private boolean shouldParse(HttpServletRequest request) {
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ class FormHttpMessageConverterTests {
|
|||||||
@Test
|
@Test
|
||||||
void canRead() {
|
void canRead() {
|
||||||
assertCanRead(MultiValueMap.class, null);
|
assertCanRead(MultiValueMap.class, null);
|
||||||
|
assertCanRead(Map.class, null);
|
||||||
assertCanRead(APPLICATION_FORM_URLENCODED);
|
assertCanRead(APPLICATION_FORM_URLENCODED);
|
||||||
|
|
||||||
assertCannotRead(String.class, null);
|
assertCannotRead(String.class, null);
|
||||||
@@ -118,12 +119,12 @@ class FormHttpMessageConverterTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void readForm() throws Exception {
|
void readFormMultiValue() throws Exception {
|
||||||
String body = "name+1=value+1&name+2=value+2%2B1&name+2=value+2%2B2&name+3";
|
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));
|
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.ISO_8859_1));
|
||||||
inputMessage.getHeaders().setContentType(
|
inputMessage.getHeaders().setContentType(
|
||||||
new MediaType("application", "x-www-form-urlencoded", StandardCharsets.ISO_8859_1));
|
new MediaType("application", "x-www-form-urlencoded", StandardCharsets.ISO_8859_1));
|
||||||
MultiValueMap<String, String> result = this.converter.read(null, inputMessage);
|
MultiValueMap<String, String> result = (MultiValueMap<String, String>) this.converter.read(null, inputMessage);
|
||||||
|
|
||||||
assertThat(result).as("Invalid result").hasSize(3);
|
assertThat(result).as("Invalid result").hasSize(3);
|
||||||
assertThat(result.getFirst("name 1")).as("Invalid result").isEqualTo("value 1");
|
assertThat(result.getFirst("name 1")).as("Invalid result").isEqualTo("value 1");
|
||||||
@@ -133,7 +134,25 @@ class FormHttpMessageConverterTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void writeForm() throws IOException {
|
@SuppressWarnings("rawtypes")
|
||||||
|
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 {
|
||||||
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
|
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
|
||||||
body.set("name 1", "value 1");
|
body.set("name 1", "value 1");
|
||||||
body.add("name 2", "value 2+1");
|
body.add("name 2", "value 2+1");
|
||||||
@@ -151,7 +170,24 @@ class FormHttpMessageConverterTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void writeMultipart() throws Exception {
|
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 {
|
||||||
|
|
||||||
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
|
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
|
||||||
parts.add("name 1", "value 1");
|
parts.add("name 1", "value 1");
|
||||||
@@ -228,6 +264,78 @@ class FormHttpMessageConverterTests {
|
|||||||
assertThat(item.getContentType()).isEqualTo("application/json");
|
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
|
@Test
|
||||||
void writeMultipartWithSourceHttpMessageConverter() throws Exception {
|
void writeMultipartWithSourceHttpMessageConverter() throws Exception {
|
||||||
|
|
||||||
@@ -401,8 +509,8 @@ class FormHttpMessageConverterTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void assertCanWrite(MediaType mediaType) {
|
private void assertCanWrite(MediaType mediaType) {
|
||||||
Class<?> clazz = MultiValueMap.class;
|
assertThat(this.converter.canWrite(MultiValueMap.class, mediaType)).as("MultiValueMap : " + mediaType).isTrue();
|
||||||
assertThat(this.converter.canWrite(clazz, mediaType)).as(clazz.getSimpleName() + " : " + mediaType).isTrue();
|
assertThat(this.converter.canWrite(Map.class, mediaType)).as("Map : " + mediaType).isTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertCannotWrite(MediaType mediaType) {
|
private void assertCannotWrite(MediaType mediaType) {
|
||||||
|
|||||||
Reference in New Issue
Block a user