Skip to content

Commit 4c3e75a

Browse files
committed
Add MethodParameter validation
Validate an individual method parameter when it's annotated with Spring's @validated rather than with @Valid, in which case method validation does not validate the parameter. Closes gh-571
1 parent f65b83f commit 4c3e75a

File tree

2 files changed

+85
-22
lines changed

2 files changed

+85
-22
lines changed

spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/ValidationHelper.java

Lines changed: 59 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -63,31 +63,45 @@ private ValidationHelper(Validator validator) {
6363
@Nullable
6464
public Consumer<Object[]> getValidationHelperFor(HandlerMethod handlerMethod) {
6565

66-
boolean required = false;
67-
Class<?>[] groups = null;
66+
boolean requiresMethodValidation = false;
67+
Class<?>[] methodValidationGroups = null;
6868

6969
Validated validatedAnnotation = findAnnotation(handlerMethod, Validated.class);
7070
if (validatedAnnotation != null) {
71-
required = true;
72-
groups = validatedAnnotation.value();
71+
requiresMethodValidation = true;
72+
methodValidationGroups = validatedAnnotation.value();
7373
}
7474
else if (findAnnotation(handlerMethod, Valid.class) != null) {
75-
required = true;
75+
requiresMethodValidation = true;
7676
}
7777

78-
for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
79-
if (required) {
80-
break;
81-
}
78+
Consumer<Object[]> parameterValidator = null;
79+
MethodParameter[] parameters = handlerMethod.getMethodParameters();
80+
81+
for (int i = 0; i < parameters.length; i++) {
82+
MethodParameter parameter = parameters[i];
8283
for (Annotation annot : parameter.getParameterAnnotations()) {
8384
MergedAnnotations merged = MergedAnnotations.from(annot);
84-
if (merged.isPresent(Valid.class) || merged.isPresent(Constraint.class) || merged.isPresent(Validated.class)) {
85-
required = true;
85+
if (merged.isPresent(Valid.class) || merged.isPresent(Constraint.class)) {
86+
requiresMethodValidation = true;
87+
}
88+
else if (annot.annotationType().equals(Validated.class)) {
89+
Class<?>[] groups = ((Validated) annot).value();
90+
parameterValidator = (parameterValidator != null ?
91+
parameterValidator.andThen(new MethodParameterValidator(i, groups)) :
92+
new MethodParameterValidator(i, groups));
8693
}
8794
}
8895
}
8996

90-
return (required ? new HandlerMethodValidator(handlerMethod, groups) : null);
97+
Consumer<Object[]> result = (requiresMethodValidation ?
98+
new HandlerMethodValidator(handlerMethod, methodValidationGroups) : null);
99+
100+
if (parameterValidator != null) {
101+
return (result != null ? result.andThen(parameterValidator) : parameterValidator);
102+
}
103+
104+
return result;
91105
}
92106

93107
@Nullable
@@ -131,10 +145,9 @@ private class HandlerMethodValidator implements Consumer<Object[]> {
131145

132146
private final HandlerMethod handlerMethod;
133147

134-
@Nullable
135148
private final Class<?>[] validationGroups;
136149

137-
private HandlerMethodValidator(HandlerMethod handlerMethod, @Nullable Class<?>[] validationGroups) {
150+
HandlerMethodValidator(HandlerMethod handlerMethod, @Nullable Class<?>[] validationGroups) {
138151
Assert.notNull(handlerMethod, "HandlerMethod is required");
139152
this.handlerMethod = handlerMethod;
140153
this.validationGroups = (validationGroups != null ? validationGroups : new Class<?>[] {});
@@ -143,12 +156,41 @@ private HandlerMethodValidator(HandlerMethod handlerMethod, @Nullable Class<?>[]
143156
@Override
144157
public void accept(Object[] arguments) {
145158

146-
Set<ConstraintViolation<Object>> result =
159+
Set<ConstraintViolation<Object>> violations =
147160
ValidationHelper.this.validator.forExecutables().validateParameters(
148161
this.handlerMethod.getBean(), this.handlerMethod.getMethod(), arguments, this.validationGroups);
149162

150-
if (!result.isEmpty()) {
151-
throw new ConstraintViolationException(result);
163+
if (!violations.isEmpty()) {
164+
throw new ConstraintViolationException(violations);
165+
}
166+
}
167+
}
168+
169+
170+
/**
171+
* Callback to apply validation to a {@link MethodParameter}, typically
172+
* because it's annotated with Spring's {@code @Validated} rather than with
173+
* {@code @Valid}.
174+
*/
175+
private class MethodParameterValidator implements Consumer<Object[]> {
176+
177+
private final int index;
178+
179+
private final Class<?>[] validationGroups;
180+
181+
MethodParameterValidator(int index, @Nullable Class<?>[] validationGroups) {
182+
this.index = index;
183+
this.validationGroups = (validationGroups != null ? validationGroups : new Class<?>[] {});
184+
}
185+
186+
@Override
187+
public void accept(Object[] arguments) {
188+
189+
Set<ConstraintViolation<Object>> violations =
190+
ValidationHelper.this.validator.validate(arguments[this.index], this.validationGroups);
191+
192+
if (!violations.isEmpty()) {
193+
throw new ConstraintViolationException(violations);
152194
}
153195
}
154196
}

spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/ValidationHelperTests.java

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,12 @@ void shouldIgnoreMethodsWithoutAnnotations() {
6060

6161
@Test
6262
void shouldRaiseValidationErrorForAnnotatedParams() {
63-
Consumer<Object[]> validator = createValidator(MyBean.class, "myValidMethod");
64-
assertViolations(() -> validator.accept(new Object[] {null, 2}))
65-
.anyMatch(violation -> violation.getPropertyPath().toString().equals("myValidMethod.arg0"));
66-
assertViolations(() -> validator.accept(new Object[] {"test", 12}))
67-
.anyMatch(violation -> violation.getPropertyPath().toString().equals("myValidMethod.arg1"));
63+
Consumer<Object[]> validator1 = createValidator(MyBean.class, "myValidMethod");
64+
assertViolation(() -> validator1.accept(new Object[] {null, 2}), "myValidMethod.arg0");
65+
assertViolation(() -> validator1.accept(new Object[] {"test", 12}), "myValidMethod.arg1");
66+
67+
Consumer<Object[]> validator2 = createValidator(MyBean.class, "myValidatedParameterMethod");
68+
assertViolation(() -> validator2.accept(new Object[] {new ConstrainedInput(100)}), "integerValue");
6869
}
6970

7071
@Test
@@ -118,6 +119,23 @@ private void assertViolation(ThrowableAssert.ThrowingCallable callable, String p
118119
}
119120

120121

122+
123+
private static class ConstrainedInput {
124+
125+
@Max(99)
126+
private final int integerValue;
127+
128+
public ConstrainedInput(int i) {
129+
this.integerValue = i;
130+
}
131+
132+
public int getIntegerValue() {
133+
return this.integerValue;
134+
}
135+
136+
}
137+
138+
121139
@SuppressWarnings("unused")
122140
private static class MyBean {
123141

@@ -129,6 +147,9 @@ public Object myValidMethod(@NotNull String arg0, @Max(10) int arg1) {
129147
return null;
130148
}
131149

150+
public Object myValidatedParameterMethod(@Validated ConstrainedInput input) {
151+
return null;
152+
}
132153
}
133154

134155

0 commit comments

Comments
 (0)