From 3e29e71a6e437cef93753b6c4615dfe42f0443c7 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Fri, 10 Dec 2021 15:50:16 +0100 Subject: [PATCH] Add support for Bean Validation This commit adds support for validating handler method inputs after binding from the data fetching environment and before invoking the handler method. This support is based on Bean Validation and is enabled only if the application context contains a `Validator` bean. Closes gh-110 --- build.gradle | 2 + graphql-spring-boot-starter/build.gradle | 4 + .../GraphQlServiceAutoConfigurationTests.java | 20 +++ .../src/docs/asciidoc/index.adoc | 46 ++++++ spring-graphql/build.gradle | 5 + .../AnnotatedControllerConfigurer.java | 45 +++++- .../support/DataFetcherHandlerMethod.java | 26 +++- .../support/HandlerMethodInputValidator.java | 98 ++++++++++++ .../HandlerMethodInputValidatorTests.java | 139 ++++++++++++++++++ 9 files changed, 377 insertions(+), 8 deletions(-) create mode 100644 spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/HandlerMethodInputValidator.java create mode 100644 spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/HandlerMethodInputValidatorTests.java diff --git a/build.gradle b/build.gradle index 44d0f1b9..fdf567ab 100644 --- a/build.gradle +++ b/build.gradle @@ -76,7 +76,9 @@ configure(moduleProjects) { dependency "com.jayway.jsonpath:json-path:2.5.0" dependency "org.skyscreamer:jsonassert:1.5.0" dependency "com.h2database:h2:1.4.200" + dependency "javax.validation:validation-api:2.0.1.Final" dependency "org.hibernate:hibernate-core:5.6.1.Final" + dependency "org.hibernate.validator:hibernate-validator:6.2.0.Final" dependencySet(group: 'org.mongodb', version: '4.3.2') { entry 'mongodb-driver-sync' entry 'mongodb-driver-reactivestreams' diff --git a/graphql-spring-boot-starter/build.gradle b/graphql-spring-boot-starter/build.gradle index 95443ffe..a8eae817 100644 --- a/graphql-spring-boot-starter/build.gradle +++ b/graphql-spring-boot-starter/build.gradle @@ -34,6 +34,7 @@ dependencies { compileOnly 'org.springframework:spring-webmvc' compileOnly 'org.springframework:spring-websocket' compileOnly 'javax.servlet:javax.servlet-api' + compileOnly 'javax.validation:validation-api' compileOnly 'javax.websocket:javax.websocket-api' compileOnly 'io.micrometer:micrometer-core' @@ -62,7 +63,10 @@ dependencies { testImplementation 'io.projectreactor:reactor-test' testImplementation 'io.projectreactor.netty:reactor-netty' testImplementation 'javax.servlet:javax.servlet-api' + testImplementation 'javax.validation:validation-api' + testImplementation 'org.hibernate.validator:hibernate-validator' testImplementation 'org.apache.tomcat.embed:tomcat-embed-core' + testImplementation 'org.apache.tomcat.embed:tomcat-embed-el' testImplementation 'org.apache.tomcat.embed:tomcat-embed-websocket' testImplementation 'org.springframework.boot:spring-boot-actuator-autoconfigure' testImplementation 'io.micrometer:micrometer-core' diff --git a/graphql-spring-boot-starter/src/test/java/org/springframework/graphql/boot/GraphQlServiceAutoConfigurationTests.java b/graphql-spring-boot-starter/src/test/java/org/springframework/graphql/boot/GraphQlServiceAutoConfigurationTests.java index 55210606..46c1f98e 100644 --- a/graphql-spring-boot-starter/src/test/java/org/springframework/graphql/boot/GraphQlServiceAutoConfigurationTests.java +++ b/graphql-spring-boot-starter/src/test/java/org/springframework/graphql/boot/GraphQlServiceAutoConfigurationTests.java @@ -26,6 +26,7 @@ import org.springframework.graphql.GraphQlService; import org.springframework.graphql.data.method.annotation.support.AnnotatedControllerConfigurer; import org.springframework.graphql.execution.BatchLoaderRegistry; import org.springframework.graphql.execution.GraphQlSource; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -50,6 +51,16 @@ class GraphQlServiceAutoConfigurationTests { }); } + @Test + void shouldConfigureValidation() { + this.contextRunner.withUserConfiguration(ValidationConfiguration.class) + .run((context) -> { + assertThat(context).hasSingleBean(AnnotatedControllerConfigurer.class); + assertThat(context.getBean(AnnotatedControllerConfigurer.class)) + .extracting("validator").isNotNull(); + }); + } + @Configuration(proxyBeanMethods = false) static class GraphQlSourceConfiguration { @@ -59,4 +70,13 @@ class GraphQlServiceAutoConfigurationTests { } } + @Configuration(proxyBeanMethods = false) + static class ValidationConfiguration { + + @Bean + public LocalValidatorFactoryBean defaultValidator() { + return new LocalValidatorFactoryBean(); + } + } + } \ No newline at end of file diff --git a/spring-graphql-docs/src/docs/asciidoc/index.adoc b/spring-graphql-docs/src/docs/asciidoc/index.adoc index fd12569b..f5da2a73 100644 --- a/spring-graphql-docs/src/docs/asciidoc/index.adoc +++ b/spring-graphql-docs/src/docs/asciidoc/index.adoc @@ -821,6 +821,52 @@ You can use `@Argument` on a `Map` argument, to obtain all argum values. The name attribute on `@Argument` must not be set. +[[controllers-schema-mapping-validation]] +==== `@Argument` validation + +If a {spring-framework-ref-docs}/core.html#validation-beanvalidation-overview[Bean Validation] +`Validator` (or typically, a `LocalValidatorFactoryBean`) bean is present in the application context, +the `AnnotatedControllerConfigurer` will auto-detect it and configure support for validation. +Controller arguments annotated with `@Valid` and `@Validated` are then validated before method invocation. + +Bean Validation lets you declare constraints on types, as the following example shows: + +[source,java,indent=0,subs="verbatim,quotes"] +---- +public class BookInput { + + @NotNull + private String title; + + @NotNull + @Size(max=13) + private String isbn; +} +---- + +We can then mark our argument for validation with `@Valid`: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Controller + public class BookController { + + @MutationMapping + public Book addBook(@Argument @Valid BookInput bookInput) { + // ... + } + } +---- + +If an error occurs during validation, a `ConstraintViolationException` is thrown and can be +later <>. + +[TIP] +==== +Unlike Spring MVC, handler method signatures do not support the injection of `BindingResult` +for reacting to validation errors: those are globally dealt with as exceptions. +==== + [[controllers-schema-mapping-projectedpayload-argument]] ==== `@ProjectPayload` Interface diff --git a/spring-graphql/build.gradle b/spring-graphql/build.gradle index 7fc42d69..6686c461 100644 --- a/spring-graphql/build.gradle +++ b/spring-graphql/build.gradle @@ -22,6 +22,8 @@ dependencies { compileOnly 'org.jetbrains.kotlinx:kotlinx-coroutines-core' compileOnly 'org.jetbrains.kotlin:kotlin-stdlib' + compileOnly 'javax.validation:validation-api' + testImplementation 'org.junit.jupiter:junit-jupiter' testImplementation 'org.assertj:assertj-core' testImplementation 'org.mockito:mockito-core' @@ -35,6 +37,7 @@ dependencies { testImplementation 'org.springframework.data:spring-data-jpa' testImplementation 'com.h2database:h2' testImplementation 'org.hibernate:hibernate-core' + testImplementation 'org.hibernate.validator:hibernate-validator' testImplementation 'org.springframework.data:spring-data-mongodb' testImplementation 'org.mongodb:mongodb-driver-sync' testImplementation 'org.mongodb:mongodb-driver-reactivestreams' @@ -44,8 +47,10 @@ dependencies { testImplementation 'com.querydsl:querydsl-core' testImplementation 'com.querydsl:querydsl-collections' testImplementation 'javax.servlet:javax.servlet-api' + testImplementation 'javax.validation:validation-api' testImplementation 'com.jayway.jsonpath:json-path' testImplementation 'com.fasterxml.jackson.core:jackson-databind' + testImplementation 'org.apache.tomcat.embed:tomcat-embed-el:9.0.55' testRuntimeOnly 'org.apache.logging.log4j:log4j-core' testRuntimeOnly 'org.apache.logging.log4j:log4j-slf4j-impl' diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/AnnotatedControllerConfigurer.java b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/AnnotatedControllerConfigurer.java index 610e971b..1fc23f17 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/AnnotatedControllerConfigurer.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/AnnotatedControllerConfigurer.java @@ -27,6 +27,8 @@ import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import javax.validation.Validator; + import graphql.schema.DataFetcher; import graphql.schema.DataFetchingEnvironment; import graphql.schema.FieldCoordinates; @@ -66,6 +68,7 @@ import org.springframework.util.StringUtils; * registers them as {@link DataFetcher}s. * * @author Rossen Stoyanchev + * @author Brian Clozel * @since 1.0.0 */ public class AnnotatedControllerConfigurer @@ -93,6 +96,9 @@ public class AnnotatedControllerConfigurer "org.springframework.security.core.context.SecurityContext", AnnotatedControllerConfigurer.class.getClassLoader()); + private final static boolean beanValidationPresent = ClassUtils.isPresent( + "javax.validation.executable.ExecutableValidator", + AnnotatedControllerConfigurer.class.getClassLoader()); @Nullable private ApplicationContext applicationContext; @@ -100,6 +106,9 @@ public class AnnotatedControllerConfigurer @Nullable private HandlerMethodArgumentResolverComposite argumentResolvers; + @Nullable + private HandlerMethodInputValidator validator; + @Nullable private ConversionService conversionService; @@ -109,6 +118,9 @@ public class AnnotatedControllerConfigurer this.applicationContext = applicationContext; } + /** + * Configure the {@link ConversionService} used for binding handler arguments. + */ public void setConversionService(ConversionService conversionService) { this.conversionService = conversionService; } @@ -145,6 +157,10 @@ public class AnnotatedControllerConfigurer // This works as a fallback, after other resolvers this.argumentResolvers.addResolver(new SourceMethodArgumentResolver()); + + if (beanValidationPresent) { + this.validator = HandlerMethodInputValidatorFactory.create(obtainApplicationContext()); + } } @Override @@ -154,7 +170,7 @@ public class AnnotatedControllerConfigurer findHandlerMethods().forEach((info) -> { DataFetcher dataFetcher; if (!info.isBatchMapping()) { - dataFetcher = new SchemaMappingDataFetcher(info, this.argumentResolvers); + dataFetcher = new SchemaMappingDataFetcher(info, this.argumentResolvers, this.validator); } else { String dataLoaderKey = registerBatchLoader(info); @@ -281,7 +297,7 @@ public class AnnotatedControllerConfigurer } Assert.hasText(typeName, - "No parentType specified, and a source/parent method argument was also not found: " + + "No parentType specified, and a source/parent method argument was also not found: " + handlerMethod.getShortLogMessage()); return new MappingInfo(typeName, field, batchMapping, handlerMethod); @@ -304,7 +320,7 @@ public class AnnotatedControllerConfigurer String methodParameters = Arrays.stream(method.getGenericParameterTypes()) .map(Type::getTypeName) .collect(Collectors.joining(",", "(", ")")); - return mappingInfo.getCoordinates() + " => " + method.getName() + methodParameters; + return mappingInfo.getCoordinates() + " => " + method.getName() + methodParameters; }) .collect(Collectors.joining("\n\t", "\n\t" + formattedType + ":" + "\n\t", "")); } @@ -378,11 +394,16 @@ public class AnnotatedControllerConfigurer private final HandlerMethodArgumentResolverComposite argumentResolvers; + @Nullable + private final HandlerMethodInputValidator validator; + private final boolean subscription; - public SchemaMappingDataFetcher(MappingInfo info, HandlerMethodArgumentResolverComposite resolvers) { + public SchemaMappingDataFetcher(MappingInfo info, HandlerMethodArgumentResolverComposite resolvers, + @Nullable HandlerMethodInputValidator validator) { this.info = info; this.argumentResolvers = resolvers; + this.validator = validator; this.subscription = this.info.getCoordinates().getTypeName().equalsIgnoreCase("Subscription"); } @@ -404,7 +425,7 @@ public class AnnotatedControllerConfigurer @Override @SuppressWarnings("ConstantConditions") public Object get(DataFetchingEnvironment environment) throws Exception { - return new DataFetcherHandlerMethod(getHandlerMethod(), this.argumentResolvers, this.subscription).invoke(environment); + return new DataFetcherHandlerMethod(getHandlerMethod(), this.argumentResolvers, this.validator, this.subscription).invoke(environment); } } @@ -420,11 +441,23 @@ public class AnnotatedControllerConfigurer @Override public Object get(DataFetchingEnvironment env) { DataLoader dataLoader = env.getDataLoaderRegistry().getDataLoader(this.dataLoaderKey); - if (dataLoader == null) { + if (dataLoader == null) { throw new IllegalStateException("No DataLoader for key '" + this.dataLoaderKey + "'"); } return dataLoader.load(env.getSource()); } } + /** + * 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; + } + } + } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/DataFetcherHandlerMethod.java b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/DataFetcherHandlerMethod.java index e3457934..3195c979 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/DataFetcherHandlerMethod.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/DataFetcherHandlerMethod.java @@ -48,17 +48,29 @@ public class DataFetcherHandlerMethod extends InvocableHandlerMethodSupport { private final HandlerMethodArgumentResolverComposite resolvers; + @Nullable + private final HandlerMethodInputValidator validator; + private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); private final boolean subscription; - public DataFetcherHandlerMethod( - HandlerMethod handlerMethod, HandlerMethodArgumentResolverComposite resolvers, boolean subscription) { + /** + * Constructor with a parent handler method. + * @param handlerMethod the handler method + * @param resolvers the argument resolvers + * @param validator the input validator + * @param subscription whether the field being fetched is of subscription type + */ + public DataFetcherHandlerMethod(HandlerMethod handlerMethod, + HandlerMethodArgumentResolverComposite resolvers, @Nullable HandlerMethodInputValidator validator, + boolean subscription) { super(handlerMethod); Assert.isTrue(!resolvers.getResolvers().isEmpty(), "No argument resolvers"); this.resolvers = resolvers; + this.validator = validator; this.subscription = subscription; } @@ -70,6 +82,13 @@ public class DataFetcherHandlerMethod extends InvocableHandlerMethodSupport { return this.resolvers; } + /** + * Return the configured input validator. + */ + @Nullable + public HandlerMethodInputValidator getValidator() { + return this.validator; + } /** * Invoke the method after resolving its argument values in the context of @@ -93,6 +112,9 @@ public class DataFetcherHandlerMethod extends InvocableHandlerMethodSupport { Object[] args; try { args = getMethodArgumentValues(environment); + if (this.validator != null) { + this.validator.validate(this, args); + } } catch (Throwable ex) { return Mono.error(ex); diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/HandlerMethodInputValidator.java b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/HandlerMethodInputValidator.java new file mode 100644 index 00000000..226df823 --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/HandlerMethodInputValidator.java @@ -0,0 +1,98 @@ +/* + * 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 javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import javax.validation.Validation; +import javax.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 query + * @param arguments the resolved arguments for the method invocation + */ + public void validate(HandlerMethod handlerMethod, Object[] arguments) { + Class[] validationGroups = determineValidationGroups(handlerMethod); + Set> 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. + *

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]); + } +} diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/HandlerMethodInputValidatorTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/HandlerMethodInputValidatorTests.java new file mode 100644 index 00000000..b7c6955e --- /dev/null +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/HandlerMethodInputValidatorTests.java @@ -0,0 +1,139 @@ +/* + * 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.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Method; +import java.util.Arrays; + +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import javax.validation.constraints.Max; +import javax.validation.constraints.NotNull; + +import org.assertj.core.api.InstanceOfAssertFactories; +import org.assertj.core.api.IterableAssert; +import org.assertj.core.api.ThrowableAssert; +import org.junit.jupiter.api.Test; + +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.assertThatNoException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Tests for {@link HandlerMethodInputValidator} + * @author Brian Clozel + */ +class HandlerMethodInputValidatorTests { + + private final HandlerMethodInputValidator validator = new HandlerMethodInputValidator(); + + @Test + void shouldFailWithNullValidator() { + assertThatThrownBy(() -> new HandlerMethodInputValidator(null)).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void shouldIgnoreMethodsWithoutAnnotations() throws Exception { + HandlerMethod method = findHandlerMethod(MyValidBean.class, "notValidatedMethod"); + assertThatNoException().isThrownBy(() -> validator.validate(method, new Object[] {"test", 12})); + } + + @Test + void shouldRaiseValidationErrorForAnnotatedParams() throws Exception { + HandlerMethod method = findHandlerMethod(MyValidBean.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})) + .anyMatch(violation -> violation.getPropertyPath().toString().equals("myValidMethod.arg1")); + } + + @Test + void shouldRaiseValidationErrorForAnnotatedParamsWithGroups() throws Exception { + HandlerMethod myValidMethodWithGroup = findHandlerMethod(MyValidBeanWithGroup.class, "myValidMethodWithGroup"); + assertViolations(() -> validator.validate(myValidMethodWithGroup, new Object[] {null})) + .anyMatch(violation -> violation.getPropertyPath().toString().equals("myValidMethodWithGroup.arg0")); + HandlerMethod myValidMethodWithGroupOnType = findHandlerMethod(MyValidBeanWithGroup.class, "myValidMethodWithGroupOnType"); + assertViolations(() -> validator.validate(myValidMethodWithGroupOnType, new Object[] {null})) + .anyMatch(violation -> violation.getPropertyPath().toString().equals("myValidMethodWithGroupOnType.arg0")); + } + + + private HandlerMethod findHandlerMethod(Class handlerType, String methodName) { + Object handler = BeanUtils.instantiateClass(handlerType); + Method method = Arrays.stream(handlerType.getMethods()) + .filter(m -> m.getName().equals(methodName)) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("Invalid method name")); + return new HandlerMethod(handler, method); + } + + private IterableAssert assertViolations(ThrowableAssert.ThrowingCallable callable) { + return assertThatThrownBy(callable) + .isInstanceOf(ConstraintViolationException.class) + .extracting("constraintViolations") + .asInstanceOf(InstanceOfAssertFactories.iterable(ConstraintViolation.class)); + } + + public static class MyValidBean { + + public String notValidatedMethod(String arg0, int arg1) { + return ""; + } + + public Object myValidMethod(@NotNull String arg0, @Max(10) int arg1) { + return null; + } + + } + + public interface FirstGroup { + } + + + public interface SecondGroup { + } + + @Validated(FirstGroup.class) + @Retention(RetentionPolicy.RUNTIME) + public @interface GroupOnParam { + } + + @Validated(SecondGroup.class) + @Retention(RetentionPolicy.RUNTIME) + public @interface GroupOnType { + } + + @GroupOnType + public static class MyValidBeanWithGroup { + + @GroupOnParam + public Object myValidMethodWithGroup(@NotNull(groups = {FirstGroup.class}) String arg0) { + return null; + } + + public Object myValidMethodWithGroupOnType(@NotNull(groups = {SecondGroup.class}) String arg0) { + return null; + } + + } + +} \ No newline at end of file