Merge branch '1.0.x'

This commit is contained in:
rstoyanchev
2022-09-19 17:07:54 +01:00
5 changed files with 262 additions and 146 deletions

View File

@@ -30,8 +30,6 @@ import java.util.concurrent.Executor;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import jakarta.validation.Validator;
import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import graphql.schema.FieldCoordinates;
@@ -127,7 +125,7 @@ public class AnnotatedControllerConfigurer
private HandlerMethodArgumentResolverComposite argumentResolvers;
@Nullable
private HandlerMethodInputValidator validator;
private HandlerMethodValidationHelper validationHelper;
@Nullable
private Consumer<DataBinder> dataBinderInitializer;
@@ -175,7 +173,8 @@ public class AnnotatedControllerConfigurer
this.argumentResolvers = initArgumentResolvers();
if (beanValidationPresent) {
this.validator = HandlerMethodInputValidatorFactory.create(obtainApplicationContext());
this.validationHelper =
HandlerMethodValidationHelper.createIfValidatorAvailable(obtainApplicationContext());
}
}
@@ -229,7 +228,7 @@ public class AnnotatedControllerConfigurer
findHandlerMethods().forEach((info) -> {
DataFetcher<?> dataFetcher;
if (!info.isBatchMapping()) {
dataFetcher = new SchemaMappingDataFetcher(info, this.argumentResolvers, this.validator, this.executor);
dataFetcher = new SchemaMappingDataFetcher(info, this.argumentResolvers, this.validationHelper, this.executor);
}
else {
String dataLoaderKey = registerBatchLoader(info);
@@ -477,21 +476,21 @@ public class AnnotatedControllerConfigurer
private final HandlerMethodArgumentResolverComposite argumentResolvers;
@Nullable
private final HandlerMethodInputValidator validator;
private final HandlerMethodValidationHelper validatorHelper;
@Nullable
private final Executor executor;
private final boolean subscription;
public SchemaMappingDataFetcher(
SchemaMappingDataFetcher(
MappingInfo info, HandlerMethodArgumentResolverComposite resolvers,
@Nullable HandlerMethodInputValidator validator,
@Nullable HandlerMethodValidationHelper validatorHelper,
@Nullable Executor executor) {
this.info = info;
this.argumentResolvers = resolvers;
this.validator = validator;
this.validatorHelper = validatorHelper;
this.executor = executor;
this.subscription = this.info.getCoordinates().getTypeName().equalsIgnoreCase("Subscription");
}
@@ -509,7 +508,7 @@ public class AnnotatedControllerConfigurer
public Object get(DataFetchingEnvironment environment) throws Exception {
DataFetcherHandlerMethod handlerMethod = new DataFetcherHandlerMethod(
getHandlerMethod(), this.argumentResolvers, this.validator, this.executor, this.subscription);
getHandlerMethod(), this.argumentResolvers, this.validatorHelper, this.executor, this.subscription);
return handlerMethod.invoke(environment);
}
@@ -520,7 +519,7 @@ public class AnnotatedControllerConfigurer
private final String dataLoaderKey;
public BatchMappingDataFetcher(String dataLoaderKey) {
BatchMappingDataFetcher(String dataLoaderKey) {
this.dataLoaderKey = dataLoaderKey;
}
@@ -534,16 +533,4 @@ public class AnnotatedControllerConfigurer
}
}
/**
* Look for a Validator bean in the context and configure validation support
*/
static class HandlerMethodInputValidatorFactory {
@Nullable
static HandlerMethodInputValidator create(ApplicationContext context) {
Validator validator = context.getBeanProvider(Validator.class).getIfAvailable();
return validator != null ? new HandlerMethodInputValidator(validator) : null;
}
}
}

View File

@@ -50,7 +50,7 @@ public class DataFetcherHandlerMethod extends InvocableHandlerMethodSupport {
private final HandlerMethodArgumentResolverComposite resolvers;
@Nullable
private final HandlerMethodInputValidator validator;
private final HandlerMethodValidationHelper validator;
private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
@@ -65,13 +65,13 @@ public class DataFetcherHandlerMethod extends InvocableHandlerMethodSupport {
* @param subscription whether the field being fetched is of subscription type
*/
public DataFetcherHandlerMethod(HandlerMethod handlerMethod,
HandlerMethodArgumentResolverComposite resolvers, @Nullable HandlerMethodInputValidator validator,
HandlerMethodArgumentResolverComposite resolvers, @Nullable HandlerMethodValidationHelper validator,
@Nullable Executor executor, boolean subscription) {
super(handlerMethod, executor);
Assert.isTrue(!resolvers.getResolvers().isEmpty(), "No argument resolvers");
this.resolvers = resolvers;
this.validator = validator;
this.validator = (validator != null && validator.requiresValidation(handlerMethod) ? validator : null);
this.subscription = subscription;
}
@@ -85,12 +85,15 @@ public class DataFetcherHandlerMethod extends InvocableHandlerMethodSupport {
/**
* Return the configured input validator.
* @deprecated as of 1.1 without a replacement
*/
@Deprecated
@Nullable
public HandlerMethodInputValidator getValidator() {
public HandlerMethodValidationHelper getValidator() {
return this.validator;
}
/**
* Invoke the method after resolving its argument values in the context of
* the given {@link DataFetchingEnvironment}.

View File

@@ -1,98 +0,0 @@
/*
* Copyright 2020-2021 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
*
* https://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.graphql.data.method.annotation.support;
import java.util.Set;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.graphql.data.method.HandlerMethod;
import org.springframework.graphql.data.method.HandlerMethodArgumentResolver;
import org.springframework.util.Assert;
import org.springframework.validation.annotation.Validated;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.validation.beanvalidation.SpringValidatorAdapter;
/**
* Strategy for validating a {@link HandlerMethod} input before invocation, based on JSR-303.
* This is called after all {@link HandlerMethodArgumentResolver} have been involved.
*
* @author Brian Clozel
*/
class HandlerMethodInputValidator {
private final Validator validator;
/**
* Create the input validator backed by a JSR-303 Validator instance.
*/
public HandlerMethodInputValidator(Validator validator) {
Assert.notNull(validator, "validator should not be null");
if (validator instanceof LocalValidatorFactoryBean) {
this.validator = ((LocalValidatorFactoryBean) validator).getValidator();
}
else if (validator instanceof SpringValidatorAdapter) {
this.validator = validator.unwrap(Validator.class);
}
else {
this.validator = validator;
}
}
/**
* Create the input validator backed by a default
* {@link Validation#buildDefaultValidatorFactory() factory instance}.
*/
public HandlerMethodInputValidator() {
this(Validation.buildDefaultValidatorFactory().getValidator());
}
/**
* Validate the {@link HandlerMethod} input before invocation, throwing
* an {@link ConstraintViolationException} if validation fails.
*
* @param handlerMethod the handler method for the current request
* @param arguments the resolved arguments for the method invocation
*/
public void validate(HandlerMethod handlerMethod, Object[] arguments) {
Class<?>[] validationGroups = determineValidationGroups(handlerMethod);
Set<ConstraintViolation<Object>> result = this.validator.forExecutables()
.validateParameters(handlerMethod.getBean(), handlerMethod.getMethod(), arguments, validationGroups);
if (!result.isEmpty()) {
throw new ConstraintViolationException(result);
}
}
/**
* Determine the validation groups to validate against for the given handler method.
* <p>Default are the validation groups as specified in the {@link Validated} annotation
* on the containing target class of the method.
* @param method the current HandlerMethod
* @return the applicable validation groups as a Class array
*/
private Class<?>[] determineValidationGroups(HandlerMethod method) {
Validated validatedAnn = AnnotationUtils.findAnnotation(method.getMethod(), Validated.class);
if (validatedAnn == null) {
validatedAnn = AnnotationUtils.findAnnotation(method.getBeanType(), Validated.class);
}
return (validatedAnn != null ? validatedAnn.value() : new Class<?>[0]);
}
}

View File

@@ -0,0 +1,146 @@
/*
* Copyright 2020-2022 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
*
* https://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.graphql.data.method.annotation.support;
import java.lang.annotation.Annotation;
import java.util.Set;
import jakarta.validation.Constraint;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import jakarta.validation.Valid;
import jakarta.validation.Validator;
import jakarta.validation.metadata.BeanDescriptor;
import org.springframework.context.ApplicationContext;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.graphql.data.method.HandlerMethod;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.validation.annotation.Validated;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.validation.beanvalidation.SpringValidatorAdapter;
/**
* Helper class to apply standard bean validation to a {@link HandlerMethod}.
*
* @author Brian Clozel
* @author Rossen Stoyanchev
* @since 1.0
*/
final class HandlerMethodValidationHelper {
private final Validator validator;
/**
* Constructor with the {@link Validator} instance to use.
*/
public HandlerMethodValidationHelper(Validator validator) {
Assert.notNull(validator, "validator should not be null");
if (validator instanceof LocalValidatorFactoryBean) {
this.validator = ((LocalValidatorFactoryBean) validator).getValidator();
}
else if (validator instanceof SpringValidatorAdapter) {
this.validator = validator.unwrap(Validator.class);
}
else {
this.validator = validator;
}
}
/**
* Validate the input values to a the {@link HandlerMethod} and throw a
* {@link ConstraintViolationException} in case of violations.
* @param handlerMethod the handler method to validate
* @param arguments the input argument values
*/
public void validate(HandlerMethod handlerMethod, Object[] arguments) {
Set<ConstraintViolation<Object>> result =
this.validator.forExecutables().validateParameters(
handlerMethod.getBean(), handlerMethod.getMethod(), arguments,
determineValidationGroups(handlerMethod));
if (!result.isEmpty()) {
throw new ConstraintViolationException(result);
}
}
/**
* Determine the validation groups to apply to a handler method, specified
* through the {@link Validated} annotation on the method or on the class.
* @param method the method to check
* @return the applicable validation groups as a Class array
*/
private Class<?>[] determineValidationGroups(HandlerMethod method) {
Validated annotation = findAnnotation(method, Validated.class);
return (annotation != null ? annotation.value() : new Class<?>[0]);
}
@Nullable
private static <A extends Annotation> A findAnnotation(HandlerMethod method, Class<A> annotationType) {
A annotation = AnnotationUtils.findAnnotation(method.getMethod(), annotationType);
if (annotation == null) {
annotation = AnnotationUtils.findAnnotation(method.getBeanType(), annotationType);
}
return annotation;
}
/**
* Whether the given method requires standard bean validation. This is the
* case if the method or one of its parameters are annotated with
* {@link Valid} or {@link Validated}, or if any method parameter is declared
* with a {@link Constraint constraint}, or the method parameter type is
* itself {@link BeanDescriptor#isBeanConstrained() constrained}.
* @param method the handler method to check
* @return {@code true} if validation is required, {@code false} otherwise
*/
public boolean requiresValidation(HandlerMethod method) {
if (findAnnotation(method, Validated.class) != null || findAnnotation(method, Valid.class) != null) {
return true;
}
for (MethodParameter parameter : method.getMethodParameters()) {
for (Annotation annotation : parameter.getParameterAnnotations()) {
MergedAnnotations merged = MergedAnnotations.from(annotation);
if (merged.isPresent(Valid.class) || merged.isPresent(Constraint.class) || merged.isPresent(Validated.class)) {
return true;
}
}
Class<?> paramType = parameter.nestedIfOptional().getNestedParameterType();
if (this.validator.getConstraintsForClass(paramType).isBeanConstrained()) {
return true;
}
}
return false;
}
/**
* Factory method for {@link HandlerMethodValidationHelper} if a
* {@link Validator} can be found.
* @param context the context to look up a {@code Validator} bean from
* @return the helper instance, or {@code null
*/
@Nullable
public static HandlerMethodValidationHelper createIfValidatorAvailable(ApplicationContext context) {
Validator validator = context.getBeanProvider(Validator.class).getIfAvailable();
return (validator != null ? new HandlerMethodValidationHelper(validator) : null);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2021 the original author or authors.
* Copyright 2020-2022 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.
@@ -20,9 +20,12 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Optional;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import jakarta.validation.Valid;
import jakarta.validation.Validation;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.NotNull;
@@ -35,31 +38,34 @@ import org.springframework.beans.BeanUtils;
import org.springframework.graphql.data.method.HandlerMethod;
import org.springframework.validation.annotation.Validated;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* Tests for {@link HandlerMethodInputValidator}
* Unit tests for {@link HandlerMethodValidationHelper}.
* @author Brian Clozel
*/
class HandlerMethodInputValidatorTests {
class HandlerMethodValidationHelperTests {
private final HandlerMethodValidationHelper validator =
new HandlerMethodValidationHelper(Validation.buildDefaultValidatorFactory().getValidator());
private final HandlerMethodInputValidator validator = new HandlerMethodInputValidator();
@Test
void shouldFailWithNullValidator() {
assertThatThrownBy(() -> new HandlerMethodInputValidator(null)).isInstanceOf(IllegalArgumentException.class);
assertThatThrownBy(() -> new HandlerMethodValidationHelper(null)).isInstanceOf(IllegalArgumentException.class);
}
@Test
void shouldIgnoreMethodsWithoutAnnotations() throws Exception {
HandlerMethod method = findHandlerMethod(MyValidBean.class, "notValidatedMethod");
void shouldIgnoreMethodsWithoutAnnotations() {
HandlerMethod method = findHandlerMethod(MyBean.class, "notValidatedMethod");
assertThatNoException().isThrownBy(() -> validator.validate(method, new Object[] {"test", 12}));
}
@Test
void shouldRaiseValidationErrorForAnnotatedParams() throws Exception {
HandlerMethod method = findHandlerMethod(MyValidBean.class, "myValidMethod");
void shouldRaiseValidationErrorForAnnotatedParams() {
HandlerMethod method = findHandlerMethod(MyBean.class, "myValidMethod");
assertViolations(() -> validator.validate(method, new Object[] {null, 2}))
.anyMatch(violation -> violation.getPropertyPath().toString().equals("myValidMethod.arg0"));
assertViolations(() -> validator.validate(method, new Object[] {"test", 12}))
@@ -67,15 +73,37 @@ class HandlerMethodInputValidatorTests {
}
@Test
void shouldRaiseValidationErrorForAnnotatedParamsWithGroups() throws Exception {
HandlerMethod myValidMethodWithGroup = findHandlerMethod(MyValidBeanWithGroup.class, "myValidMethodWithGroup");
void shouldRaiseValidationErrorForAnnotatedParamsWithGroups() {
HandlerMethod myValidMethodWithGroup = findHandlerMethod(MyValidationGroupsBean.class, "myValidMethodWithGroup");
assertViolations(() -> validator.validate(myValidMethodWithGroup, new Object[] {null}))
.anyMatch(violation -> violation.getPropertyPath().toString().equals("myValidMethodWithGroup.arg0"));
HandlerMethod myValidMethodWithGroupOnType = findHandlerMethod(MyValidBeanWithGroup.class, "myValidMethodWithGroupOnType");
HandlerMethod myValidMethodWithGroupOnType = findHandlerMethod(MyValidationGroupsBean.class, "myValidMethodWithGroupOnType");
assertViolations(() -> validator.validate(myValidMethodWithGroupOnType, new Object[] {null}))
.anyMatch(violation -> violation.getPropertyPath().toString().equals("myValidMethodWithGroupOnType.arg0"));
}
@Test
void shouldRecognizeMethodsThatRequireValidation() {
HandlerMethod method = findHandlerMethod(RequiresValidationBean.class, "processConstrainedValue");
assertThat(validator.requiresValidation(method)).isTrue();
method = findHandlerMethod(RequiresValidationBean.class, "processValidInput");
assertThat(validator.requiresValidation(method)).isTrue();
method = findHandlerMethod(RequiresValidationBean.class, "processValidatedInput");
assertThat(validator.requiresValidation(method)).isTrue();
method = findHandlerMethod(RequiresValidationBean.class, "processInputWithConstrainedValue");
assertThat(validator.requiresValidation(method)).isTrue();
method = findHandlerMethod(RequiresValidationBean.class, "processOptionalInputWithConstrainedValue");
assertThat(validator.requiresValidation(method)).isTrue();
method = findHandlerMethod(RequiresValidationBean.class, "processValue");
assertThat(validator.requiresValidation(method)).isFalse();
}
private HandlerMethod findHandlerMethod(Class<?> handlerType, String methodName) {
Object handler = BeanUtils.instantiateClass(handlerType);
@@ -93,7 +121,9 @@ class HandlerMethodInputValidatorTests {
.asInstanceOf(InstanceOfAssertFactories.iterable(ConstraintViolation.class));
}
public static class MyValidBean {
@SuppressWarnings("unused")
private static class MyBean {
public String notValidatedMethod(String arg0, int arg1) {
return "";
@@ -105,27 +135,32 @@ class HandlerMethodInputValidatorTests {
}
public interface FirstGroup {
interface FirstGroup {
}
public interface SecondGroup {
interface SecondGroup {
}
@Validated(FirstGroup.class)
@Retention(RetentionPolicy.RUNTIME)
public @interface GroupOnParam {
@interface MethodLevelGroup {
}
@Validated(SecondGroup.class)
@Retention(RetentionPolicy.RUNTIME)
public @interface GroupOnType {
@interface TypeLevelGroup {
}
@GroupOnType
public static class MyValidBeanWithGroup {
@GroupOnParam
@TypeLevelGroup
@SuppressWarnings("unused")
private static class MyValidationGroupsBean {
@MethodLevelGroup
public Object myValidMethodWithGroup(@NotNull(groups = {FirstGroup.class}) String arg0) {
return null;
}
@@ -136,4 +171,47 @@ class HandlerMethodInputValidatorTests {
}
}
@SuppressWarnings("unused")
private static class RequiresValidationBean {
public void processConstrainedValue(@Max(99) int i) {
}
public void processValidInput(@Valid MyInput input) {
}
public void processValidatedInput(@Validated MyInput input) {
}
public void processInputWithConstrainedValue(MyConstrainedInput input) {
}
public void processOptionalInputWithConstrainedValue(Optional<MyConstrainedInput> input) {
}
public void processValue(int i) {
}
}
private static class MyInput {
}
private static class MyConstrainedInput {
@Max(99)
private int i;
public int getI() {
return this.i;
}
public void setI(int i) {
this.i = i;
}
}
}