) 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.