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 e61d50f0d6..94ac48217b 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 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. @@ -31,13 +31,17 @@ import jakarta.servlet.http.HttpServletRequestWrapper; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.Part; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; -import org.springframework.http.MediaType; import org.springframework.mock.web.MockMultipartFile; import org.springframework.mock.web.MockPart; import org.springframework.stereotype.Controller; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMultipartHttpServletRequestBuilder; import org.springframework.ui.Model; +import org.springframework.util.StreamUtils; +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; @@ -55,19 +59,24 @@ import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standal /** * @author Rossen Stoyanchev * @author Juergen Hoeller + * @author Jaebin Joo */ public class MultipartControllerTests { - @Test - public void multipartRequestWithSingleFile() throws Exception { + @ParameterizedTest + @ValueSource(strings = {"/multipartfile", "/part"}) + public void multipartRequestWithSingleFileOrPart(String url) throws Exception { byte[] fileContent = "bar".getBytes(StandardCharsets.UTF_8); - MockMultipartFile filePart = new MockMultipartFile("file", "orig", null, fileContent); byte[] json = "{\"name\":\"yeeeah\"}".getBytes(StandardCharsets.UTF_8); MockMultipartFile jsonPart = new MockMultipartFile("json", "json", "application/json", json); + MockMultipartHttpServletRequestBuilder requestBuilder = (url.endsWith("file") ? + multipart(url).file(new MockMultipartFile("file", "orig", null, fileContent)) : + multipart(url).part(new MockPart("part", "orig", fileContent))); + standaloneSetup(new MultipartController()).build() - .perform(multipart("/multipartfile").file(filePart).file(jsonPart)) + .perform(requestBuilder.file(jsonPart)) .andExpect(status().isFound()) .andExpect(model().attribute("fileContent", fileContent)) .andExpect(model().attribute("jsonContent", Collections.singletonMap("name", "yeeeah"))); @@ -224,19 +233,14 @@ public class MultipartControllerTests { } @Test - public void multipartRequestWithServletParts() throws Exception { + public void multipartRequestWithDataBindingToFile() 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("/multipartfile").part(filePart).part(jsonPart)) + .perform(multipart("/multipartfilebinding").part(filePart)) .andExpect(status().isFound()) - .andExpect(model().attribute("fileContent", fileContent)) - .andExpect(model().attribute("jsonContent", Collections.singletonMap("name", "yeeeah"))); + .andExpect(model().attribute("fileContent", fileContent)); } @Test // SPR-13317 @@ -342,10 +346,13 @@ public class MultipartControllerTests { } @RequestMapping(value = "/part", method = RequestMethod.POST) - public String processPart(@RequestParam Part part, + public String processPart(@RequestPart Part part, @RequestPart Map json, Model model) throws IOException { - model.addAttribute("fileContent", part.getInputStream()); + if (part != null) { + byte[] content = StreamUtils.copyToByteArray(part.getInputStream()); + model.addAttribute("fileContent", content); + } model.addAttribute("jsonContent", json); return "redirect:/index"; @@ -356,6 +363,32 @@ public class MultipartControllerTests { model.addAttribute("json", json); return "redirect:/index"; } + + @RequestMapping(value = "/multipartfilebinding", 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"; + } + } + + private static class MultipartFileBean { + + private MultipartFile file; + + public MultipartFile getFile() { + return file; + } + + public void setFile(MultipartFile file) { + this.file = file; + } } 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 4f77717775..3ff10bd718 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,13 @@ public class ServletRequestDataBinder extends WebDataBinder { * 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; - * all metadata like original file name, content type, etc are lost in those cases. + * byte[], or String. Servlet Part binding is also supported when the + * request has not been parsed to MultipartRequest via MultipartResolver. * @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 2c1da31e1a..98ba841a27 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 @@ -107,9 +107,9 @@ public class WebRequestDataBinder extends WebDataBinder { *

Multipart files are bound via their parameter name, just like normal * 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; - * all metadata like original file name, content type, etc are lost in those cases. + *

The type of the target property for a multipart file can be MultipartFile, + * byte[], or String. Servlet Part binding is also supported when the + * request has not been parsed to MultipartRequest via MultipartResolver. * @param request the request with parameters to bind (can be multipart) * @see org.springframework.web.multipart.MultipartRequest * @see org.springframework.web.multipart.MultipartFile diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java index 094c8a7287..7f970f670b 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 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. @@ -16,6 +16,7 @@ package org.springframework.web.reactive.result.method.annotation; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.function.Function; @@ -213,21 +214,30 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Application InvocableHandlerMethod invocable = this.methodResolver.getExceptionHandlerMethod(exception, handlerMethod); if (invocable != null) { + ArrayList exceptions = new ArrayList<>(); try { if (logger.isDebugEnabled()) { logger.debug(exchange.getLogPrefix() + "Using @ExceptionHandler " + invocable); } bindingContext.getModel().asMap().clear(); - Throwable cause = exception.getCause(); - if (cause != null) { - return invocable.invoke(exchange, bindingContext, exception, cause, handlerMethod); - } - else { - return invocable.invoke(exchange, bindingContext, exception, handlerMethod); + + // Expose causes as provided arguments as well + Throwable exToExpose = exception; + while (exToExpose != null) { + exceptions.add(exToExpose); + Throwable cause = exToExpose.getCause(); + exToExpose = (cause != exToExpose ? cause : null); } + Object[] arguments = new Object[exceptions.size() + 1]; + exceptions.toArray(arguments); // efficient arraycopy call in ArrayList + arguments[arguments.length - 1] = handlerMethod; + + return invocable.invoke(exchange, bindingContext, arguments); } catch (Throwable invocationEx) { - if (logger.isWarnEnabled()) { + // Any other than the original exception (or a cause) is unintended here, + // probably an accident (e.g. failed assertion or the like). + if (!exceptions.contains(invocationEx) && logger.isWarnEnabled()) { logger.warn(exchange.getLogPrefix() + "Failure in @ExceptionHandler " + invocable, invocationEx); } } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ControllerAdviceTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ControllerAdviceTests.java index ec0e73ee02..d506d2af4b 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ControllerAdviceTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ControllerAdviceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 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. @@ -82,9 +82,10 @@ public class ControllerAdviceTests { @Test public void resolveExceptionWithAssertionErrorAsRootCause() throws Exception { - AssertionError cause = new AssertionError("argh"); - FatalBeanException exception = new FatalBeanException("wrapped", cause); - testException(exception, cause.toString()); + AssertionError rootCause = new AssertionError("argh"); + FatalBeanException cause = new FatalBeanException("wrapped", rootCause); + Exception exception = new Exception(cause); + testException(exception, rootCause.toString()); } private void testException(Throwable exception, String expected) throws Exception { diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingExceptionHandlingIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingExceptionHandlingIntegrationTests.java index c6039b06c8..a090f3c07f 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingExceptionHandlingIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingExceptionHandlingIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 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. @@ -139,7 +139,7 @@ class RequestMappingExceptionHandlingIntegrationTests extends AbstractRequestMap @GetMapping("/thrown-exception-with-cause-to-handle") public Publisher handleAndThrowExceptionWithCauseToHandle() { - throw new RuntimeException("State", new IOException("IO")); + throw new RuntimeException("State1", new RuntimeException("State2", new IOException("IO"))); } @GetMapping(path = "/mono-error")