piątek, 3 lutego 2012

Przekazywanie adnotacji do adnotacji.

Adnotacje mają pewne swoje ograniczenia. Pierwsze z nich to fakt, że danej adnotacji do danego targetu możemy użyć tylko raz.

Bazując na przykładzie z poprzedniego posta, jeśli w klasie Entity, mamy 2 pary pól do identycznej walidacji, np:
public class Entity {

    private Date from;
    private Date to;
   
    private Date activeFrom;
    private Date activeTo;
}

To użycie tej samej adnotacji walidującej dwa razy jest niedozwolone.
@CheckDates(dateFrom="from", dateTo="to")
@CheckDates(dateFrom="activeFrom", dateTo="activeTo")
public class Entity {

    private Date from;
    private Date to;
    
    private Date activeFrom;
    private Date activeTo;
}

Rozwiązaniem tego problemu jest wykorzystanie kolejnego ograniczenia adnotacji, mówięcego, że parametrem adnotacji może być tylko typ prymitywny, String, Class, enum, inna adnotacja, lub tablica 1-wymiarowa tablica wymienionych wcześniej klas.

Implementacja czegoś takiego mogłba by być następująca:
@Target({ TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = MultiCheckDatesValidator.class)
public @interface MultiCheckDates {

    String message() default "{pl.costam.MultiCheckDates}";

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

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

    CheckDates[] value();
}

I sam walidator, który tak de facto w pętli wywołuje walidator z poprzedniego posta (musimy jedynie dopisać konstruktor):
public class MultiCheckDatesValidator implements ConstraintValidator<MultiCheckDates, Object> {

    private CheckDates[] checkDates;


    @Override
    public void initialize(MultiCheckDates constraintAnnotation) {

        checkDates = constraintAnnotation.value();
    }

    @Override
    public boolean isValid(Object object, ConstraintValidatorContext context) {

        boolean isValid = true;

        for (CheckDates checkDate : checkDates) {

            /**
             * tworzymy i wywołujemy walidator dla pojedyńczej pary from-to
             */
            if (!new CheckDatesValidator(checkDate).isValid(object, context)) {

                isValid = false;
            }
        }

        return isValid;
    }
}

I samo wywołanie:
@MultiCheckDates({
        @CheckDates(dateFrom = "from", dateTo = "to"),
        @CheckDates(dateFrom = "activeFrom", dateTo = "activeTo") })
public class Entity {

Za piękne może to i nie jest, ale warto mieć świadomość limitów wykorzystania adnotacji. Prawdopodobnie lepszym rozwiązaniem byłoby stworzenie osobnej klasy DateRange, jak zasugerował to Michał Gruca w swoim komentarzu.
Choć czasami zastajemy kod taki a nie inny i refaktor bywa bardzo bolesny.

Hibernate Validator (JSR 303) + mechanizm refleksji = walidacja absolutna

Przygarnięcie przez JavaEE projektu Hibernate Validator pod numerem JSR 303 było wg. mnie kolejnym dobrym krokiem w standaryzacji dobry rozwiązań z projektów opensourcowych.

Jednak po jakimś czasie używania (jakże przyjemnego) standardowych walidatorów, doszedłem do wniosku, że to za mało. Napisanie kilku własnych, które swoją drogą również tworzy się bardzo prosto, tylko na chwilę zaspokoiło moje potrzeby. Dopiero połączenie JSR 303 i mechanizmu refleksji w javie, daje maksymalne możliwości wykorzystania tego standardu.

Załóżmy, że mamy klasę która posiada dwie daty: od i do.
public class Entity {

    private Date from;
    private Date to;
}

Chcielibyśmy sprawdzić czy podane daty są po kolei, tj 'from' <= 'to'. Możemy napisać własny walidator dla klasy Entity, ale w projekcie takich klas z datami możemy mieć mnóstwo i dla każdej z nich należałoby stworzyć oddzielny walidator. Dzięki refleksją w javie możemy zrobić jeden uniwersalny, który jako parametry przyjmowałby 2 wartości, nazwa pola 'from', nazwa pola 'to'.
@Target({ TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = CheckDatesValidator.class)
public @interface CheckDates {

    String message() default "{pl.costam.CheckDates}";

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

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

    String dateFrom();

    String dateTo();

}

I sam walidator:
public class CheckDatesValidator implements ConstraintValidator<CheckDates, Object> {

    private String dateFromFieldName;
    private String dateToFieldName;
    private String message;

    @Override
    public void initialize(CheckDates checkDates) {
        
        dateFromFieldName = checkDates.dateFrom();
        dateToFieldName = checkDates.dateTo();
        message = checkDates.message();
    }

    @Override
    public boolean isValid(Object object, ConstraintValidatorContext context) {

        boolean result = validateDates(object);

        if (!result) {
            
            //jeśli walidacja nie powiodła sie to tworzymy własny constraint - wskazujemy dla którego pola wystąpił błąd
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate(message)
                    .addNode(dateToFieldName)
                    .addConstraintViolation();
        }

        return result;

    }

    private boolean validateDates(Object object) {

        try {
            
            Date dateFrom = field(dateFromFieldName).ofType(Date.class).in(object).get();
            Date dateTo = field(dateToFieldName).ofType(Date.class).in(object).get();

            // sprawdzenie dat
            return checkDatesInterval(dateFrom, dateTo);
        }
        catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
}

Poprzez refleksję można odwołać sie do pola używając właściwie "czystej" javy, ale wygląda to średnio w kodzie i lepiej użyć jakiegoś gotowego rozwiązania. Ja osobiście polecam biliotekę: FEST

Zastowanie adnotacji jest następujące:
@CheckDates(dateFrom="from", dateTo="to")
public class Entity {

    private Date from;
    private Date to;
}