Skip to content

Commit

Permalink
HV-1122 Allowing to configure constraint behavior (isValid()) using L…
Browse files Browse the repository at this point in the history
…ambda expressions in configuration DSL
  • Loading branch information
gunnarmorling committed Oct 24, 2016
1 parent 082f5d6 commit 9210102
Show file tree
Hide file tree
Showing 5 changed files with 173 additions and 1 deletion.
Expand Up @@ -39,4 +39,27 @@ public interface ConstraintDefinitionContext<A extends Annotation> extends Const
* @return This context for method chaining.
*/
ConstraintDefinitionContext<A> validatedBy(Class<? extends ConstraintValidator<A, ?>> validator);

/**
* Allows to configure a validation implementation using a Lambda expression or method reference. Useful for simple
* validations without the need for accessing constraint properties or customization of error messages etc.
*
* @param type The type of the value to validate
* @return This context for method chaining
*/
<T> ConstraintValidatorDefinitionContext<A, T> validateType(Class<T> type);

interface ConstraintValidatorDefinitionContext<A extends Annotation, T> {

/**
* Applies the given Lambda expression or referenced method to values to be validated. It is guaranteed that
* never {@code null} is passed to these expressions or methods.
*/
ConstraintDefinitionContext<A> with(ValidationCallable<T> vc);
}

@FunctionalInterface
interface ValidationCallable<T> {
boolean isValid(T object);
}
}
Expand Up @@ -51,11 +51,32 @@ public ConstraintDefinitionContext<A> validatedBy(Class<? extends ConstraintVali
return this;
}

@Override
public <T> ConstraintDefinitionContext.ConstraintValidatorDefinitionContext<A, T> validateType(Class<T> type) {
return new ConstraintValidatorDefinitionContextImpl<>( type );
}

@SuppressWarnings("unchecked")
ConstraintDefinitionContribution<A> build() {
return new ConstraintDefinitionContribution<>(
annotationType,
CollectionHelper.newArrayList( validatorTypes ),
includeExistingValidators );
}

private class ConstraintValidatorDefinitionContextImpl<T> implements ConstraintDefinitionContext.ConstraintValidatorDefinitionContext<A, T> {

private final Class<T> type;

public ConstraintValidatorDefinitionContextImpl(Class<T> type) {
this.type = type;
}

@Override
public ConstraintDefinitionContext<A> with(ConstraintDefinitionContext.ValidationCallable<T> vc) {
validatorTypes.add( ConstraintValidatorDescriptor.forLambda( annotationType, type, vc ) );

return ConstraintDefinitionContextImpl.this;
}
}
}
Expand Up @@ -15,6 +15,8 @@
import javax.validation.ConstraintValidatorFactory;
import javax.validation.constraintvalidation.ValidationTarget;

import org.hibernate.validator.cfg.context.ConstraintDefinitionContext.ValidationCallable;

