Bez wątpienia walidacja danych biznesowych wprowadzanych przez użytkownika jest niezwykle istotną sprawą i do tego nikogo przekonywać nie trzeba (; Jednak sposób przeprowadzenia takiej walidacji czasami może okazać się dość kłopotliwy, szczególnie jeśli walidację chcemy zaimplementować w wielu warstwach (np. po wprowadzeniu danych przez użytkownika i przed utrwaleniem ich w bazie danych). Z pomocą w rozwiązaniu tego problemu przychodzi specyfikacja JSR 303 i jej RI Hibernate Validator.

Mimo istniejących już adnotacji dostarczonych przez hibernate validator, które doskonale zaspokajają większość standardowych wymagań dotyczących walidacji czasami pojawia się potrzeba przeprowadzenia specyficznej walidacji. W moim przypadku ta specyficzna walidacja polegała na sprawdzaniu zależności pomiędzy atrybutami pewnej klasy.

Otóż chodziło o wymaganie

attr1, attr2 ... <= attrMax

, gdzie każda wartość dostarczana jest przez użytkownika. Stosowanie standardowych adnotacji niestety w tym przypadku się nie sprawdza.

Własna adnotacja JSR 303

Każda adnotacja musi zawierać atrybuty message, groups i payload. Zgodnie z dokumentacją hibernate validator ostatni atrybut nie jest wykorzystywany przez standardowy mechanizm walidacji a jedynie przez dodatkowe narzędzia. Atrybut groups przypisuje daną walidację do grupy dzięki czemu uzyskujemy możliwość przeprowadzania różnych zestawów walidacji na obiekcie. Atrybut message jest komunikatem wyświetlanym w przypadku obiektu nie przechodzącego walidacji.

Trzeba tu zwrócić uwagę na znaki { }, ich dokładne działanie wyjaśnione jest w paragrafie 4.3 specyfikacji jsr (upraszczając, wszystko co znajduje się między tymi znakami jest wyszukiwane w message beanach, dzięki czemu w bardzo łatwy sposób uzyskujemy internacjonalizację naszych komunikatów).

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = FieldsLowerOrEqualValidator.class)
@Documented
public @interface FieldsLowerOrEqual {
    String message() default "{javax.validation.constraints.FieldsLowerOrEqual.message}";
    Class[] groups() default {};
    Class[] payload() default {};
    String maxField();
    String[] lowerOrEqualFields();
}

Niezwykle istotna jest także adnotacja @Constraint, która wskazuje na właściwą klasę reprezentującą walidator.

public class FieldsLowerOrEqualValidator
        implements ConstraintValidator<FieldsLowerOrEqual, Object> {

    private String maxField;

    private String[] lowerOrEqualFields;

    @Override
    public void initialize(FieldsLowerOrEqual constraintAnnotation) {
        this.maxField = constraintAnnotation.maxField();
        this.lowerOrEqualFields = constraintAnnotation.lowerOrEqualFields();
    }

    @Override
    @SuppressWarnings("unchecked")
    public boolean isValid(Object instance, ConstraintValidatorContext context) {

        try {
            Comparable maxValue = (Comparable) PropertyUtils.getProperty(instance, maxField);
            Comparable tempValue = null;

            for(String fieldName : lowerOrEqualFields) {
                tempValue = (Comparable) PropertyUtils.getProperty(instance, fieldName);
                if(tempValue != null && tempValue.compareTo(maxValue) == 1) {
                    return false;
                }
            }
        }
        catch (Exception e) {
            return false;
        }

        return true;
    }

}

Poniżej test prezentujący właściwe działanie adnotacji.

public class FieldsLowerOrEqualValidatorTest {

    @FieldsLowerOrEqual(maxField="max", lowerOrEqualFields= {"field1", "field2"})
    public static class LowerOrEqualTestClass {

        @Getter
        private final Integer max;
        @Getter
        private final Integer field1;
        @Getter
        private final Integer field2;

        public LowerOrEqualTestClass(Integer max, Integer field1, Integer field2) {
            this.max = max;
            this.field1 = field1;
            this.field2 = field2;
        }
    }


    private Validator validator;

    @Before
    public void setUp() {
        ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
        validator = validatorFactory.getValidator();
    }

    @Test
    public void shouldFindOneValidationError() {
        Set<ConstraintViolation<LowerOrEqualTestClass>> violations =
        validator.validate(new LowerOrEqualTestClass(10, 11, 12));

        assertThat(violations, is(notNullValue()));
        assertThat(violations.size(), is(1));
    }

    @Test
    public void shouldntFindAnyValidationErrors() {
        Set<ConstraintViolation<LowerOrEqualTestClass>> violations =
        validator.validate(new LowerOrEqualTestClass(10, 10, 9));

        assertThat(violations, is(notNullValue()));
        assertThat(violations.isEmpty(), is(true));
    }

}