From 929cda67902316eb335b2b6d62548709f6742ca8 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 24 Nov 2014 23:34:24 +0100 Subject: [PATCH] Allow custom @Validated annotations for handler method parameters Issue: SPR-12406 --- .../support/PayloadArgumentResolver.java | 29 +++++++-------- .../support/PayloadArgumentResolverTests.java | 22 +++++++----- .../support/HandlerMethodInvoker.java | 22 +++++++----- .../ModelAttributeMethodProcessor.java | 9 +++-- .../RequestPartMethodArgumentResolver.java | 11 +++--- .../RequestResponseBodyMethodProcessor.java | 9 +++-- .../web/servlet/config/MvcNamespaceTests.java | 35 +++++++++++++------ .../ServletAnnotationControllerTests.java | 10 ++++-- 8 files changed, 91 insertions(+), 56 deletions(-) diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/PayloadArgumentResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/PayloadArgumentResolver.java index ec6e86f9de..7a24738068 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/PayloadArgumentResolver.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/PayloadArgumentResolver.java @@ -34,14 +34,15 @@ import org.springframework.validation.BindingResult; import org.springframework.validation.ObjectError; import org.springframework.validation.SmartValidator; import org.springframework.validation.Validator; +import org.springframework.validation.annotation.Validated; /** * A resolver to extract and convert the payload of a message using a * {@link MessageConverter}. It also validates the payload using a * {@link Validator} if the argument is annotated with a Validation annotation. * - *

This {@link HandlerMethodArgumentResolver} should be ordered last as it supports all - * types and does not require the {@link Payload} annotation. + *

This {@link HandlerMethodArgumentResolver} should be ordered last as it + * supports all types and does not require the {@link Payload} annotation. * * @author Rossen Stoyanchev * @author Brian Clozel @@ -70,14 +71,14 @@ public class PayloadArgumentResolver implements HandlerMethodArgumentResolver { @Override public Object resolveArgument(MethodParameter param, Message message) throws Exception { - Payload annot = param.getParameterAnnotation(Payload.class); - if ((annot != null) && StringUtils.hasText(annot.value())) { + Payload ann = param.getParameterAnnotation(Payload.class); + if (ann != null && StringUtils.hasText(ann.value())) { throw new IllegalStateException("@Payload SpEL expressions not supported by this resolver"); } Object payload = message.getPayload(); if (isEmptyPayload(payload)) { - if (annot == null || annot.required()) { + if (ann == null || ann.required()) { String paramName = getParameterName(param); BindingResult bindingResult = new BeanPropertyBindingResult(payload, paramName); bindingResult.addError(new ObjectError(paramName, "@Payload param is required")); @@ -97,7 +98,7 @@ public class PayloadArgumentResolver implements HandlerMethodArgumentResolver { payload = this.converter.fromMessage(message, targetClass); if (payload == null) { throw new MessageConversionException(message, - "No converter found to convert to " + targetClass + ", message=" + message, null); + "No converter found to convert to " + targetClass + ", message=" + message); } validate(message, param, payload); return payload; @@ -106,7 +107,7 @@ public class PayloadArgumentResolver implements HandlerMethodArgumentResolver { private String getParameterName(MethodParameter param) { String paramName = param.getParameterName(); - return (paramName == null ? "Arg " + param.getParameterIndex() : paramName); + return (paramName != null ? paramName : "Arg " + param.getParameterIndex()); } /** @@ -132,26 +133,22 @@ public class PayloadArgumentResolver implements HandlerMethodArgumentResolver { if (this.validator == null) { return; } - - for (Annotation annot : parameter.getParameterAnnotations()) { - if (annot.annotationType().getSimpleName().startsWith("Valid")) { + for (Annotation ann : parameter.getParameterAnnotations()) { + Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); + if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { + Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); + Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); BeanPropertyBindingResult bindingResult = new BeanPropertyBindingResult(target, getParameterName(parameter)); - - Object hints = AnnotationUtils.getValue(annot); - Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); - if (!ObjectUtils.isEmpty(validationHints) && this.validator instanceof SmartValidator) { ((SmartValidator) this.validator).validate(target, bindingResult, validationHints); } else { this.validator.validate(target, bindingResult); } - if (bindingResult.hasErrors()) { throw new MethodArgumentNotValidException(message, parameter, bindingResult); } - break; } } diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/PayloadArgumentResolverTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/PayloadArgumentResolverTests.java index a75f34fd49..2c40b6b465 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/PayloadArgumentResolverTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/PayloadArgumentResolverTests.java @@ -16,6 +16,10 @@ package org.springframework.messaging.handler.annotation.support; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.lang.reflect.Method; import java.util.Locale; @@ -72,10 +76,8 @@ public class PayloadArgumentResolverTests { @Before public void setup() throws Exception { - this.resolver = new PayloadArgumentResolver(new StringMessageConverter(), testValidator()); - - payloadMethod = PayloadArgumentResolverTests.class.getDeclaredMethod("handleMessage", + this.payloadMethod = PayloadArgumentResolverTests.class.getDeclaredMethod("handleMessage", String.class, String.class, Locale.class, String.class, String.class, String.class, String.class); this.paramAnnotated = getMethodParameter(this.payloadMethod, 0); @@ -115,7 +117,6 @@ public class PayloadArgumentResolverTests { @Test public void resolveNotRequired() throws Exception { - Message emptyByteArrayMessage = MessageBuilder.withPayload(new byte[0]).build(); assertNull(this.resolver.resolveArgument(this.paramAnnotatedNotRequired, emptyByteArrayMessage)); @@ -168,14 +169,12 @@ public class PayloadArgumentResolverTests { @Test public void resolveNonAnnotatedParameter() throws Exception { - Message notEmptyMessage = MessageBuilder.withPayload("ABC".getBytes()).build(); assertEquals("ABC", this.resolver.resolveArgument(this.paramNotAnnotated, notEmptyMessage)); Message emptyStringMessage = MessageBuilder.withPayload("").build(); thrown.expect(MethodArgumentNotValidException.class); this.resolver.resolveArgument(this.paramValidated, emptyStringMessage); - } @Test @@ -188,8 +187,8 @@ public class PayloadArgumentResolverTests { assertEquals("invalidValue", this.resolver.resolveArgument(this.paramValidatedNotAnnotated, message)); } - private Validator testValidator() { + private Validator testValidator() { return new Validator() { @Override public boolean supports(Class clazz) { @@ -216,9 +215,16 @@ public class PayloadArgumentResolverTests { @Payload(required=false) String paramNotRequired, @Payload(required=true) Locale nonConvertibleRequiredParam, @Payload("foo.bar") String paramWithSpelExpression, - @Validated @Payload String validParam, + @MyValid @Payload String validParam, @Validated String validParamNotAnnotated, String paramNotAnnotated) { } + + @Validated + @Target({ElementType.PARAMETER}) + @Retention(RetentionPolicy.RUNTIME) + public @interface MyValid { + } + } diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvoker.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvoker.java index 11f98ba95e..d1e86e4ffb 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvoker.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvoker.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2014 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. @@ -59,6 +59,7 @@ import org.springframework.util.ReflectionUtils; import org.springframework.validation.BindException; import org.springframework.validation.BindingResult; import org.springframework.validation.Errors; +import org.springframework.validation.annotation.Validated; import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.CookieValue; @@ -82,11 +83,11 @@ import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartRequest; /** - * Support class for invoking an annotated handler method. Operates on the introspection results of a {@link - * HandlerMethodResolver} for a specific handler type. + * Support class for invoking an annotated handler method. Operates on the introspection + * results of a {@link HandlerMethodResolver} for a specific handler type. * - *

Used by {@link org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter} and {@link - * org.springframework.web.portlet.mvc.annotation.AnnotationMethodHandlerAdapter}. + *

Used by {@link org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter} + * and {@link org.springframework.web.portlet.mvc.annotation.AnnotationMethodHandlerAdapter}. * * @author Juergen Hoeller * @author Arjen Poutsma @@ -295,10 +296,13 @@ public class HandlerMethodInvoker { else if (Value.class.isInstance(paramAnn)) { defaultValue = ((Value) paramAnn).value(); } - else if (paramAnn.annotationType().getSimpleName().startsWith("Valid")) { - validate = true; - Object value = AnnotationUtils.getValue(paramAnn); - validationHints = (value instanceof Object[] ? (Object[]) value : new Object[] {value}); + else { + Validated validatedAnn = AnnotationUtils.getAnnotation(paramAnn, Validated.class); + if (validatedAnn != null || paramAnn.annotationType().getSimpleName().startsWith("Valid")) { + validate = true; + Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(paramAnn)); + validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[]{hints}); + } } } 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 2ecd2c8173..be7b0b54ed 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 @@ -27,6 +27,7 @@ import org.springframework.core.MethodParameter; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.validation.BindException; import org.springframework.validation.Errors; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.support.WebDataBinderFactory; @@ -157,9 +158,11 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) { Annotation[] annotations = parameter.getParameterAnnotations(); for (Annotation ann : annotations) { - if (ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = AnnotationUtils.getValue(ann); - binder.validate(hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); + Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); + if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { + Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); + Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); + binder.validate(validationHints); break; } } 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 207bfd6ff6..5d076b2e79 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 @@ -31,6 +31,7 @@ import org.springframework.http.converter.HttpMessageConverter; import org.springframework.util.Assert; import org.springframework.validation.BindingResult; import org.springframework.validation.Errors; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.RequestBody; @@ -223,10 +224,12 @@ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterM 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}); + for (Annotation ann : annotations) { + Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); + if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { + Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); + Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); + binder.validate(validationHints); BindingResult bindingResult = binder.getBindingResult(); if (bindingResult.hasErrors()) { if (isBindingErrorFatal(parameter)) { 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 08fe6a9e96..c13e8f1a7c 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 @@ -33,6 +33,7 @@ import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.validation.BindingResult; import org.springframework.validation.Errors; +import org.springframework.validation.annotation.Validated; import org.springframework.web.HttpMediaTypeNotAcceptableException; import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.accept.ContentNegotiationManager; @@ -114,9 +115,11 @@ public class RequestResponseBodyMethodProcessor extends AbstractMessageConverter private void validate(WebDataBinder binder, MethodParameter parameter) throws Exception { Annotation[] annotations = parameter.getParameterAnnotations(); for (Annotation ann : annotations) { - if (ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = AnnotationUtils.getValue(ann); - binder.validate(hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); + Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); + if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { + Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); + Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); + binder.validate(validationHints); BindingResult bindingResult = binder.getBindingResult(); if (bindingResult.hasErrors()) { if (isBindExceptionRequired(binder, parameter)) { diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java index e7d8db60a1..7fc8ef0f14 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java @@ -16,8 +16,10 @@ package org.springframework.web.servlet.config; +import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.lang.reflect.Method; import java.util.Arrays; import java.util.Collection; @@ -150,6 +152,7 @@ public class MvcNamespaceTests { private HandlerMethod handlerMethod; + @Before public void setUp() throws Exception { TestMockServletContext servletContext = new TestMockServletContext(); @@ -187,7 +190,7 @@ public class MvcNamespaceTests { List> converters = adapter.getMessageConverters(); assertTrue(converters.size() > 0); - for(HttpMessageConverter converter : converters) { + for (HttpMessageConverter converter : converters) { if (converter instanceof AbstractJackson2HttpMessageConverter) { ObjectMapper objectMapper = ((AbstractJackson2HttpMessageConverter)converter).getObjectMapper(); assertFalse(objectMapper.getDeserializationConfig().isEnabled(MapperFeature.DEFAULT_VIEW_INCLUSION)); @@ -262,9 +265,6 @@ public class MvcNamespaceTests { doTestCustomValidator("mvc-config-custom-validator-32.xml"); } - /** - * @throws Exception - */ private void doTestCustomValidator(String xml) throws Exception { loadBeanDefinitions(xml, 13); @@ -808,12 +808,11 @@ public class MvcNamespaceTests { assertEquals(TestPathHelper.class, viewController.getUrlPathHelper().getClass()); assertEquals(TestPathMatcher.class, viewController.getPathMatcher().getClass()); - for(SimpleUrlHandlerMapping handlerMapping : appContext.getBeansOfType(SimpleUrlHandlerMapping.class).values()) { + for (SimpleUrlHandlerMapping handlerMapping : appContext.getBeansOfType(SimpleUrlHandlerMapping.class).values()) { assertNotNull(handlerMapping); assertEquals(TestPathHelper.class, handlerMapping.getUrlPathHelper().getClass()); assertEquals(TestPathMatcher.class, handlerMapping.getPathMatcher().getClass()); } - } @@ -827,13 +826,25 @@ public class MvcNamespaceTests { } + @DateTimeFormat(iso=ISO.DATE) + @Target({ElementType.PARAMETER}) + @Retention(RetentionPolicy.RUNTIME) + public @interface IsoDate { + } + + @Validated(MyGroup.class) + @Target({ElementType.PARAMETER}) + @Retention(RetentionPolicy.RUNTIME) + public @interface MyValid { + } + @Controller public static class TestController { private boolean recordedValidationError; @RequestMapping - public void testBind(@RequestParam @DateTimeFormat(iso=ISO.DATE) Date date, @Validated(MyGroup.class) TestBean bean, BindingResult result) { + public void testBind(@RequestParam @IsoDate Date date, @MyValid TestBean bean, BindingResult result) { this.recordedValidationError = (result.getErrorCount() == 1); } } @@ -885,9 +896,11 @@ public class MvcNamespaceTests { } } - public static class TestCallableProcessingInterceptor extends CallableProcessingInterceptorAdapter { } + public static class TestCallableProcessingInterceptor extends CallableProcessingInterceptorAdapter { + } - public static class TestDeferredResultProcessingInterceptor extends DeferredResultProcessingInterceptorAdapter { } + public static class TestDeferredResultProcessingInterceptor extends DeferredResultProcessingInterceptorAdapter { + } public static class TestPathMatcher implements PathMatcher { @@ -927,9 +940,11 @@ public class MvcNamespaceTests { } } - public static class TestPathHelper extends UrlPathHelper { } + public static class TestPathHelper extends UrlPathHelper { + } public static class TestCacheManager implements CacheManager { + @Override public Cache getCache(String name) { return new ConcurrentMapCache(name); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationControllerTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationControllerTests.java index d0b1bfcdd7..f780690c7b 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationControllerTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationControllerTests.java @@ -2388,7 +2388,12 @@ public class ServletAnnotationControllerTests { @Controller private static class MyTypedCommandProvidingFormController extends MyCommandProvidingFormController { + } + @Validated(MyGroup.class) + @Target({ElementType.PARAMETER}) + @Retention(RetentionPolicy.RUNTIME) + public @interface MyValid { } @Controller @@ -2409,7 +2414,7 @@ public class ServletAnnotationControllerTests { @Override @RequestMapping("/myPath.do") - public String myHandle(@ModelAttribute("myCommand") @Validated(MyGroup.class) TestBean tb, BindingResult errors, ModelMap model) { + public String myHandle(@ModelAttribute("myCommand") @MyValid TestBean tb, BindingResult errors, ModelMap model) { if (!errors.hasFieldErrors("sex")) { throw new IllegalStateException("requiredFields not applied"); } @@ -2712,11 +2717,10 @@ public class ServletAnnotationControllerTests { } } + @Controller @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) - @Controller public @interface MyControllerAnnotation { - } @MyControllerAnnotation