diff --git a/spring-test/src/main/java/org/springframework/test/web/client/match/ContentRequestMatchers.java b/spring-test/src/main/java/org/springframework/test/web/client/match/ContentRequestMatchers.java index 36678088637a..de937e6051bd 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/match/ContentRequestMatchers.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/match/ContentRequestMatchers.java @@ -174,13 +174,12 @@ public RequestMatcher formDataContains(Map expected) { return formData(multiValueMap, false); } - @SuppressWarnings("unchecked") private RequestMatcher formData(MultiValueMap expectedMap, boolean containsExactly) { return request -> { MockClientHttpRequest mockRequest = (MockClientHttpRequest) request; MockHttpInputMessage message = new MockHttpInputMessage(mockRequest.getBodyAsBytes()); message.getHeaders().putAll(mockRequest.getHeaders()); - MultiValueMap actualMap = (MultiValueMap) new FormHttpMessageConverter().read(null, message); + MultiValueMap actualMap = new FormHttpMessageConverter().read(null, message); if (containsExactly) { assertEquals("Form data", expectedMap, actualMap); } diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/request/AbstractMockHttpServletRequestBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/request/AbstractMockHttpServletRequestBuilder.java index f8b0a54f3dc4..3adb4fa10b1b 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/request/AbstractMockHttpServletRequestBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/request/AbstractMockHttpServletRequestBuilder.java @@ -902,7 +902,7 @@ public HttpHeaders getHeaders() { }; try { - return (MultiValueMap) 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); diff --git a/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java index 93440fe9422c..4ebbd80b9950 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java @@ -50,24 +50,12 @@ * Implementation of {@link HttpMessageConverter} to read and write 'normal' HTML * forms and also to write (but not read) multipart data (e.g. file uploads). * - *

- * The following table shows an overview of the supported media and class types. - * - * - * - * - * - * - * - * - * - * - * - * - *
Media typeReadWrite
{@code "application/x-www-form-urlencoded"}{@link MultiValueMap MultiValueMap<String, String>}{@link Map Map<String, String>}
- * {@link MultiValueMap MultiValueMap<String, String>}
{@code "multipart/form-data"}
- * {@code "multipart/mixed"}
Unsupported{@link Map Map<String, Object>}
- * {@link MultiValueMap MultiValueMap<String, Object>}
+ *

In other words, this converter can read and write the + * {@code "application/x-www-form-urlencoded"} media type as + * {@link MultiValueMap MultiValueMap<String, String>}, and it can also + * write (but not read) the {@code "multipart/form-data"} and + * {@code "multipart/mixed"} media types as + * {@link MultiValueMap MultiValueMap<String, Object>}. * *

Multipart Data

* @@ -167,10 +155,6 @@ *

Some methods in this class were inspired by * {@code org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity}. * - *

As of 6.2, the {@code FormHttpMessageConverter} is parameterized over - * {@code Map} in order to support writing single-value maps. - * Before 6.2, this class was parameterized over {@code MultiValueMap}. - * * @author Arjen Poutsma * @author Rossen Stoyanchev * @author Juergen Hoeller @@ -179,7 +163,7 @@ * @see org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter * @see org.springframework.util.MultiValueMap */ -public class FormHttpMessageConverter implements HttpMessageConverter> { +public class FormHttpMessageConverter implements HttpMessageConverter> { /** The default charset used by the converter. */ public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; @@ -342,7 +326,7 @@ public boolean canRead(Class clazz, @Nullable MediaType mediaType) { @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 boolean canWrite(Class clazz, @Nullable MediaType mediaType) { } @Override - public Map read(@Nullable Class> clazz, + public MultiValueMap read(@Nullable Class> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { MediaType contentType = inputMessage.getHeaders().getContentType(); @@ -383,38 +367,33 @@ public boolean canWrite(Class clazz, @Nullable MediaType mediaType) { @Override @SuppressWarnings("unchecked") - public void write(Map map, @Nullable MediaType contentType, HttpOutputMessage outputMessage) + public void write(MultiValueMap map, @Nullable MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { if (isMultipart(map, contentType)) { - writeMultipart((Map) map, contentType, outputMessage); + writeMultipart((MultiValueMap) map, contentType, outputMessage); } else { - writeForm((Map) map, contentType, outputMessage); + writeForm((MultiValueMap) map, contentType, outputMessage); } } - private boolean isMultipart(Map map, @Nullable MediaType contentType) { + private boolean isMultipart(MultiValueMap 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 formData, @Nullable MediaType mediaType, + private void writeForm(MultiValueMap formData, @Nullable MediaType mediaType, HttpOutputMessage outputMessage) throws IOException { mediaType = getFormContentType(mediaType); @@ -462,36 +441,30 @@ protected MediaType getFormContentType(@Nullable MediaType contentType) { return contentType; } - protected String serializeForm(Map formData, Charset charset) { + protected String serializeForm(MultiValueMap 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)); - } + return builder.toString(); } private void writeMultipart( - Map parts, @Nullable MediaType contentType, HttpOutputMessage outputMessage) + MultiValueMap 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 @@ private boolean isFilenameCharsetSet() { return (this.multipartCharset != null); } - private void writeParts(OutputStream os, Map parts, byte[] boundary) throws IOException { - for (Map.Entry entry : parts.entrySet()) { + private void writeParts(OutputStream os, MultiValueMap parts, byte[] boundary) throws IOException { + for (Map.Entry> 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); - } } } diff --git a/spring-web/src/main/java/org/springframework/web/filter/FormContentFilter.java b/spring-web/src/main/java/org/springframework/web/filter/FormContentFilter.java index 5d028416bca3..bad214fb5d59 100644 --- a/spring-web/src/main/java/org/springframework/web/filter/FormContentFilter.java +++ b/spring-web/src/main/java/org/springframework/web/filter/FormContentFilter.java @@ -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 @@ protected void doFilterInternal( } @Nullable - @SuppressWarnings("unchecked") private MultiValueMap parseIfNecessary(HttpServletRequest request) throws IOException { if (!shouldParse(request)) { return null; @@ -107,7 +106,7 @@ public InputStream getBody() throws IOException { return request.getInputStream(); } }; - return (MultiValueMap) this.formConverter.read(null, inputMessage); + return this.formConverter.read(null, inputMessage); } private boolean shouldParse(HttpServletRequest request) { diff --git a/spring-web/src/test/java/org/springframework/http/converter/FormHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/FormHttpMessageConverterTests.java index 299ca939bf80..98c937bc7339 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/FormHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/FormHttpMessageConverterTests.java @@ -118,13 +118,12 @@ void addSupportedMediaTypes() { } @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 result = (MultiValueMap) this.converter.read(null, inputMessage); + MultiValueMap 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 @@ void writeForm() throws IOException { } @Test - void writeFormSingleValue() throws IOException { - Map 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 parts = new LinkedMultiValueMap<>(); parts.add("name 1", "value 1"); @@ -246,78 +228,6 @@ public String getFilename() { assertThat(item.getContentType()).isEqualTo("application/json"); } - @Test - void writeMultipartSingleValue() throws Exception { - - Map 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 entity = new HttpEntity<>(myBean, entityHeaders); - parts.put("json", entity); - - Map 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 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 @@ private void assertCannotRead(Class clazz, MediaType mediaType) { } 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) {