Sometimes I just want to apply to a property exactly the same Bean Validator validations I apply to another property.
Point in case: I have a password
property in a User
class, and there is a ChangePassword
data transfer object somewhere else that happens to have currentPassword
, newPassword
and repeatPassword
fields, used precisely to change User.password
.
It is obvious that I will want to apply the validations for User.property
to all these fields. And, of course, I want changes to validation rules in the original property to be taken into account, automatically.
Copying them is a no-no, because we will violate the DRY (Don’ Repeat Yourself) rule, and creating a custom validation annotation, say @Password
, might be overkill. And it would not convey the right meaning, or that’s how I see it.
Creating some kind of DelegateValidation
validator that replicates other validations makes a lot of sense, as it clearly conveys the intention and keeps code DRY.
Well, here is the code for the validation annotation itself:
@Target( { ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE }) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = DelegateValidationValidator.class) @Documented public @interface DelegateValidation { Class<?> clazz(); String property(); Class<? extends Payload>[] payload() default {}; String message() default ""; Class<?>[] groups() default {}; }
Here clazz
and property
are the class and property whose validations we will replicate to validate our own property.
For example, if we have a ValidatedClass
like this one,
public static class ValidatedClass { public static final String OTHER_PROPERTY= "otherProperty"; @Length( min = 3) @NotNull @Size(min = 4) public String otherProperty; @NotNull public String getSecondProperty() { return "hi"; } }
we can apply the validations in that class otherProperty
and secondProperty
(corresponding to the getSecondProperty
getter) to other properties in our MyClass
class as follows:
public static class MyClass { @DelegateValidation(clazz=ValidatedClass.class, property=ValidatedClass.OTHER_PROPERTY) public String value; @DelegateValidation( clazz=ValidatedClass.class, property="secondProperty") public String value; }
Here is the validator implementation:
public class DelegateValidationValidator implements ConstraintValidator<DelegateValidation, Object> { private static transient Validator globalValidator; public static void setGlobalValidator(Validator v ) { assert v != null; assert globalValidator == null; globalValidator = v; } @Inject private transient javax.validation.Validator validator; private Validator getValidator() { if( this.validator == null ) { assert globalValidator != null: "A validator has not been injected: please, supply one via " + "setGlobalValidator"; return globalValidator; } return this.validator; } private Class<Object> referencedClass; private String property; // To make happy very demanding compiler with regard to generic parameters @SuppressWarnings("unchecked") @Override public void initialize(DelegateValidation constraintAnnotation) { // To make happy very demanding compiler with regard to generic parameters this.referencedClass = (Class<Object>)constraintAnnotation.clazz(); this.property = constraintAnnotation.property(); } @Override public boolean isValid(Object value, ConstraintValidatorContext context) { Set<ConstraintViolation<Object>> constraints = getValidator().validateValue(this.referencedClass, this.property, value); if( constraints.isEmpty() ) { return true; } context.disableDefaultConstraintViolation(); for( ConstraintViolation<Object> c : constraints) { context.buildConstraintViolationWithTemplate( c.getMessage()).addConstraintViolation(); } return false; } }
Here we must get access to the Validator
we are using to peform validations to ask it to perform validations against the tracked property. Since I use CDI, as well as the Seam validtor module, I rely on them to inject the right Validator instance in my validator implementation.
However, if you are not using CDI/Seam, or just to make testing easier, I am providing a way to supply a validator via the setGlobalValidator
method, that you should call before any validations are performed.
Last but not least, let me tellyou that, if you provide a wrong property name for the validator, the validation will fail with a ValidationException
, as attested by some of the following tests.
The tests
For the sake of completeness, here are my tests, written against TestNg:
public class DelegateValidationTest { public static class ValidatedClass { public static final String OTHER_PROPERTY= "otherProperty"; @Length( min = 3) @NotNull @Size(min = 4) public String otherProperty; @NotNull public String getSecondProperty() { return "hi"; } private Integer propertyWithNoValidations; } private Validator validator; @BeforeClass void classSetUp() { if( this.validator == null ) { this.validator = Validation.buildDefaultValidatorFactory().getValidator(); DelegateValidationValidator.setGlobalValidator(this.validator); } } public static class MyClass { @DelegateValidation(clazz=ValidatedClass.class, property=ValidatedClass.OTHER_PROPERTY) public String value; } @Test public void testValueValidation() { Assert.assertEquals( this.validator.validateValue( MyClass.class, "value", null).size(), 1); Assert.assertEquals( this.validator.validateValue( MyClass.class, "value", "ab").size(), 2); Assert.assertEquals( this.validator.validateValue( MyClass.class, "value", "abc").size(), 1); Assert.assertTrue( this.validator.validateValue( MyClass.class, "value", "abcd").isEmpty()); } @Test public void testWholeBeanValidation() { ConstraintViolation<MyClass> firstV; ConstraintViolation<MyClass> secondV; Iterator<ConstraintViolation<MyClass>> it; MyClass mc = new MyClass(); mc.value = "ab"; Set<ConstraintViolation<MyClass>> violations = this.validator.validate(mc); Assert.assertEquals( violations.size(), 2); it = violations.iterator(); firstV = it.next(); secondV = it.next(); // We expect MyClass, even if validations are run // against ValidatedClass Assert.assertEquals( firstV.getRootBeanClass(), MyClass.class); // We expect "value", even if validations are run // against "otherProperty" Assert.assertEquals( firstV.getPropertyPath().toString(), "value"); // We expect MyClass, even if validations are run // against ValidatedClass Assert.assertEquals( secondV.getRootBeanClass(), MyClass.class); // We expect "value", even if validations are run // against "otherProperty" Assert.assertEquals( secondV.getPropertyPath().toString(), "value"); } public static class MyClassWithWrongDelegationProperty { @DelegateValidation(clazz=ValidatedClass.class, property="wrongPropertyName") public String value; } @Test(expectedExceptions={ValidationException.class}) public void testReferencePropertyDoesNotExist() { this.validator.validateValue( MyClassWithWrongDelegationProperty.class, "value", "ab"); } public static class MyClassTrackingGetter { @DelegateValidation( clazz=ValidatedClass.class, property="secondProperty") public String value; } @Test public void testWorksWithGetters() { Assert.assertEquals( this.validator.validateValue( MyClassTrackingGetter.class, "value", null).size(), 1); Assert.assertTrue( this.validator.validateValue( MyClassTrackingGetter.class, "value", "a").isEmpty()); } public static class MyClassTrackingPropertyWithNoValidations { @DelegateValidation( clazz=ValidatedClass.class, property="propertyWithNoValidations") public Integer value; } @Test public void testWorksWithPropertyHavingNoValidations() { Assert.assertTrue( this.validator.validateValue( MyClassTrackingPropertyWithNoValidations.class, "value", null). isEmpty()); } }