/**
* Represents a specific validator type.
*
Expand All @@ -30,4 +32,8 @@ public interface ConstraintValidatorDescriptor<A extends Annotation> extends Ser
static <A extends Annotation> ConstraintValidatorDescriptor<A> forClass(Class<? extends ConstraintValidator<A, ?>> validatorClass) {
return new ClassBasedValidatorDescriptor<>( validatorClass );
}

static <A extends Annotation, T> ConstraintValidatorDescriptor<A> forLambda(Class<A> annotationType, Type validatedType, ValidationCallable<T> lambda) {
return new LambdaBasedValidatorDescriptor<>( validatedType, lambda );
}
}
@@ -0,0 +1,76 @@
/*
* Hibernate Validator, declare and validate application constraints
*
* License: Apache License, Version 2.0
* See the license.txt file in the root directory or <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package org.hibernate.validator.internal.engine.constraintvalidation;

import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.EnumSet;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import javax.validation.ConstraintValidatorFactory;
import javax.validation.constraintvalidation.ValidationTarget;

import org.hibernate.validator.cfg.context.ConstraintDefinitionContext.ValidationCallable;

/**
* Represents a constraint validator based on a Lambda expression.
*
* @author Gunnar Morling
*/
class LambdaBasedValidatorDescriptor<A extends Annotation> implements ConstraintValidatorDescriptor<A> {

private static final long serialVersionUID = 5129757824081595723L;

private final Type validatedType;
private final ValidationCallable<?> lambda;

public LambdaBasedValidatorDescriptor(Type validatedType, ValidationCallable<?> lambda) {
this.validatedType = validatedType;
this.lambda = lambda;
}

@SuppressWarnings("unchecked")
@Override
public Class<? extends ConstraintValidator<A, ?>> getValidatorClass() {
Class<?> clazz = LambdaExecutor.class;
return (Class<? extends ConstraintValidator<A, ?>>) clazz;
}

@Override
public EnumSet<ValidationTarget> getValidationTargets() {
return EnumSet.of( ValidationTarget.ANNOTATED_ELEMENT );
}

@Override
public Type getValidatedType() {
return validatedType;
}

@Override
public ConstraintValidator<A, ?> newInstance(ConstraintValidatorFactory constraintFactory) {
return new LambdaExecutor<>( lambda );
}

private static class LambdaExecutor<A extends Annotation, T> implements ConstraintValidator<A, T> {

private final ValidationCallable<T> lambda;

public LambdaExecutor(ValidationCallable<T> lambda) {
this.lambda = lambda;
}

@Override
public void initialize(A constraintAnnotation) {
}

@Override
public boolean isValid(T value, ConstraintValidatorContext context) {
return lambda.isValid( value );
}
}
}
Expand Up @@ -11,9 +11,11 @@
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import static org.hibernate.validator.testutil.ConstraintViolationAssert.assertCorrectConstraintTypes;
import static org.hibernate.validator.testutil.ConstraintViolationAssert.assertCorrectConstraintViolationMessages;
import static org.hibernate.validator.testutil.ConstraintViolationAssert.assertNumberOfViolations;

import java.io.File;
import java.io.InputStream;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
Expand Down Expand Up @@ -269,6 +271,32 @@ public void testMultipleValidatorsForSameTypeWithNoCallToIncludeExistingValidato
validator.validate( new ConstrainedIntegerFieldBean() );
}

@Test
public void testConstraintBasedOnMethodReference() {
mapping.constraintDefinition( Directory.class )
.validateType( File.class ).with( File::exists );

config.addMapping( mapping );
Validator validator = config.buildValidatorFactory().getValidator();

Set<? extends ConstraintViolation<?>> violations = validator.validate( new MyBean() );
assertNumberOfViolations( violations, 1 );
assertCorrectConstraintTypes( violations, Directory.class );
}

@Test
public void testConstraintBasedOnLambdaExpression() {
mapping.constraintDefinition( Directory.class )
.validateType( File.class ).with( b -> b.exists() );

config.addMapping( mapping );
Validator validator = config.buildValidatorFactory().getValidator();

Set<? extends ConstraintViolation<?>> violations = validator.validate( new MyBean() );
assertNumberOfViolations( violations, 1 );
assertCorrectConstraintTypes( violations, Directory.class );
}

@Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Documented
Expand All @@ -295,7 +323,7 @@ public void testMultipleValidatorsForSameTypeWithNoCallToIncludeExistingValidato
@SuppressWarnings("unchecked")
private static void assertCorrectValidatorTypes(Set<? extends ConstraintViolation<?>> violations,
Class<? /* extends StubValidator */>... validatorClasses) {
List<String> expectedMessages = new ArrayList<String>();
List<String> expectedMessages = new ArrayList<>();
for ( Class<?> validatorClass : validatorClasses ) {
String identifyingMessage = StubValidator.getIdentifyingMessage(
(Class<? extends StubValidator<?>>) validatorClass
Expand Down Expand Up @@ -403,4 +431,22 @@ private static class ConstrainedShortFieldBean {
private Short field;
}

private class MyBean {

@Directory
File someDir = new File( "not existing" );
}

@Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {})
public @interface Directory {
String message() default "";

Class<?>[] groups() default { };

Class<? extends Payload>[] payload() default { };
}

}

0 comments on commit 9210102

Please sign in to comment.