From 49a2aaf023e2da5efb0ef49785499053ebd42c61 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Sat, 3 Dec 2011 15:44:33 +0000 Subject: [PATCH] added SmartValidator interface with general support for validation hints; added custom @Valid annotation with support for JSR-303 validation groups; JSR-303 SpringValidatorAdapter and MVC data binding provide support for validation groups (SPR-6373) --- .../validation/DataBinder.java | 16 +- .../validation/SmartValidator.java | 47 +++++ .../validation/annotation/Valid.java | 56 ++++++ .../validation/annotation/package-info.java | 8 + .../SpringValidatorAdapter.java | 30 ++- .../RequestPartMethodArgumentResolver.java | 30 +-- .../RequestResponseBodyMethodProcessor.java | 50 ++--- .../web/servlet/config/MvcNamespaceTests.java | 29 ++- .../ServletAnnotationControllerTests.java | 178 ++++++++++++------ .../support/HandlerMethodInvoker.java | 12 +- .../ModelAttributeMethodProcessorTests.java | 10 - 11 files changed, 324 insertions(+), 142 deletions(-) create mode 100644 org.springframework.context/src/main/java/org/springframework/validation/SmartValidator.java create mode 100644 org.springframework.context/src/main/java/org/springframework/validation/annotation/Valid.java create mode 100644 org.springframework.context/src/main/java/org/springframework/validation/annotation/package-info.java diff --git a/org.springframework.context/src/main/java/org/springframework/validation/DataBinder.java b/org.springframework.context/src/main/java/org/springframework/validation/DataBinder.java index 0d39ff6eb7..448da57812 100644 --- a/org.springframework.context/src/main/java/org/springframework/validation/DataBinder.java +++ b/org.springframework.context/src/main/java/org/springframework/validation/DataBinder.java @@ -706,8 +706,22 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { * @see #getBindingResult() */ public void validate() { + this.validator.validate(getTarget(), getBindingResult()); + } + + /** + * Invoke the specified Validator, if any, with the given validation hints. + *

Note: Validation hints may get ignored by the actual target Validator. + * @param validationHints one or more hint objects to be passed to a {@link SmartValidator} + * @see #setValidator(Validator) + * @see SmartValidator#validate(Object, Errors, Object...) + */ + public void validate(Object... validationHints) { Validator validator = getValidator(); - if (validator != null) { + if (validator instanceof SmartValidator) { + ((SmartValidator) validator).validate(getTarget(), getBindingResult(), validationHints); + } + else if (validator != null) { validator.validate(getTarget(), getBindingResult()); } } diff --git a/org.springframework.context/src/main/java/org/springframework/validation/SmartValidator.java b/org.springframework.context/src/main/java/org/springframework/validation/SmartValidator.java new file mode 100644 index 0000000000..afb7fda442 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/validation/SmartValidator.java @@ -0,0 +1,47 @@ +/* + * 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.validation; + +/** + * Extended variant of the {@link Validator} interface, adding support for + * validation 'hints'. + * + * @author Juergen Hoeller + * @since 3.1 + */ +public interface SmartValidator extends Validator { + + /** + * Validate the supplied target object, which must be + * of a {@link Class} for which the {@link #supports(Class)} method + * typically has (or would) return true. + *

The supplied {@link Errors errors} instance can be used to report + * any resulting validation errors. + *

This variant of validate supports validation hints, + * such as validation groups against a JSR-303 provider (in this case, + * the provided hint objects need to be annotation arguments of type Class). + *

Note: Validation hints may get ignored by the actual target Validator, + * in which case this method is supposed to be behave just like its regular + * {@link #validate(Object, Errors)} sibling. + * @param target the object that is to be validated (can be null) + * @param errors contextual state about the validation process (never null) + * @param validationHints one or more hint objects to be passed to the validation engine + * @see ValidationUtils + */ + void validate(Object target, Errors errors, Object... validationHints); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/validation/annotation/Valid.java b/org.springframework.context/src/main/java/org/springframework/validation/annotation/Valid.java new file mode 100644 index 0000000000..b7807ab912 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/validation/annotation/Valid.java @@ -0,0 +1,56 @@ +/* + * 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.validation.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Extended variant of JSR-303's {@link javax.validation.Valid}, + * supporting the specification of validation groups. Designed for + * convenient use with Spring's JSR-303 support but not JSR-303 specific. + * + *

Can be used e.g. with Spring MVC handler methods arguments. + * Supported through {@link org.springframework.validation.SmartValidator}'s + * validation hint concept, with validation group classes acting as hint objects. + * + * @author Juergen Hoeller + * @since 3.1 + * @see javax.validation.Validator#validate(Object, Class[]) + * @see org.springframework.validation.SmartValidator#validate(Object, org.springframework.validation.Errors, Object...) + * @see org.springframework.validation.beanvalidation.SpringValidatorAdapter + */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Valid { + + /** + * Specify one or more validation groups to apply to the validation step + * kicked off by this annotation. + *

JSR-303 defines validation groups as custom annotations which an application declares + * for the sole purpose of using them as type-safe group arguments, as implemented in + * {@link org.springframework.validation.beanvalidation.SpringValidatorAdapter}. + *

Other {@link org.springframework.validation.SmartValidator} implementations may + * support class arguments in other ways as well. + */ + Class[] value() default {}; + +} diff --git a/org.springframework.context/src/main/java/org/springframework/validation/annotation/package-info.java b/org.springframework.context/src/main/java/org/springframework/validation/annotation/package-info.java new file mode 100644 index 0000000000..f13f798131 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/validation/annotation/package-info.java @@ -0,0 +1,8 @@ +/** + * Support classes for annotation-based constraint evaluation, + * e.g. using a JSR-303 Bean Validation provider. + * + *

Provides an extended variant of JSR-303's @Valid, + * supporting the specification of validation groups. + */ +package org.springframework.validation.annotation; diff --git a/org.springframework.context/src/main/java/org/springframework/validation/beanvalidation/SpringValidatorAdapter.java b/org.springframework.context/src/main/java/org/springframework/validation/beanvalidation/SpringValidatorAdapter.java index a03c621631..490fffdebd 100644 --- a/org.springframework.context/src/main/java/org/springframework/validation/beanvalidation/SpringValidatorAdapter.java +++ b/org.springframework.context/src/main/java/org/springframework/validation/beanvalidation/SpringValidatorAdapter.java @@ -17,6 +17,7 @@ package org.springframework.validation.beanvalidation; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -33,7 +34,7 @@ import org.springframework.validation.BindingResult; import org.springframework.validation.Errors; import org.springframework.validation.FieldError; import org.springframework.validation.ObjectError; -import org.springframework.validation.Validator; +import org.springframework.validation.SmartValidator; /** * Adapter that takes a JSR-303 javax.validator.Validator @@ -46,7 +47,7 @@ import org.springframework.validation.Validator; * @author Juergen Hoeller * @since 3.0 */ -public class SpringValidatorAdapter implements Validator, javax.validation.Validator { +public class SpringValidatorAdapter implements SmartValidator, javax.validation.Validator { private static final Set internalAnnotationAttributes = new HashSet(3); @@ -85,8 +86,29 @@ public class SpringValidatorAdapter implements Validator, javax.validation.Valid } public void validate(Object target, Errors errors) { - Set> result = this.targetValidator.validate(target); - for (ConstraintViolation violation : result) { + processConstraintViolations(this.targetValidator.validate(target), errors); + } + + public void validate(Object target, Errors errors, Object[] validationHints) { + Set groups = new LinkedHashSet(); + if (validationHints != null) { + for (Object hint : validationHints) { + if (hint instanceof Class) { + groups.add((Class) hint); + } + } + } + processConstraintViolations(this.targetValidator.validate(target, groups.toArray(new Class[groups.size()])), errors); + } + + /** + * Process the given JSR-303 ConstraintViolations, adding corresponding errors to + * the provided Spring {@link Errors} object. + * @param violations the JSR-303 ConstraintViolation results + * @param errors the Spring errors object to register to + */ + protected void processConstraintViolations(Set> violations, Errors errors) { + for (ConstraintViolation violation : violations) { String field = violation.getPropertyPath().toString(); FieldError fieldError = errors.getFieldError(field); if (fieldError == null || !fieldError.isBindingFailure()) { diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/RequestPartMethodArgumentResolver.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/RequestPartMethodArgumentResolver.java index bd03b9d3c1..3e2c15f27e 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/RequestPartMethodArgumentResolver.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/RequestPartMethodArgumentResolver.java @@ -19,11 +19,11 @@ package org.springframework.web.servlet.mvc.method.annotation.support; import java.lang.annotation.Annotation; import java.util.Collection; import java.util.List; - import javax.servlet.http.HttpServletRequest; import org.springframework.core.GenericCollectionTypeResolver; import org.springframework.core.MethodParameter; +import org.springframework.core.annotation.AnnotationUtils; import org.springframework.http.HttpInputMessage; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.util.Assert; @@ -106,14 +106,12 @@ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterM } } - public Object resolveArgument(MethodParameter parameter, - ModelAndViewContainer mavContainer, - NativeWebRequest request, - WebDataBinderFactory binderFactory) throws Exception { + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest request, WebDataBinderFactory binderFactory) throws Exception { HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class); if (!isMultipartRequest(servletRequest)) { - throw new MultipartException("The current request is not a multipart request."); + throw new MultipartException("The current request is not a multipart request"); } MultipartHttpServletRequest multipartRequest = @@ -137,15 +135,19 @@ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterM try { HttpInputMessage inputMessage = new RequestPartServletServerHttpRequest(servletRequest, partName); arg = readWithMessageConverters(inputMessage, parameter, parameter.getParameterType()); - if (isValidationApplicable(arg, parameter)) { - WebDataBinder binder = binderFactory.createBinder(request, arg, partName); - binder.validate(); - BindingResult bindingResult = binder.getBindingResult(); - if (bindingResult.hasErrors()) { - throw new MethodArgumentNotValidException(parameter, bindingResult); + Annotation[] annotations = parameter.getParameterAnnotations(); + for (Annotation annot : annotations) { + if ("Valid".equals(annot.annotationType().getSimpleName())) { + 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); + } } } - } + } catch (MissingServletRequestPartException e) { // handled below arg = null; @@ -153,7 +155,7 @@ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterM } RequestPart annot = parameter.getParameterAnnotation(RequestPart.class); - boolean isRequired = (annot != null) ? annot.required() : true; + boolean isRequired = (annot == null || annot.required()); if (arg == null && isRequired) { throw new MissingServletRequestPartException(partName); 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 74e07211bb..711b5ace4f 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 @@ -22,6 +22,7 @@ import java.util.List; import org.springframework.core.Conventions; import org.springframework.core.MethodParameter; +import org.springframework.core.annotation.AnnotationUtils; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.validation.BindingResult; import org.springframework.web.HttpMediaTypeNotAcceptableException; @@ -64,43 +65,30 @@ public class RequestResponseBodyMethodProcessor extends AbstractMessageConverter return returnType.getMethodAnnotation(ResponseBody.class) != null; } - public Object resolveArgument(MethodParameter parameter, - ModelAndViewContainer mavContainer, - NativeWebRequest webRequest, - WebDataBinderFactory binderFactory) throws Exception { + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + Object arg = readWithMessageConverters(webRequest, parameter, parameter.getParameterType()); - if (isValidationApplicable(arg, parameter)) { - String name = Conventions.getVariableNameForParameter(parameter); - WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name); - binder.validate(); - BindingResult bindingResult = binder.getBindingResult(); - if (bindingResult.hasErrors()) { - throw new MethodArgumentNotValidException(parameter, bindingResult); + Annotation[] annotations = parameter.getParameterAnnotations(); + for (Annotation annot : annotations) { + if ("Valid".equals(annot.annotationType().getSimpleName())) { + 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); + } } } return arg; } - /** - * Whether to validate the given {@code @RequestBody} method argument. - * The default implementation looks for {@code @javax.validation.Valid}. - * @param argument the resolved argument value - * @param parameter the method argument - */ - protected boolean isValidationApplicable(Object argument, MethodParameter parameter) { - 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, - NativeWebRequest webRequest) throws IOException, HttpMediaTypeNotAcceptableException { + public void handleReturnValue(Object returnValue, MethodParameter returnType, + ModelAndViewContainer mavContainer, NativeWebRequest webRequest) + throws IOException, HttpMediaTypeNotAcceptableException { + mavContainer.setRequestHandled(true); if (returnValue != null) { writeWithMessageConverters(returnValue, returnType, webRequest); diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java index f996a5a822..a0ae6f79b9 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java @@ -16,24 +16,18 @@ package org.springframework.web.servlet.config; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; - +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Method; import java.util.Date; import java.util.List; import java.util.Locale; - import javax.servlet.RequestDispatcher; -import javax.validation.Valid; import javax.validation.constraints.NotNull; import org.junit.Before; import org.junit.Test; + import org.springframework.beans.DirectFieldAccessor; import org.springframework.beans.TypeMismatchException; import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; @@ -53,6 +47,7 @@ import org.springframework.stereotype.Controller; import org.springframework.validation.BindingResult; import org.springframework.validation.Errors; import org.springframework.validation.Validator; +import org.springframework.validation.annotation.Valid; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -75,6 +70,8 @@ import org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler import org.springframework.web.servlet.resource.ResourceHttpRequestHandler; import org.springframework.web.servlet.theme.ThemeChangeInterceptor; +import static org.junit.Assert.*; + /** * @author Keith Donald * @author Arjen Poutsma @@ -453,12 +450,8 @@ public class MvcNamespaceTests { private boolean recordedValidationError; @RequestMapping - public void testBind(@RequestParam @DateTimeFormat(iso=ISO.DATE) Date date, @Valid TestBean bean, BindingResult result) { - if (result.getErrorCount() == 1) { - this.recordedValidationError = true; - } else { - this.recordedValidationError = false; - } + public void testBind(@RequestParam @DateTimeFormat(iso=ISO.DATE) Date date, @Valid(MyGroup.class) TestBean bean, BindingResult result) { + this.recordedValidationError = (result.getErrorCount() == 1); } } @@ -475,9 +468,13 @@ public class MvcNamespaceTests { } } + @Retention(RetentionPolicy.RUNTIME) + public @interface MyGroup { + } + private static class TestBean { - @NotNull + @NotNull(groups=MyGroup.class) private String field; @SuppressWarnings("unused") diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationControllerTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationControllerTests.java index cc27df192c..48799de180 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationControllerTests.java +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationControllerTests.java @@ -16,14 +16,6 @@ package org.springframework.web.servlet.mvc.annotation; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - import java.beans.PropertyEditorSupport; import java.io.IOException; import java.io.Serializable; @@ -38,16 +30,17 @@ import java.security.Principal; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.Iterator; +import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; - import javax.servlet.ServletConfig; import javax.servlet.ServletContext; import javax.servlet.ServletException; @@ -55,11 +48,11 @@ import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; -import javax.validation.Valid; import javax.validation.constraints.NotNull; import javax.xml.bind.annotation.XmlRootElement; import org.junit.Test; + import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.aop.interceptor.SimpleTraceInterceptor; import org.springframework.aop.support.DefaultPointcutAdvisor; @@ -67,6 +60,8 @@ import org.springframework.beans.BeansException; import org.springframework.beans.DerivedTestBean; import org.springframework.beans.GenericBean; import org.springframework.beans.ITestBean; +import org.springframework.beans.PropertyEditorRegistrar; +import org.springframework.beans.PropertyEditorRegistry; import org.springframework.beans.TestBean; import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.annotation.Autowired; @@ -108,6 +103,7 @@ import org.springframework.util.StringUtils; import org.springframework.validation.BindingResult; import org.springframework.validation.Errors; import org.springframework.validation.FieldError; +import org.springframework.validation.annotation.Valid; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.CookieValue; @@ -142,6 +138,8 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes; import org.springframework.web.servlet.view.InternalResourceViewResolver; import org.springframework.web.util.NestedServletException; +import static org.junit.Assert.*; + /** * @author Juergen Hoeller * @author Sam Brannen @@ -1718,6 +1716,61 @@ public class ServletAnnotationControllerTests { assertEquals("templatePath", response.getContentAsString()); } + @Test + public void testMatchWithoutMethodLevelPath() throws Exception { + initServlet(NoPathGetAndM2PostController.class); + + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/t1/m2"); + MockHttpServletResponse response = new MockHttpServletResponse(); + servlet.service(request, response); + assertEquals(405, response.getStatus()); + } + + // SPR-8536 + + @Test + public void testHeadersCondition() throws Exception { + initServlet(HeadersConditionController.class); + + // No "Accept" header + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/"); + MockHttpServletResponse response = new MockHttpServletResponse(); + servlet.service(request, response); + + assertEquals(200, response.getStatus()); + assertEquals("home", response.getForwardedUrl()); + + // Accept "*/*" + request = new MockHttpServletRequest("GET", "/"); + request.addHeader("Accept", "*/*"); + response = new MockHttpServletResponse(); + servlet.service(request, response); + + assertEquals(200, response.getStatus()); + assertEquals("home", response.getForwardedUrl()); + + // Accept "application/json" + request = new MockHttpServletRequest("GET", "/"); + request.addHeader("Accept", "application/json"); + response = new MockHttpServletResponse(); + servlet.service(request, response); + + assertEquals(200, response.getStatus()); + assertEquals("application/json", response.getHeader("Content-Type")); + assertEquals("homeJson", response.getContentAsString()); + } + + @Test + public void redirectAttribute() throws Exception { + initServlet(RedirectAttributesController.class); + try { + servlet.service(new MockHttpServletRequest("GET", "/"), new MockHttpServletResponse()); + } + catch (NestedServletException ex) { + assertTrue(ex.getMessage().contains("not assignable from the actual model")); + } + } + /* * See SPR-6021 */ @@ -1868,60 +1921,59 @@ public class ServletAnnotationControllerTests { } @Test - public void testMatchWithoutMethodLevelPath() throws Exception { - initServlet(NoPathGetAndM2PostController.class); + public void parameterCsvAsIntegerSetWithCustomSeparator() throws Exception { + servlet = new DispatcherServlet() { + @Override + protected WebApplicationContext createWebApplicationContext(WebApplicationContext parent) { + GenericWebApplicationContext wac = new GenericWebApplicationContext(); + wac.registerBeanDefinition("controller", new RootBeanDefinition(CsvController.class)); + RootBeanDefinition csDef = new RootBeanDefinition(FormattingConversionServiceFactoryBean.class); + RootBeanDefinition wbiDef = new RootBeanDefinition(ConfigurableWebBindingInitializer.class); + wbiDef.getPropertyValues().add("conversionService", csDef); + wbiDef.getPropertyValues().add("propertyEditorRegistrars", new RootBeanDefinition(ListEditorRegistrar.class)); + RootBeanDefinition adapterDef = new RootBeanDefinition(AnnotationMethodHandlerAdapter.class); + adapterDef.getPropertyValues().add("webBindingInitializer", wbiDef); + wac.registerBeanDefinition("handlerAdapter", adapterDef); + wac.refresh(); + return wac; + } + }; + servlet.init(new MockServletConfig()); - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/t1/m2"); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setRequestURI("/integerSet"); + request.setMethod("POST"); + request.addParameter("content", "1;2"); MockHttpServletResponse response = new MockHttpServletResponse(); servlet.service(request, response); - assertEquals(405, response.getStatus()); + assertEquals("1-2", response.getContentAsString()); } - - // SPR-8536 - @Test - public void testHeadersCondition() throws Exception { - initServlet(HeadersConditionController.class); + public static class ListEditorRegistrar implements PropertyEditorRegistrar { - // No "Accept" header - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/"); - MockHttpServletResponse response = new MockHttpServletResponse(); - servlet.service(request, response); - - assertEquals(200, response.getStatus()); - assertEquals("home", response.getForwardedUrl()); - - // Accept "*/*" - request = new MockHttpServletRequest("GET", "/"); - request.addHeader("Accept", "*/*"); - response = new MockHttpServletResponse(); - servlet.service(request, response); - - assertEquals(200, response.getStatus()); - assertEquals("home", response.getForwardedUrl()); - - // Accept "application/json" - request = new MockHttpServletRequest("GET", "/"); - request.addHeader("Accept", "application/json"); - response = new MockHttpServletResponse(); - servlet.service(request, response); - - assertEquals(200, response.getStatus()); - assertEquals("application/json", response.getHeader("Content-Type")); - assertEquals("homeJson", response.getContentAsString()); - } - - @Test - public void redirectAttribute() throws Exception { - initServlet(RedirectAttributesController.class); - try { - servlet.service(new MockHttpServletRequest("GET", "/"), new MockHttpServletResponse()); - } - catch (NestedServletException ex) { - assertTrue(ex.getMessage().contains("not assignable from the actual model")); + public void registerCustomEditors(PropertyEditorRegistry registry) { + registry.registerCustomEditor(Set.class, new ListEditor()); } } - + + public static class ListEditor extends PropertyEditorSupport { + + @SuppressWarnings("unchecked") + @Override + public String getAsText() { + return StringUtils.collectionToDelimitedString((Collection) getValue(), ";"); + } + + @Override + public void setAsText(String text) throws IllegalArgumentException { + Set s = new LinkedHashSet(); + for (String t : text.split(";")) { + s.add(t); + } + setValue(s); + } + } + /* * Controllers @@ -2226,7 +2278,7 @@ public class ServletAnnotationControllerTests { public static class ValidTestBean extends TestBean { - @NotNull + @NotNull(groups = MyGroup.class) private String validCountry; public void setValidCountry(String validCountry) { @@ -2264,9 +2316,7 @@ public class ServletAnnotationControllerTests { @SuppressWarnings("unused") @ModelAttribute("myCommand") - private ValidTestBean createTestBean(@RequestParam T defaultName, - Map model, - @RequestParam Date date) { + private ValidTestBean createTestBean(@RequestParam T defaultName, Map model, @RequestParam Date date) { model.put("myKey", "myOriginalValue"); ValidTestBean tb = new ValidTestBean(); tb.setName(defaultName.getClass().getSimpleName() + ":" + defaultName.toString()); @@ -2275,7 +2325,7 @@ public class ServletAnnotationControllerTests { @Override @RequestMapping("/myPath.do") - public String myHandle(@ModelAttribute("myCommand") @Valid TestBean tb, BindingResult errors, ModelMap model) { + public String myHandle(@ModelAttribute("myCommand") @Valid(MyGroup.class) TestBean tb, BindingResult errors, ModelMap model) { if (!errors.hasFieldErrors("validCountry")) { throw new IllegalStateException("Declarative validation not applied"); } @@ -2333,7 +2383,7 @@ public class ServletAnnotationControllerTests { @Override @RequestMapping("/myPath.do") - public String myHandle(@ModelAttribute("myCommand") @Valid TestBean tb, BindingResult errors, ModelMap model) { + public String myHandle(@ModelAttribute("myCommand") @Valid(MyGroup.class) TestBean tb, BindingResult errors, ModelMap model) { if (!errors.hasFieldErrors("sex")) { throw new IllegalStateException("requiredFields not applied"); } @@ -2360,6 +2410,10 @@ public class ServletAnnotationControllerTests { } } + @Retention(RetentionPolicy.RUNTIME) + public @interface MyGroup { + } + private static class MyWebBindingInitializer implements WebBindingInitializer { public void initBinder(WebDataBinder binder, WebRequest request) { diff --git a/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvoker.java b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvoker.java index fb4c82772f..42bc358a03 100644 --- a/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvoker.java +++ b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvoker.java @@ -34,6 +34,7 @@ import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; + import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.BridgeMethodResolver; @@ -251,6 +252,7 @@ public class HandlerMethodInvoker { boolean required = false; String defaultValue = null; boolean validate = false; + Object[] validationHints = null; int annotationsFound = 0; Annotation[] paramAnns = methodParam.getParameterAnnotations(); @@ -295,6 +297,8 @@ public class HandlerMethodInvoker { } else if ("Valid".equals(paramAnn.annotationType().getSimpleName())) { validate = true; + Object value = AnnotationUtils.getValue(paramAnn); + validationHints = (value instanceof Object[] ? (Object[]) value : new Object[] {value}); } } @@ -360,7 +364,7 @@ public class HandlerMethodInvoker { resolveModelAttribute(attrName, methodParam, implicitModel, webRequest, handler); boolean assignBindingResult = (args.length > i + 1 && Errors.class.isAssignableFrom(paramTypes[i + 1])); if (binder.getTarget() != null) { - doBind(binder, webRequest, validate, !assignBindingResult); + doBind(binder, webRequest, validate, validationHints, !assignBindingResult); } args[i] = binder.getTarget(); if (assignBindingResult) { @@ -803,12 +807,12 @@ public class HandlerMethodInvoker { return new WebRequestDataBinder(target, objectName); } - private void doBind(WebDataBinder binder, NativeWebRequest webRequest, boolean validate, boolean failOnErrors) - throws Exception { + private void doBind(WebDataBinder binder, NativeWebRequest webRequest, boolean validate, + Object[] validationHints, boolean failOnErrors) throws Exception { doBind(binder, webRequest); if (validate) { - binder.validate(); + binder.validate(validationHints); } if (failOnErrors && binder.getBindingResult().hasErrors()) { throw new BindException(binder.getBindingResult()); diff --git a/org.springframework.web/src/test/java/org/springframework/web/method/annotation/support/ModelAttributeMethodProcessorTests.java b/org.springframework.web/src/test/java/org/springframework/web/method/annotation/support/ModelAttributeMethodProcessorTests.java index fdca12571d..ad530bdcd1 100644 --- a/org.springframework.web/src/test/java/org/springframework/web/method/annotation/support/ModelAttributeMethodProcessorTests.java +++ b/org.springframework.web/src/test/java/org/springframework/web/method/annotation/support/ModelAttributeMethodProcessorTests.java @@ -140,16 +140,6 @@ public class ModelAttributeMethodProcessorTests { assertTrue(processor.supportsReturnType(returnParamNonSimpleType)); } - @Test - public void validationApplicable() throws Exception { - assertTrue(processor.isValidationApplicable(null, paramNamedValidModelAttr)); - } - - @Test - public void validationNotApplicable() throws Exception { - assertFalse(processor.isValidationApplicable(null, paramNonSimpleType)); - } - @Test public void bindExceptionRequired() throws Exception { assertTrue(processor.isBindExceptionRequired(null, paramNonSimpleType));