diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java index f7ba780479..9af71225ce 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java @@ -100,9 +100,8 @@ import java.util.concurrent.Callable; * converted to the declared method argument type using * {@linkplain org.springframework.http.converter.HttpMessageConverter message * converters}. Such parameters may optionally be annotated with {@code @Valid} - * but do not support access to validation results through a - * {@link org.springframework.validation.Errors} / - * {@link org.springframework.validation.BindingResult} argument. + * and also support access to validation results through an + * {@link org.springframework.validation.Errors} argument. * Instead a {@link org.springframework.web.servlet.mvc.method.annotation.MethodArgumentNotValidException} * exception is raised. *
  • {@link RequestPart @RequestPart} annotated parameters @@ -112,9 +111,8 @@ import java.util.concurrent.Callable; * converted to the declared method argument type using * {@linkplain org.springframework.http.converter.HttpMessageConverter message * converters}. Such parameters may optionally be annotated with {@code @Valid} - * but do not support access to validation results through a - * {@link org.springframework.validation.Errors} / - * {@link org.springframework.validation.BindingResult} argument. + * and support access to validation results through a + * {@link org.springframework.validation.Errors} argument. * Instead a {@link org.springframework.web.servlet.mvc.method.annotation.MethodArgumentNotValidException} * exception is raised. *
  • {@link org.springframework.http.HttpEntity HttpEntity<?>} parameters diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/ErrorsMethodArgumentResolver.java b/spring-web/src/main/java/org/springframework/web/method/annotation/ErrorsMethodArgumentResolver.java index 2caca5b17c..63e713685b 100644 --- a/spring-web/src/main/java/org/springframework/web/method/annotation/ErrorsMethodArgumentResolver.java +++ b/spring-web/src/main/java/org/springframework/web/method/annotation/ErrorsMethodArgumentResolver.java @@ -60,8 +60,8 @@ public class ErrorsMethodArgumentResolver implements HandlerMethodArgumentResolv } throw new IllegalStateException( - "An Errors/BindingResult argument is expected to be immediately after the model attribute " + - "argument in the controller method signature: " + parameter.getMethod()); + "An Errors/BindingResult argument is expected to be declared immediately after the model attribute, " + + "the @RequestBody or the @RequestPart arguments to which they apply: " + parameter.getMethod()); } } diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java b/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java index a14c85888f..d2e2529360 100644 --- a/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java +++ b/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java @@ -157,16 +157,16 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol if (annot.annotationType().getSimpleName().startsWith("Valid")) { Object hints = AnnotationUtils.getValue(annot); binder.validate(hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); + break; } } } /** - * Whether to raise a {@link BindException} on bind or validation errors. - * The default implementation returns {@code true} if the next method - * argument is not of type {@link Errors}. + * Whether to raise a {@link BindException} on validation errors. * @param binder the data binder used to perform data binding * @param parameter the method argument + * @return {@code true} if the next method argument is not of type {@link Errors}. */ protected boolean isBindExceptionRequired(WebDataBinder binder, MethodParameter parameter) { int i = parameter.getParameterIndex(); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java index ef2b4d96d9..3450cf6f93 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java @@ -38,8 +38,8 @@ import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.HandlerMethodArgumentResolver; /** - * A base class for resolving method argument values by reading from the body of a request - * with {@link HttpMessageConverter}s. + * A base class for resolving method argument values by reading from the body of + * a request with {@link HttpMessageConverter}s. * * @author Arjen Poutsma * @author Rossen Stoyanchev @@ -48,9 +48,9 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver; public abstract class AbstractMessageConverterMethodArgumentResolver implements HandlerMethodArgumentResolver { protected final Log logger = LogFactory.getLog(getClass()); - + protected final List> messageConverters; - + protected final List allSupportedMediaTypes; public AbstractMessageConverterMethodArgumentResolver(List> messageConverters) { @@ -60,8 +60,8 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements } /** - * Returns the media types supported by all provided message converters preserving their ordering and - * further sorting by specificity via {@link MediaType#sortBySpecificity(List)}. + * Return the media types supported by all provided message converters sorted + * by specificity via {@link MediaType#sortBySpecificity(List)}. */ private static List getAllSupportedMediaTypes(List> messageConverters) { Set allSupportedMediaTypes = new LinkedHashSet(); @@ -72,11 +72,12 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements MediaType.sortBySpecificity(result); return Collections.unmodifiableList(result); } - + /** - * Creates the method argument value of the expected parameter type by reading from the given request. - * - * @param the expected type of the argument value to be created + * Creates the method argument value of the expected parameter type by + * reading from the given request. + * + * @param the expected type of the argument value to be created * @param webRequest the current request * @param methodParam the method argument * @param paramType the type of the argument value to be created @@ -86,15 +87,16 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements */ protected Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter methodParam, Class paramType) throws IOException, HttpMediaTypeNotSupportedException { - - HttpInputMessage inputMessage = createInputMessage(webRequest); - return readWithMessageConverters(inputMessage, methodParam, paramType); - } + + HttpInputMessage inputMessage = createInputMessage(webRequest); + return readWithMessageConverters(inputMessage, methodParam, paramType); + } /** - * Creates the method argument value of the expected parameter type by reading from the given HttpInputMessage. - * - * @param the expected type of the argument value to be created + * Creates the method argument value of the expected parameter type by reading + * from the given HttpInputMessage. + * + * @param the expected type of the argument value to be created * @param inputMessage the HTTP input message representing the current request * @param methodParam the method argument * @param paramType the type of the argument value to be created @@ -103,14 +105,14 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements * @throws HttpMediaTypeNotSupportedException if no suitable message converter is found */ @SuppressWarnings("unchecked") - protected Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter methodParam, Class paramType) throws IOException, - HttpMediaTypeNotSupportedException { - + protected Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter methodParam, + Class paramType) throws IOException, HttpMediaTypeNotSupportedException { + MediaType contentType = inputMessage.getHeaders().getContentType(); if (contentType == null) { contentType = MediaType.APPLICATION_OCTET_STREAM; } - + for (HttpMessageConverter messageConverter : this.messageConverters) { if (messageConverter.canRead(paramType, contentType)) { if (logger.isDebugEnabled()) { @@ -120,7 +122,7 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements return ((HttpMessageConverter) messageConverter).read(paramType, inputMessage); } } - + throw new HttpMediaTypeNotSupportedException(contentType, allSupportedMediaTypes); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartMethodArgumentResolver.java index f517cc0350..cea6c55783 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartMethodArgumentResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartMethodArgumentResolver.java @@ -28,6 +28,7 @@ import org.springframework.http.HttpInputMessage; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.util.Assert; import org.springframework.validation.BindingResult; +import org.springframework.validation.Errors; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.RequestBody; @@ -49,26 +50,26 @@ import org.springframework.web.util.WebUtils; * Resolves the following method arguments: *
      *
    • Annotated with {@code @RequestPart} - *
    • Of type {@link MultipartFile} in conjunction with Spring's + *
    • Of type {@link MultipartFile} in conjunction with Spring's * {@link MultipartResolver} abstraction - *
    • Of type {@code javax.servlet.http.Part} in conjunction with + *
    • Of type {@code javax.servlet.http.Part} in conjunction with * Servlet 3.0 multipart requests *
    - * - *

    When a parameter is annotated with {@code @RequestPart} the content of the - * part is passed through an {@link HttpMessageConverter} to resolve the method - * argument with the 'Content-Type' of the request part in mind. This is + * + *

    When a parameter is annotated with {@code @RequestPart} the content of the + * part is passed through an {@link HttpMessageConverter} to resolve the method + * argument with the 'Content-Type' of the request part in mind. This is * analogous to what @{@link RequestBody} does to resolve an argument based on * the content of a regular request. - * - *

    When a parameter is not annotated or the name of the part is not specified, + * + *

    When a parameter is not annotated or the name of the part is not specified, * it is derived from the name of the method argument. - * - *

    Automatic validation may be applied if the argument is annotated with - * {@code @javax.validation.Valid}. In case of validation failure, a + * + *

    Automatic validation may be applied if the argument is annotated with + * {@code @javax.validation.Valid}. In case of validation failure, a * {@link MethodArgumentNotValidException} is raised and a 400 response status * code returned if {@link DefaultHandlerExceptionResolver} is configured. - * + * * @author Rossen Stoyanchev * @since 3.1 */ @@ -111,8 +112,8 @@ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterM HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class); assertIsMultipartRequest(servletRequest); - - MultipartHttpServletRequest multipartRequest = + + MultipartHttpServletRequest multipartRequest = WebUtils.getNativeRequest(servletRequest, MultipartHttpServletRequest.class); String partName = getPartName(parameter); @@ -133,20 +134,11 @@ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterM try { HttpInputMessage inputMessage = new RequestPartServletServerHttpRequest(servletRequest, partName); arg = readWithMessageConverters(inputMessage, parameter, parameter.getParameterType()); + WebDataBinder binder = binderFactory.createBinder(request, arg, partName); if (arg != null) { - Annotation[] annotations = parameter.getParameterAnnotations(); - for (Annotation annot : annotations) { - if (annot.annotationType().getSimpleName().startsWith("Valid")) { - WebDataBinder binder = binderFactory.createBinder(request, arg, partName); - Object hints = AnnotationUtils.getValue(annot); - binder.validate(hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); - BindingResult bindingResult = binder.getBindingResult(); - if (bindingResult.hasErrors()) { - throw new MethodArgumentNotValidException(parameter, bindingResult); - } - } - } + validate(binder, parameter); } + mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + partName, binder.getBindingResult()); } catch (MissingServletRequestPartException ex) { // handled below @@ -160,7 +152,7 @@ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterM if (arg == null && isRequired) { throw new MissingServletRequestPartException(partName); } - + return arg; } @@ -170,7 +162,7 @@ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterM throw new MultipartException("The current request is not a multipart request"); } } - + private String getPartName(MethodParameter parameter) { RequestPart annot = parameter.getParameterAnnotation(RequestPart.class); String partName = (annot != null) ? annot.value() : ""; @@ -181,7 +173,7 @@ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterM } return partName; } - + private boolean isMultipartFileCollection(MethodParameter parameter) { Class paramType = parameter.getParameterType(); if (Collection.class.equals(paramType) || List.class.isAssignableFrom(paramType)){ @@ -193,4 +185,35 @@ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterM return false; } + private void validate(WebDataBinder binder, MethodParameter parameter) throws MethodArgumentNotValidException { + + Annotation[] annotations = parameter.getParameterAnnotations(); + for (Annotation annot : annotations) { + if (annot.annotationType().getSimpleName().startsWith("Valid")) { + Object hints = AnnotationUtils.getValue(annot); + binder.validate(hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); + BindingResult bindingResult = binder.getBindingResult(); + if (bindingResult.hasErrors()) { + if (isBindExceptionRequired(binder, parameter)) { + throw new MethodArgumentNotValidException(parameter, bindingResult); + } + } + } + } + } + + /** + * Whether to raise a {@link MethodArgumentNotValidException} on validation errors. + * @param binder the data binder used to perform data binding + * @param parameter the method argument + * @return {@code true} if the next method argument is not of type {@link Errors}. + */ + private boolean isBindExceptionRequired(WebDataBinder binder, MethodParameter parameter) { + int i = parameter.getParameterIndex(); + Class[] paramTypes = parameter.getMethod().getParameterTypes(); + boolean hasBindingResult = (paramTypes.length > (i + 1) && Errors.class.isAssignableFrom(paramTypes[i + 1])); + + return !hasBindingResult; + } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java index bfaf4aa86a..06e321ad10 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java @@ -27,6 +27,7 @@ import org.springframework.http.HttpInputMessage; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.validation.BindingResult; +import org.springframework.validation.Errors; import org.springframework.web.HttpMediaTypeNotAcceptableException; import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.accept.ContentNegotiationManager; @@ -85,33 +86,52 @@ public class RequestResponseBodyMethodProcessor extends AbstractMessageConverter public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { - Object arg = readWithMessageConverters(webRequest, parameter, parameter.getParameterType()); - validate(parameter, webRequest, binderFactory, arg); - return arg; + Object argument = readWithMessageConverters(webRequest, parameter, parameter.getParameterType()); + + String name = Conventions.getVariableNameForParameter(parameter); + WebDataBinder binder = binderFactory.createBinder(webRequest, argument, name); + + if (argument != null) { + validate(binder, parameter); + } + + mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult()); + + return argument; } - private void validate(MethodParameter parameter, NativeWebRequest webRequest, - WebDataBinderFactory binderFactory, Object arg) throws Exception, MethodArgumentNotValidException { + private void validate(WebDataBinder binder, MethodParameter parameter) throws Exception, MethodArgumentNotValidException { - if (arg == null) { - return; - } Annotation[] annotations = parameter.getParameterAnnotations(); for (Annotation annot : annotations) { - if (!annot.annotationType().getSimpleName().startsWith("Valid")) { - continue; - } - String name = Conventions.getVariableNameForParameter(parameter); - WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name); - Object hints = AnnotationUtils.getValue(annot); - binder.validate(hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); - BindingResult bindingResult = binder.getBindingResult(); - if (bindingResult.hasErrors()) { - throw new MethodArgumentNotValidException(parameter, bindingResult); + if (annot.annotationType().getSimpleName().startsWith("Valid")) { + Object hints = AnnotationUtils.getValue(annot); + binder.validate(hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); + BindingResult bindingResult = binder.getBindingResult(); + if (bindingResult.hasErrors()) { + if (isBindExceptionRequired(binder, parameter)) { + throw new MethodArgumentNotValidException(parameter, bindingResult); + } + } + break; } } } + /** + * Whether to raise a {@link MethodArgumentNotValidException} on validation errors. + * @param binder the data binder used to perform data binding + * @param parameter the method argument + * @return {@code true} if the next method argument is not of type {@link Errors}. + */ + private boolean isBindExceptionRequired(WebDataBinder binder, MethodParameter parameter) { + int i = parameter.getParameterIndex(); + Class[] paramTypes = parameter.getMethod().getParameterTypes(); + boolean hasBindingResult = (paramTypes.length > (i + 1) && Errors.class.isAssignableFrom(paramTypes[i + 1])); + + return !hasBindingResult; + } + @Override protected Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter methodParam, Class paramType) throws IOException, HttpMediaTypeNotSupportedException { diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterIntegrationTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterIntegrationTests.java index 8531f97fd2..e75dafbb02 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterIntegrationTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterIntegrationTests.java @@ -89,29 +89,29 @@ import org.springframework.web.util.UriComponentsBuilder; /** * A test fixture with a controller with all supported method signature styles - * and arguments. A convenient place to test or confirm a problem with a - * specific argument or return value type. + * and arguments. A convenient place to test or confirm a problem with a + * specific argument or return value type. * * @author Rossen Stoyanchev - * + * * @see HandlerMethodAnnotationDetectionTests * @see ServletAnnotationControllerHandlerMethodTests */ public class RequestMappingHandlerAdapterIntegrationTests { private final Object handler = new Handler(); - + private RequestMappingHandlerAdapter handlerAdapter; - + private MockHttpServletRequest request; - + private MockHttpServletResponse response; @Before public void setup() throws Exception { ConfigurableWebBindingInitializer bindingInitializer = new ConfigurableWebBindingInitializer(); bindingInitializer.setValidator(new StubValidator()); - + List customResolvers = new ArrayList(); customResolvers.add(new ServletWebArgumentResolverAdapter(new ColorArgumentResolver())); @@ -127,11 +127,11 @@ public class RequestMappingHandlerAdapterIntegrationTests { request = new MockHttpServletRequest(); response = new MockHttpServletResponse(); - + // Expose request to the current thread (for SpEL expressions) RequestContextHolder.setRequestAttributes(new ServletWebRequest(request)); } - + @After public void teardown() { RequestContextHolder.resetRequestAttributes(); @@ -139,7 +139,7 @@ public class RequestMappingHandlerAdapterIntegrationTests { @Test public void handle() throws Exception { - + Class[] parameterTypes = new Class[] { int.class, String.class, String.class, String.class, Map.class, Date.class, Map.class, String.class, String.class, TestBean.class, Errors.class, TestBean.class, Color.class, HttpServletRequest.class, HttpServletResponse.class, User.class, OtherUser.class, @@ -186,7 +186,7 @@ public class RequestMappingHandlerAdapterIntegrationTests { assertEquals("paramByConventionValue", map.get("paramByConvention")); assertEquals("/contextPath", model.get("value")); - + TestBean modelAttr = (TestBean) model.get("modelAttr"); assertEquals(25, modelAttr.getAge()); assertEquals("Set by model method [modelAttr]", modelAttr.getName()); @@ -208,13 +208,13 @@ public class RequestMappingHandlerAdapterIntegrationTests { assertTrue(model.get("customArg") instanceof Color); assertEquals(User.class, model.get("user").getClass()); assertEquals(OtherUser.class, model.get("otherUser").getClass()); - + assertEquals(new URI("http://localhost/contextPath/main/path"), model.get("url")); } - + @Test public void handleRequestBody() throws Exception { - + Class[] parameterTypes = new Class[] { byte[].class }; request.addHeader("Content-Type", "text/plain; charset=utf-8"); @@ -229,6 +229,23 @@ public class RequestMappingHandlerAdapterIntegrationTests { assertEquals(HttpStatus.ACCEPTED.value(), response.getStatus()); } + @Test + public void handleAndValidateRequestBody() throws Exception { + + Class[] parameterTypes = new Class[] { TestBean.class, Errors.class }; + + request.addHeader("Content-Type", "text/plain; charset=utf-8"); + request.setContent("Hello Server".getBytes("UTF-8")); + + HandlerMethod handlerMethod = handlerMethod("handleAndValidateRequestBody", parameterTypes); + + ModelAndView mav = handlerAdapter.handle(request, response, handlerMethod); + + assertNull(mav); + assertEquals("Error count [1]", new String(response.getContentAsByteArray(), "UTF-8")); + assertEquals(HttpStatus.ACCEPTED.value(), response.getStatus()); + } + @Test public void handleHttpEntity() throws Exception { @@ -254,11 +271,23 @@ public class RequestMappingHandlerAdapterIntegrationTests { HandlerMethod handlerMethod = handlerMethod("handleRequestPart", String.class, Model.class); ModelAndView mav = handlerAdapter.handle(multipartRequest, response, handlerMethod); - + assertNotNull(mav); assertEquals("content", mav.getModelMap().get("requestPart")); } - + + @Test + public void handleAndValidateRequestPart() throws Exception { + MockMultipartHttpServletRequest multipartRequest = new MockMultipartHttpServletRequest(); + multipartRequest.addFile(new MockMultipartFile("requestPart", "", "text/plain", "content".getBytes("UTF-8"))); + + HandlerMethod handlerMethod = handlerMethod("handleAndValidateRequestPart", String.class, Errors.class, Model.class); + ModelAndView mav = handlerAdapter.handle(multipartRequest, response, handlerMethod); + + assertNotNull(mav); + assertEquals(1, mav.getModelMap().get("error count")); + } + @Test public void handleAndCompleteSession() throws Exception { HandlerMethod handlerMethod = handlerMethod("handleAndCompleteSession", SessionStatus.class); @@ -266,7 +295,7 @@ public class RequestMappingHandlerAdapterIntegrationTests { assertFalse(request.getSession().getAttributeNames().hasMoreElements()); } - + private HandlerMethod handlerMethod(String methodName, Class... paramTypes) throws Exception { Method method = handler.getClass().getDeclaredMethod(methodName, paramTypes); return new InvocableHandlerMethod(handler, method); @@ -277,7 +306,7 @@ public class RequestMappingHandlerAdapterIntegrationTests { private static class Handler { @InitBinder("dateParam") - public void initBinder(WebDataBinder dataBinder, @RequestParam("datePattern") String datePattern) { + public void initBinder(WebDataBinder dataBinder, @RequestParam("datePattern") String datePattern) { SimpleDateFormat dateFormat = new SimpleDateFormat(datePattern); dataBinder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false)); } @@ -291,7 +320,7 @@ public class RequestMappingHandlerAdapterIntegrationTests { modelAttr = new TestBean(); modelAttr.setName("Set by model method [modelAttrByConvention]"); model.addAttribute(modelAttr); - + model.addAttribute(new OtherUser()); } @@ -322,13 +351,13 @@ public class RequestMappingHandlerAdapterIntegrationTests { .addAttribute("paramByConvention", paramByConvention).addAttribute("value", value) .addAttribute("customArg", customArg).addAttribute(user) .addAttribute("url", builder.path("/path").build().toUri()); - + assertNotNull(request); assertNotNull(response); return "viewName"; } - + @ResponseStatus(value=HttpStatus.ACCEPTED) @ResponseBody public String handleRequestBody(@RequestBody byte[] bytes) throws Exception { @@ -336,17 +365,29 @@ public class RequestMappingHandlerAdapterIntegrationTests { return "Handled requestBody=[" + requestBody + "]"; } + @ResponseStatus(value=HttpStatus.ACCEPTED) + @ResponseBody + public String handleAndValidateRequestBody(@Valid TestBean modelAttr, Errors errors) throws Exception { + return "Error count [" + errors.getErrorCount() + "]"; + } + public ResponseEntity handleHttpEntity(HttpEntity httpEntity) throws Exception { HttpHeaders responseHeaders = new HttpHeaders(); responseHeaders.set("header", "headerValue"); String responseBody = "Handled requestBody=[" + new String(httpEntity.getBody(), "UTF-8") + "]"; return new ResponseEntity(responseBody, responseHeaders, HttpStatus.ACCEPTED); } - + public void handleRequestPart(@RequestPart String requestPart, Model model) { model.addAttribute("requestPart", requestPart); } - + + public void handleAndValidateRequestPart(@RequestPart @Valid String requestPart, + Errors errors, Model model) throws Exception { + + model.addAttribute("error count", errors.getErrorCount()); + } + public void handleAndCompleteSession(SessionStatus sessionStatus) { sessionStatus.setComplete(); } @@ -367,7 +408,7 @@ public class RequestMappingHandlerAdapterIntegrationTests { return new Color(0); } } - + private static class User implements Principal { public String getName() { return "user"; diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessorTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessorTests.java index 35170a7376..4696fd87eb 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessorTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessorTests.java @@ -144,7 +144,7 @@ public class RequestResponseBodyMethodProcessorTests { expect(messageConverter.read(eq(String.class), isA(HttpInputMessage.class))).andReturn(body); replay(messageConverter); - Object result = processor.resolveArgument(paramRequestBodyString, mavContainer, webRequest, null); + Object result = processor.resolveArgument(paramRequestBodyString, mavContainer, webRequest, new ValidatingBinderFactory()); assertEquals("Invalid argument", body, result); assertFalse("The requestHandled flag shouldn't change", mavContainer.isRequestHandled()); @@ -211,7 +211,7 @@ public class RequestResponseBodyMethodProcessorTests { @Test public void resolveArgumentNotRequiredNoContent() throws Exception { - assertNull(processor.resolveArgument(paramStringNotRequired, mavContainer, webRequest, null)); + assertNull(processor.resolveArgument(paramStringNotRequired, mavContainer, webRequest, new ValidatingBinderFactory())); } @Test diff --git a/src/reference/docbook/mvc.xml b/src/reference/docbook/mvc.xml index 7868013ccb..3b8d17ebe2 100644 --- a/src/reference/docbook/mvc.xml +++ b/src/reference/docbook/mvc.xml @@ -1584,13 +1584,13 @@ public void handle(@RequestBody String body, Writer writer) throws IOException { An @RequestBody method parameter can be annotated with @Valid, in which case it will be validated using the configured Validator - instance. When using the MVC namespace a JSR-303 validator is - configured automatically assuming a JSR-303 implementation is + instance. When using the MVC namespace or Java config, a JSR-303 validator + is configured automatically assuming a JSR-303 implementation is available on the classpath. - Unlike @ModelAttribute parameters, for which - a BindingResult can be used to examine the errors, - @RequestBody validation errors always result in a - MethodArgumentNotValidException being raised. + Just like with @ModelAttribute parameters, + an Errors argument can be used to examine the errors. + If such an argument is not declared, a + MethodArgumentNotValidException will be raised. The exception is handled in the DefaultHandlerExceptionResolver, which sends a 400 error back to the client.