diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/RequestBodyNotValidException.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/RequestBodyNotValidException.java new file mode 100644 index 00000000000..ab8482bdc15 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/RequestBodyNotValidException.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2011 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet.mvc.method.annotation.support; + +import org.springframework.validation.Errors; +import org.springframework.web.bind.annotation.RequestBody; + +/** + * Thrown by {@link RequestResponseBodyMethodProcessor} when an @{@link RequestBody} argument annotated with + * {@code @Valid} results in validation errors. + * + * @author Rossen Stoyanchev + * @since 3.1 + */ +@SuppressWarnings("serial") +public class RequestBodyNotValidException extends RuntimeException { + + private final Errors errors; + + /** + * @param errors contains the results of validating an @{@link RequestBody} argument. + */ + public RequestBodyNotValidException(Errors errors) { + this.errors = errors; + } + + /** + * Returns an Errors instance with validation errors. + */ + public Errors getErrors() { + return errors; + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/RequestResponseBodyMethodProcessor.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/RequestResponseBodyMethodProcessor.java index a1b968bae6a..4fcb9c73d55 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/RequestResponseBodyMethodProcessor.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/RequestResponseBodyMethodProcessor.java @@ -17,21 +17,30 @@ package org.springframework.web.servlet.mvc.method.annotation.support; import java.io.IOException; +import java.lang.annotation.Annotation; import java.util.List; +import org.springframework.core.Conventions; import org.springframework.core.MethodParameter; import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; import org.springframework.web.HttpMediaTypeNotAcceptableException; -import org.springframework.web.HttpMediaTypeNotSupportedException; +import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.ModelAndViewContainer; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; /** - * Resolves method arguments annotated with @{@link RequestBody}. Handles return values from methods annotated with - * {@link ResponseBody}. + * Resolves method arguments annotated with @{@link RequestBody} and handles return values from methods + * annotated with {@link ResponseBody}. + * + *

An @{@link RequestBody} method argument will be validated if annotated with {@code @Valid}. A + * {@link Validator} instance can be configured globally in XML configuration with the Spring MVC namespace + * or in Java-based configuration with @{@link EnableWebMvc}. * * @author Arjen Poutsma * @author Rossen Stoyanchev @@ -54,11 +63,37 @@ public class RequestResponseBodyMethodProcessor extends AbstractMessageConverter public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, - WebDataBinderFactory binderFactory) - throws IOException, HttpMediaTypeNotSupportedException { - return readWithMessageConverters(webRequest, parameter, parameter.getParameterType()); + WebDataBinderFactory binderFactory) throws Exception { + Object arg = readWithMessageConverters(webRequest, parameter, parameter.getParameterType()); + if (shouldValidate(parameter, arg)) { + String argName = Conventions.getVariableNameForParameter(parameter); + WebDataBinder binder = binderFactory.createBinder(webRequest, arg, argName); + binder.validate(); + Errors errors = binder.getBindingResult(); + if (errors.hasErrors()) { + throw new RequestBodyNotValidException(errors); + } + } + return arg; } + /** + * Whether to validate the given @{@link RequestBody} method argument. The default implementation checks + * if the parameter is also annotated with {@code @Valid}. + * @param parameter the method argument for which to check if validation is needed + * @param argumentValue the method argument value (instantiated with a message converter) + * @return {@code true} if validation should be invoked, {@code false} otherwise. + */ + protected boolean shouldValidate(MethodParameter parameter, Object argumentValue) { + Annotation[] annotations = parameter.getParameterAnnotations(); + for (Annotation annot : annotations) { + if ("Valid".equals(annot.annotationType().getSimpleName())) { + return true; + } + } + return false; + } + public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/support/RequestResponseBodyMethodProcessorTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/support/RequestResponseBodyMethodProcessorTests.java index 376334ba1c6..b7d69f3f8d1 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/support/RequestResponseBodyMethodProcessorTests.java +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/support/RequestResponseBodyMethodProcessorTests.java @@ -16,15 +16,30 @@ package org.springframework.web.servlet.mvc.method.annotation.support; +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.eq; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.isA; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.reset; +import static org.easymock.EasyMock.verify; +import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import javax.validation.Valid; +import javax.validation.constraints.NotNull; + import org.junit.Before; import org.junit.Test; - import org.springframework.core.MethodParameter; import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpOutputMessage; @@ -34,18 +49,19 @@ import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.validation.DataBinder; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; import org.springframework.web.HttpMediaTypeNotAcceptableException; import org.springframework.web.HttpMediaTypeNotSupportedException; +import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.ServletWebRequest; import org.springframework.web.method.support.ModelAndViewContainer; import org.springframework.web.servlet.HandlerMapping; -import static org.easymock.EasyMock.*; -import static org.junit.Assert.*; - /** * Test fixture with {@link RequestResponseBodyMethodProcessor} and mock {@link HttpMessageConverter}. * @@ -60,9 +76,9 @@ public class RequestResponseBodyMethodProcessorTests { private MethodParameter paramRequestBodyString; private MethodParameter paramInt; + private MethodParameter paramValidBean; private MethodParameter returnTypeString; private MethodParameter returnTypeInt; - private MethodParameter returnTypeStringProduces; private ModelAndViewContainer mavContainer; @@ -87,10 +103,9 @@ public class RequestResponseBodyMethodProcessorTests { paramRequestBodyString = new MethodParameter(handle, 0); paramInt = new MethodParameter(handle, 1); returnTypeString = new MethodParameter(handle, -1); - returnTypeInt = new MethodParameter(getClass().getMethod("handle2"), -1); - returnTypeStringProduces = new MethodParameter(getClass().getMethod("handle3"), -1); + paramValidBean = new MethodParameter(getClass().getMethod("handle4", ValidBean.class), 0); mavContainer = new ModelAndViewContainer(); @@ -119,7 +134,6 @@ public class RequestResponseBodyMethodProcessorTests { String body = "Foo"; expect(messageConverter.canRead(String.class, contentType)).andReturn(true); expect(messageConverter.read(eq(String.class), isA(HttpInputMessage.class))).andReturn(body); - replay(messageConverter); Object result = processor.resolveArgument(paramRequestBodyString, mavContainer, webRequest, null); @@ -129,12 +143,50 @@ public class RequestResponseBodyMethodProcessorTests { verify(messageConverter); } + @SuppressWarnings("unchecked") + @Test + public void resolveArgumentNotValid() throws Exception { + MediaType contentType = MediaType.TEXT_PLAIN; + servletRequest.addHeader("Content-Type", contentType.toString()); + + HttpMessageConverter beanConverter = createMock(HttpMessageConverter.class); + expect(beanConverter.getSupportedMediaTypes()).andReturn(Collections.singletonList(MediaType.TEXT_PLAIN)); + expect(beanConverter.canRead(ValidBean.class, contentType)).andReturn(true); + expect(beanConverter.read(eq(ValidBean.class), isA(HttpInputMessage.class))).andReturn(new ValidBean(null)); + replay(beanConverter); + + processor = new RequestResponseBodyMethodProcessor(Collections.>singletonList(beanConverter)); + try { + processor.resolveArgument(paramValidBean, mavContainer, webRequest, new ValidatingBinderFactory()); + fail("Expected exception"); + } catch (RequestBodyNotValidException e) { + assertEquals("validBean", e.getErrors().getObjectName()); + assertEquals(1, e.getErrors().getErrorCount()); + assertNotNull(e.getErrors().getFieldError("name")); + } + } + + @SuppressWarnings("unchecked") + @Test + public void resolveArgumentValid() throws Exception { + MediaType contentType = MediaType.TEXT_PLAIN; + servletRequest.addHeader("Content-Type", contentType.toString()); + + HttpMessageConverter beanConverter = createMock(HttpMessageConverter.class); + expect(beanConverter.getSupportedMediaTypes()).andReturn(Collections.singletonList(MediaType.TEXT_PLAIN)); + expect(beanConverter.canRead(ValidBean.class, contentType)).andReturn(true); + expect(beanConverter.read(eq(ValidBean.class), isA(HttpInputMessage.class))).andReturn(new ValidBean("name")); + replay(beanConverter); + + processor = new RequestResponseBodyMethodProcessor(Collections.>singletonList(beanConverter)); + processor.resolveArgument(paramValidBean, mavContainer, webRequest, new ValidatingBinderFactory()); + } + @Test(expected = HttpMediaTypeNotSupportedException.class) public void resolveArgumentNotReadable() throws Exception { MediaType contentType = MediaType.TEXT_PLAIN; servletRequest.addHeader("Content-Type", contentType.toString()); - expect(messageConverter.getSupportedMediaTypes()).andReturn(Arrays.asList(contentType)); expect(messageConverter.canRead(String.class, contentType)).andReturn(false); replay(messageConverter); @@ -242,4 +294,32 @@ public class RequestResponseBodyMethodProcessorTests { return null; } + public void handle4(@Valid @RequestBody ValidBean b) { + } + + private final class ValidatingBinderFactory implements WebDataBinderFactory { + public WebDataBinder createBinder(NativeWebRequest webRequest, Object target, String objectName) throws Exception { + LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); + validator.afterPropertiesSet(); + WebDataBinder dataBinder = new WebDataBinder(target, objectName); + dataBinder.setValidator(validator); + return dataBinder; + } + } + + @SuppressWarnings("unused") + private static class ValidBean { + + @NotNull + private final String name; + + public ValidBean(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + } \ No newline at end of file