diff --git a/validation/src/main/java/io/micronaut/validation/validator/BeanValidationContext.java b/validation/src/main/java/io/micronaut/validation/validator/BeanValidationContext.java index cd9c54a8..af0d0ac8 100644 --- a/validation/src/main/java/io/micronaut/validation/validator/BeanValidationContext.java +++ b/validation/src/main/java/io/micronaut/validation/validator/BeanValidationContext.java @@ -17,6 +17,7 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.beans.BeanProperty; +import io.micronaut.core.util.ArrayUtils; import java.util.Arrays; import java.util.List; @@ -43,9 +44,13 @@ default List> groups() { * @return The context */ static @NonNull BeanValidationContext fromGroups(Class... groups) { - return new DefaultBeanValidationContext( - groups != null ? Arrays.asList(groups) : List.of() - ); + if (ArrayUtils.isEmpty(groups)) { + return DEFAULT; + } else { + return new DefaultBeanValidationContext( + Arrays.asList(groups) + ); + } } /** diff --git a/validation/src/main/java/io/micronaut/validation/validator/DefaultValidator.java b/validation/src/main/java/io/micronaut/validation/validator/DefaultValidator.java index c208e08c..8141e639 100644 --- a/validation/src/main/java/io/micronaut/validation/validator/DefaultValidator.java +++ b/validation/src/main/java/io/micronaut/validation/validator/DefaultValidator.java @@ -377,17 +377,23 @@ public Set> validateParameters(@NonNull T object, @NonNull ExecutableMethod method, @NonNull Object[] parameterValues, @NonNull Class... groups) { + requireNonNull("groups", groups); + return validateParameters(object, method, parameterValues, BeanValidationContext.fromGroups(groups)); + } + + @Override + public Set> validateParameters(T object, ExecutableMethod method, @NonNull Object[] parameterValues, BeanValidationContext validationContext) { requireNonNull("parameterValues", parameterValues); requireNonNull("object", object); requireNonNull("method", method); - requireNonNull("groups", groups); + requireNonNull("context", validationContext); final Argument[] arguments = method.getArguments(); final int argLen = arguments.length; if (argLen != parameterValues.length) { throw new IllegalArgumentException("The method parameter array must have exactly " + argLen + " elements."); } - DefaultConstraintValidatorContext context = new DefaultConstraintValidatorContext<>(this, null, object, BeanValidationContext.fromGroups(groups)); + DefaultConstraintValidatorContext context = new DefaultConstraintValidatorContext<>(this, null, object, validationContext); try (DefaultConstraintValidatorContext.ValidationCloseable ignored1 = context.withExecutableParameterValues(parameterValues)) { try (ValidationPath.ContextualPath ignored = context.getCurrentPath().addMethodNode(method)) { AnnotationMetadata methodAnnotationMetadata = method.getAnnotationMetadata().getDeclaredMetadata(); @@ -468,8 +474,18 @@ public Set> validateReturnValue(@NonNull T object, @NonNull Class... groups) { requireNonNull("groups", groups); + return validateReturnValue( + bean, + executableMethod, + returnValue, + BeanValidationContext.fromGroups(groups) + ); + } + + @Override + public Set> validateReturnValue(T bean, ExecutableMethod executableMethod, Object returnValue, BeanValidationContext validationContext) { final ReturnType returnType = executableMethod.getReturnType(); - final DefaultConstraintValidatorContext context = new DefaultConstraintValidatorContext<>(this, null, bean, BeanValidationContext.fromGroups(groups)); + final DefaultConstraintValidatorContext context = new DefaultConstraintValidatorContext<>(this, null, bean, validationContext); try (DefaultConstraintValidatorContext.ValidationCloseable ignored1 = context.withExecutableReturnValue(returnValue)) { try (ValidationPath.ContextualPath ignored2 = context.getCurrentPath().addMethodNode(executableMethod)) { @@ -549,12 +565,22 @@ public Set> validateConstructorParameters(Class[] groups) { requireNonNull("groups", groups); + return validateConstructorParameters( + beanType, + constructorArguments, + parameterValues, + BeanValidationContext.fromGroups(groups) + ); + } + + @Override + public Set> validateConstructorParameters(Class beanType, @NonNull Argument[] constructorArguments, @NonNull Object[] parameterValues, BeanValidationContext validationContext) { parameterValues = parameterValues != null ? parameterValues : ArrayUtils.EMPTY_OBJECT_ARRAY; final int argLength = constructorArguments.length; if (parameterValues.length != argLength) { throw new IllegalArgumentException("Expected exactly [" + argLength + "] constructor arguments"); } - DefaultConstraintValidatorContext context = (DefaultConstraintValidatorContext) new DefaultConstraintValidatorContext<>(this, null, beanType, BeanValidationContext.fromGroups(groups)); + DefaultConstraintValidatorContext context = (DefaultConstraintValidatorContext) new DefaultConstraintValidatorContext<>(this, null, beanType, validationContext); try (DefaultConstraintValidatorContext.ValidationCloseable ignored1 = context.withExecutableParameterValues(parameterValues)) { try (ValidationPath.ContextualPath ignored = context.getCurrentPath().addConstructorNode(beanType.getSimpleName(), constructorArguments)) { validateParametersInternal(context, null, AnnotationMetadata.EMPTY_METADATA, parameterValues, constructorArguments, argLength); diff --git a/validation/src/main/java/io/micronaut/validation/validator/ExecutableMethodValidator.java b/validation/src/main/java/io/micronaut/validation/validator/ExecutableMethodValidator.java index 45cc2188..8ee004e2 100644 --- a/validation/src/main/java/io/micronaut/validation/validator/ExecutableMethodValidator.java +++ b/validation/src/main/java/io/micronaut/validation/validator/ExecutableMethodValidator.java @@ -66,6 +66,21 @@ public interface ExecutableMethodValidator extends ExecutableValidator { @NonNull Object[] parameterValues, @Nullable Class... groups); + /** + * Validate the parameter values of the given {@link ExecutableMethod}. + * @param object The object + * @param method The method + * @param parameterValues The values + * @param context The context + * @param The object type + * @return The constraint violations. + */ + @NonNull Set> validateParameters( + @NonNull T object, + @NonNull ExecutableMethod method, + @NonNull Object[] parameterValues, + @Nullable BeanValidationContext context); + /** * Validate the parameter values of the given {@link ExecutableMethod}. * @param object The object @@ -96,6 +111,22 @@ public interface ExecutableMethodValidator extends ExecutableValidator { @Nullable Object returnValue, @Nullable Class... groups); + + /** + * Validates the return value of a {@link ExecutableMethod}. + * @param object The object + * @param executableMethod The method + * @param returnValue The return value + * @param validationContext The validation context + * @param The object type + * @return A set of contstraint violations + */ + @NonNull Set> validateReturnValue( + @NonNull T object, + @NonNull ExecutableMethod executableMethod, + @Nullable Object returnValue, + @Nullable BeanValidationContext validationContext); + /** * Validates parameters for the given introspection and values. * @param introspection The introspection @@ -126,6 +157,22 @@ Set> validateConstructorParameters( @Nullable Class[] groups ); + /** + * Validates arguments for the given bean type and constructor arguments. + * @param beanType The bean type + * @param constructorArguments The constructor arguments + * @param parameterValues The parameter values + * @param validationContext The validation context + * @param The generic type of the bean + * @return A set of constraint violations, if any + */ + Set> validateConstructorParameters( + @NonNull Class beanType, + @NonNull Argument[] constructorArguments, + @NonNull Object[] parameterValues, + @Nullable BeanValidationContext validationContext + ); + @Override @NonNull Set> validateParameters(@NonNull T object, @NonNull Method method, @NonNull Object[] parameterValues, @Nullable Class... groups); diff --git a/validation/src/test/groovy/io/micronaut/validation/validator/replacement/ReplaceInterceptorSpec.groovy b/validation/src/test/groovy/io/micronaut/validation/validator/replacement/ReplaceInterceptorSpec.groovy new file mode 100644 index 00000000..5da9fdf4 --- /dev/null +++ b/validation/src/test/groovy/io/micronaut/validation/validator/replacement/ReplaceInterceptorSpec.groovy @@ -0,0 +1,77 @@ +package io.micronaut.validation.validator.replacement + +import io.micronaut.aop.InterceptorBean +import io.micronaut.aop.MethodInvocationContext +import io.micronaut.context.annotation.Replaces +import io.micronaut.core.beans.BeanProperty +import io.micronaut.core.convert.ConversionService +import io.micronaut.test.annotation.MockBean +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import io.micronaut.validation.Pojo +import io.micronaut.validation.Validated +import io.micronaut.validation.ValidatingInterceptor +import io.micronaut.validation.validator.BeanValidationContext +import io.micronaut.validation.validator.Validator +import jakarta.inject.Inject +import jakarta.inject.Singleton +import jakarta.validation.ConstraintViolation +import jakarta.validation.ConstraintViolationException +import jakarta.validation.Valid +import jakarta.validation.ValidatorFactory +import spock.lang.Specification + +@MicronautTest +class ReplaceInterceptorSpec extends Specification { + + @Inject TestService testService + + void "test replacement interceptor"() { + when: + testService.validatePojo(new Pojo(email: "junk", name: "")) + + then: + def e = thrown(ConstraintViolationException) + e.message == 'validatePojo.pojo.email: Email should be valid' + } + + @MockBean(ValidatingInterceptor) + @InterceptorBean(Validated) + static class MyInterceptor extends ValidatingInterceptor { + Validator micronautValidator + MyInterceptor(Validator micronautValidator, ValidatorFactory validatorFactory, ConversionService conversionService) { + super(micronautValidator, validatorFactory, conversionService) + this.micronautValidator = micronautValidator; + } + + @Override + Object intercept(MethodInvocationContext context) { + if (context.parameterValues[0] instanceof Pojo) { + def constraintViolations = micronautValidator.forExecutables() + .validateParameters( + context.parameterValues[0], + context.executableMethod, + context.parameterValues, + new BeanValidationContext() { + boolean isPropertyValidated(Object object, BeanProperty property) { + return property.name == 'email' + } + } + ) + if (constraintViolations) { + throw new ConstraintViolationException(constraintViolations) + } + return context.proceed() + } else { + return super.intercept(context) + } + } + } + + @Singleton + @Validated + static class TestService { + void validatePojo(@Valid Pojo pojo) { + println(pojo) + } + } +}