Bean Validator: a custom validator that checks against other field validations

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());
   }
}

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s