Skip to content

Commit

Permalink
Extend BeanValidationContext to ExecutableValidator (#320)
Browse files Browse the repository at this point in the history
add test to override interceptors and customize includes
  • Loading branch information
graemerocher authored Mar 4, 2024
1 parent a8fe07d commit f9d1f9e
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -43,9 +44,13 @@ default List<Class<?>> 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)
);
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -377,17 +377,23 @@ public <T> Set<ConstraintViolation<T>> 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 <T> Set<ConstraintViolation<T>> 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<T> context = new DefaultConstraintValidatorContext<>(this, null, object, BeanValidationContext.fromGroups(groups));
DefaultConstraintValidatorContext<T> 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();
Expand Down Expand Up @@ -468,8 +474,18 @@ public <T> Set<ConstraintViolation<T>> validateReturnValue(@NonNull T object,
@NonNull Class<?>... groups) {
requireNonNull("groups", groups);

return validateReturnValue(
bean,
executableMethod,
returnValue,
BeanValidationContext.fromGroups(groups)
);
}

@Override
public <T> Set<ConstraintViolation<T>> validateReturnValue(T bean, ExecutableMethod<?, Object> executableMethod, Object returnValue, BeanValidationContext validationContext) {
final ReturnType<Object> returnType = executableMethod.getReturnType();
final DefaultConstraintValidatorContext<T> context = new DefaultConstraintValidatorContext<>(this, null, bean, BeanValidationContext.fromGroups(groups));
final DefaultConstraintValidatorContext<T> context = new DefaultConstraintValidatorContext<>(this, null, bean, validationContext);

try (DefaultConstraintValidatorContext.ValidationCloseable ignored1 = context.withExecutableReturnValue(returnValue)) {
try (ValidationPath.ContextualPath ignored2 = context.getCurrentPath().addMethodNode(executableMethod)) {
Expand Down Expand Up @@ -549,12 +565,22 @@ public <T> Set<ConstraintViolation<T>> validateConstructorParameters(Class<? ext
@NonNull Class<?>[] groups) {
requireNonNull("groups", groups);

return validateConstructorParameters(
beanType,
constructorArguments,
parameterValues,
BeanValidationContext.fromGroups(groups)
);
}

@Override
public <T> Set<ConstraintViolation<T>> validateConstructorParameters(Class<? extends T> 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<T> context = (DefaultConstraintValidatorContext<T>) new DefaultConstraintValidatorContext<>(this, null, beanType, BeanValidationContext.fromGroups(groups));
DefaultConstraintValidatorContext<T> context = (DefaultConstraintValidatorContext<T>) 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <T> The object type
* @return The constraint violations.
*/
@NonNull <T> Set<ConstraintViolation<T>> 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
Expand Down Expand Up @@ -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 <T> The object type
* @return A set of contstraint violations
*/
@NonNull <T> Set<ConstraintViolation<T>> validateReturnValue(
@NonNull T object,
@NonNull ExecutableMethod<?, Object> executableMethod,
@Nullable Object returnValue,
@Nullable BeanValidationContext validationContext);

/**
* Validates parameters for the given introspection and values.
* @param introspection The introspection
Expand Down Expand Up @@ -126,6 +157,22 @@ <T> Set<ConstraintViolation<T>> 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 <T> The generic type of the bean
* @return A set of constraint violations, if any
*/
<T> Set<ConstraintViolation<T>> validateConstructorParameters(
@NonNull Class<? extends T> beanType,
@NonNull Argument<?>[] constructorArguments,
@NonNull Object[] parameterValues,
@Nullable BeanValidationContext validationContext
);

@Override
@NonNull <T> Set<ConstraintViolation<T>> validateParameters(@NonNull T object, @NonNull Method method, @NonNull Object[] parameterValues, @Nullable Class<?>... groups);

Expand Down
Original file line number Diff line number Diff line change
@@ -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<Object, Object> 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<Object, Object> 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)
}
}
}

0 comments on commit f9d1f9e

Please sign in to comment.