diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/MultipartControllerTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/MultipartControllerTests.java index a00dd2774e3a..141f8072c156 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/MultipartControllerTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/MultipartControllerTests.java @@ -39,6 +39,9 @@ import org.springframework.stereotype.Controller; import org.springframework.test.web.servlet.MockMvc; import org.springframework.ui.Model; +import org.springframework.util.StreamUtils; +import org.springframework.validation.BindException; +import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; @@ -56,6 +59,7 @@ /** * @author Rossen Stoyanchev * @author Juergen Hoeller + * @author Jaebin Joo */ public class MultipartControllerTests { @@ -225,7 +229,7 @@ public void multipartRequestWithOptionalFileListNotPresent() throws Exception { } @Test - public void multipartRequestWithServletParts() throws Exception { + public void multipartRequestWithParts_resolvesMultipartFileArguments() throws Exception { byte[] fileContent = "bar".getBytes(StandardCharsets.UTF_8); MockPart filePart = new MockPart("file", "orig", fileContent); @@ -240,6 +244,50 @@ public void multipartRequestWithServletParts() throws Exception { .andExpect(model().attribute("jsonContent", Collections.singletonMap("name", "yeeeah"))); } + @Test + public void multipartRequestWithParts_resolvesPartArguments() throws Exception { + byte[] fileContent = "bar".getBytes(StandardCharsets.UTF_8); + MockPart filePart = new MockPart("file", "orig", fileContent); + + byte[] json = "{\"name\":\"yeeeah\"}".getBytes(StandardCharsets.UTF_8); + MockPart jsonPart = new MockPart("json", json); + jsonPart.getHeaders().setContentType(MediaType.APPLICATION_JSON); + + standaloneSetup(new MultipartController()).build() + .perform(multipart("/part").part(filePart).part(jsonPart)) + .andExpect(status().isFound()) + .andExpect(model().attribute("fileContent", fileContent)) + .andExpect(model().attribute("jsonContent", Collections.singletonMap("name", "yeeeah"))); + } + + @Test + public void multipartRequestWithParts_resolvesMultipartFileProperties() throws Exception { + byte[] fileContent = "bar".getBytes(StandardCharsets.UTF_8); + MockPart filePart = new MockPart("file", "orig", fileContent); + + standaloneSetup(new MultipartController()).build() + .perform(multipart("/multipartfileproperty").part(filePart)) + .andExpect(status().isFound()) + .andExpect(model().attribute("fileContent", fileContent)); + } + + @Test + public void multipartRequestWithParts_cannotResolvePartProperties() throws Exception { + byte[] fileContent = "bar".getBytes(StandardCharsets.UTF_8); + MockPart filePart = new MockPart("file", "orig", fileContent); + + Exception exception = standaloneSetup(new MultipartController()).build() + .perform(multipart("/partproperty").part(filePart)) + .andExpect(status().is4xxClientError()) + .andReturn() + .getResolvedException(); + + assertThat(exception).isNotNull(); + assertThat(exception).isInstanceOf(BindException.class); + assertThat(((BindException) exception).getFieldError("file")) + .as("MultipartRequest would not bind Part properties.").isNotNull(); + } + @Test // SPR-13317 public void multipartRequestWrapped() throws Exception { byte[] json = "{\"name\":\"yeeeah\"}".getBytes(StandardCharsets.UTF_8); @@ -343,10 +391,13 @@ public String processOptionalFileList(@RequestParam Optional } @RequestMapping(value = "/part", method = RequestMethod.POST) - public String processPart(@RequestParam Part part, + public String processPart(@RequestPart Part file, @RequestPart Map json, Model model) throws IOException { - model.addAttribute("fileContent", part.getInputStream()); + if (file != null) { + byte[] content = StreamUtils.copyToByteArray(file.getInputStream()); + model.addAttribute("fileContent", content); + } model.addAttribute("jsonContent", json); return "redirect:/index"; @@ -357,8 +408,60 @@ public String processMultipart(@RequestPart Map json, Model mode model.addAttribute("json", json); return "redirect:/index"; } + + @RequestMapping(value = "/multipartfileproperty", method = RequestMethod.POST) + public String processMultipartFileBean(MultipartFileBean multipartFileBean, Model model, BindingResult bindingResult) + throws IOException { + + if (!bindingResult.hasErrors()) { + MultipartFile file = multipartFileBean.getFile(); + if (file != null) { + model.addAttribute("fileContent", file.getBytes()); + } + } + return "redirect:/index"; + } + + @RequestMapping(value = "/partproperty", method = RequestMethod.POST) + public String processPartBean(PartBean partBean, Model model, BindingResult bindingResult) + throws IOException { + + if (!bindingResult.hasErrors()) { + Part file = partBean.getFile(); + if (file != null) { + byte[] content = StreamUtils.copyToByteArray(file.getInputStream()); + model.addAttribute("fileContent", content); + } + } + return "redirect:/index"; + } } + private static class MultipartFileBean { + + private MultipartFile file; + + public MultipartFile getFile() { + return file; + } + + public void setFile(MultipartFile file) { + this.file = file; + } + } + + private static class PartBean { + + private Part file; + + public Part getFile() { + return file; + } + + public void setFile(Part file) { + this.file = file; + } + } private static class RequestWrappingFilter extends OncePerRequestFilter { diff --git a/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java b/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java index 864e21843984..24c37ce3cbcf 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java +++ b/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java @@ -104,11 +104,14 @@ public ServletRequestDataBinder(@Nullable Object target, String objectName) { * HTTP parameters: i.e. "uploadedFile" to an "uploadedFile" bean property, * invoking a "setUploadedFile" setter method. *

The type of the target property for a multipart file can be MultipartFile, - * byte[], or String. The latter two receive the contents of the uploaded file; + * Part, byte[], or String. The Part binding is only supported when the request + * is not a MultipartRequest. The latter two receive the contents of the uploaded file; * all metadata like original file name, content type, etc are lost in those cases. * @param request the request with parameters to bind (can be multipart) * @see org.springframework.web.multipart.MultipartHttpServletRequest + * @see org.springframework.web.multipart.MultipartRequest * @see org.springframework.web.multipart.MultipartFile + * @see jakarta.servlet.http.Part * @see #bind(org.springframework.beans.PropertyValues) */ public void bind(ServletRequest request) { diff --git a/spring-web/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java b/spring-web/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java index 805667f69b87..ed0d1b305127 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java +++ b/spring-web/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java @@ -108,7 +108,8 @@ public WebRequestDataBinder(@Nullable Object target, String objectName) { * HTTP parameters: i.e. "uploadedFile" to an "uploadedFile" bean property, * invoking a "setUploadedFile" setter method. *

The type of the target property for a multipart file can be Part, MultipartFile, - * byte[], or String. The latter two receive the contents of the uploaded file; + * byte[], or String. The Part binding is only supported when the request + * is not a MultipartRequest. The latter two receive the contents of the uploaded file; * all metadata like original file name, content type, etc are lost in those cases. * @param request the request with parameters to bind (can be multipart) * @see org.springframework.web.multipart.MultipartRequest