diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/ValidationHelper.java b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/ValidationHelper.java index 31cea7e3..a0f5d06d 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/ValidationHelper.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/ValidationHelper.java @@ -63,31 +63,45 @@ class ValidationHelper { @Nullable public Consumer getValidationHelperFor(HandlerMethod handlerMethod) { - boolean required = false; - Class[] groups = null; + boolean requiresMethodValidation = false; + Class[] methodValidationGroups = null; Validated validatedAnnotation = findAnnotation(handlerMethod, Validated.class); if (validatedAnnotation != null) { - required = true; - groups = validatedAnnotation.value(); + requiresMethodValidation = true; + methodValidationGroups = validatedAnnotation.value(); } else if (findAnnotation(handlerMethod, Valid.class) != null) { - required = true; + requiresMethodValidation = true; } - for (MethodParameter parameter : handlerMethod.getMethodParameters()) { - if (required) { - break; - } + Consumer parameterValidator = null; + MethodParameter[] parameters = handlerMethod.getMethodParameters(); + + for (int i = 0; i < parameters.length; i++) { + MethodParameter parameter = parameters[i]; for (Annotation annot : parameter.getParameterAnnotations()) { MergedAnnotations merged = MergedAnnotations.from(annot); - if (merged.isPresent(Valid.class) || merged.isPresent(Constraint.class) || merged.isPresent(Validated.class)) { - required = true; + if (merged.isPresent(Valid.class) || merged.isPresent(Constraint.class)) { + requiresMethodValidation = true; + } + else if (annot.annotationType().equals(Validated.class)) { + Class[] groups = ((Validated) annot).value(); + parameterValidator = (parameterValidator != null ? + parameterValidator.andThen(new MethodParameterValidator(i, groups)) : + new MethodParameterValidator(i, groups)); } } } - return (required ? new HandlerMethodValidator(handlerMethod, groups) : null); + Consumer result = (requiresMethodValidation ? + new HandlerMethodValidator(handlerMethod, methodValidationGroups) : null); + + if (parameterValidator != null) { + return (result != null ? result.andThen(parameterValidator) : parameterValidator); + } + + return result; } @Nullable @@ -131,10 +145,9 @@ class ValidationHelper { private final HandlerMethod handlerMethod; - @Nullable private final Class[] validationGroups; - private HandlerMethodValidator(HandlerMethod handlerMethod, @Nullable Class[] validationGroups) { + HandlerMethodValidator(HandlerMethod handlerMethod, @Nullable Class[] validationGroups) { Assert.notNull(handlerMethod, "HandlerMethod is required"); this.handlerMethod = handlerMethod; this.validationGroups = (validationGroups != null ? validationGroups : new Class[] {}); @@ -143,12 +156,41 @@ class ValidationHelper { @Override public void accept(Object[] arguments) { - Set> result = + Set> violations = ValidationHelper.this.validator.forExecutables().validateParameters( this.handlerMethod.getBean(), this.handlerMethod.getMethod(), arguments, this.validationGroups); - if (!result.isEmpty()) { - throw new ConstraintViolationException(result); + if (!violations.isEmpty()) { + throw new ConstraintViolationException(violations); + } + } + } + + + /** + * Callback to apply validation to a {@link MethodParameter}, typically + * because it's annotated with Spring's {@code @Validated} rather than with + * {@code @Valid}. + */ + private class MethodParameterValidator implements Consumer { + + private final int index; + + private final Class[] validationGroups; + + MethodParameterValidator(int index, @Nullable Class[] validationGroups) { + this.index = index; + this.validationGroups = (validationGroups != null ? validationGroups : new Class[] {}); + } + + @Override + public void accept(Object[] arguments) { + + Set> violations = + ValidationHelper.this.validator.validate(arguments[this.index], this.validationGroups); + + if (!violations.isEmpty()) { + throw new ConstraintViolationException(violations); } } } diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/ValidationHelperTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/ValidationHelperTests.java index 195b59a5..dd841e9e 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/ValidationHelperTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/ValidationHelperTests.java @@ -60,11 +60,12 @@ class ValidationHelperTests { @Test void shouldRaiseValidationErrorForAnnotatedParams() { - Consumer validator = createValidator(MyBean.class, "myValidMethod"); - assertViolations(() -> validator.accept(new Object[] {null, 2})) - .anyMatch(violation -> violation.getPropertyPath().toString().equals("myValidMethod.arg0")); - assertViolations(() -> validator.accept(new Object[] {"test", 12})) - .anyMatch(violation -> violation.getPropertyPath().toString().equals("myValidMethod.arg1")); + Consumer validator1 = createValidator(MyBean.class, "myValidMethod"); + assertViolation(() -> validator1.accept(new Object[] {null, 2}), "myValidMethod.arg0"); + assertViolation(() -> validator1.accept(new Object[] {"test", 12}), "myValidMethod.arg1"); + + Consumer validator2 = createValidator(MyBean.class, "myValidatedParameterMethod"); + assertViolation(() -> validator2.accept(new Object[] {new ConstrainedInput(100)}), "integerValue"); } @Test @@ -118,6 +119,23 @@ class ValidationHelperTests { } + + private static class ConstrainedInput { + + @Max(99) + private final int integerValue; + + public ConstrainedInput(int i) { + this.integerValue = i; + } + + public int getIntegerValue() { + return this.integerValue; + } + + } + + @SuppressWarnings("unused") private static class MyBean { @@ -129,6 +147,9 @@ class ValidationHelperTests { return null; } + public Object myValidatedParameterMethod(@Validated ConstrainedInput input) { + return null; + } }