Browse Source

Allow custom @Validated annotations for handler method parameters

Issue: SPR-12406
pull/701/head
Juergen Hoeller 10 years ago
parent
commit
929cda6790
  1. 29
      spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/PayloadArgumentResolver.java
  2. 22
      spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/PayloadArgumentResolverTests.java
  3. 20
      spring-web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvoker.java
  4. 9
      spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java
  5. 11
      spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartMethodArgumentResolver.java
  6. 9
      spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java
  7. 35
      spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java
  8. 10
      spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationControllerTests.java

29
spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/PayloadArgumentResolver.java

@ -34,14 +34,15 @@ import org.springframework.validation.BindingResult; @@ -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.
*
* <p>This {@link HandlerMethodArgumentResolver} should be ordered last as it supports all
* types and does not require the {@link Payload} annotation.
* <p>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 { @@ -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 { @@ -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 { @@ -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 { @@ -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;
}
}

22
spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/PayloadArgumentResolverTests.java

@ -16,6 +16,10 @@ @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 {
}
}

20
spring-web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvoker.java

@ -1,5 +1,5 @@ @@ -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; @@ -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; @@ -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.
*
* <p>Used by {@link org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter} and {@link
* org.springframework.web.portlet.mvc.annotation.AnnotationMethodHandlerAdapter}.
* <p>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 { @@ -295,10 +296,13 @@ public class HandlerMethodInvoker {
else if (Value.class.isInstance(paramAnn)) {
defaultValue = ((Value) paramAnn).value();
}
else if (paramAnn.annotationType().getSimpleName().startsWith("Valid")) {
else {
Validated validatedAnn = AnnotationUtils.getAnnotation(paramAnn, Validated.class);
if (validatedAnn != null || paramAnn.annotationType().getSimpleName().startsWith("Valid")) {
validate = true;
Object value = AnnotationUtils.getValue(paramAnn);
validationHints = (value instanceof Object[] ? (Object[]) value : new Object[] {value});
Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(paramAnn));
validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[]{hints});
}
}
}

9
spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java

@ -27,6 +27,7 @@ import org.springframework.core.MethodParameter; @@ -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 @@ -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;
}
}

11
spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartMethodArgumentResolver.java

@ -31,6 +31,7 @@ import org.springframework.http.converter.HttpMessageConverter; @@ -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 @@ -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)) {

9
spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java

@ -33,6 +33,7 @@ import org.springframework.http.converter.HttpMessageNotReadableException; @@ -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 @@ -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)) {

35
spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java

@ -16,8 +16,10 @@ @@ -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 { @@ -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 { @@ -187,7 +190,7 @@ public class MvcNamespaceTests {
List<HttpMessageConverter<?>> 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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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);

10
spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationControllerTests.java

@ -2388,7 +2388,12 @@ public class ServletAnnotationControllerTests { @@ -2388,7 +2388,12 @@ public class ServletAnnotationControllerTests {
@Controller
private static class MyTypedCommandProvidingFormController
extends MyCommandProvidingFormController<Integer, TestBean, ITestBean> {
}
@Validated(MyGroup.class)
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyValid {
}
@Controller
@ -2409,7 +2414,7 @@ public class ServletAnnotationControllerTests { @@ -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 { @@ -2712,11 +2717,10 @@ public class ServletAnnotationControllerTests {
}
}
@Controller
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Controller
public @interface MyControllerAnnotation {
}
@MyControllerAnnotation

Loading…
Cancel
Save