In occasione di un contributo tra CoseNonJaviste e MokaByte, avevamo avuto modo di presentare la bean validation e le sue caratteristiche principali. Ovviamente come ogni buon framework che si rispetti è possibile personalizzarlo e aggiungere del nostro. Vediamo come.
Custom validation: casi d’uso
Nella modellazione di un dominio, ci imbattiamo sempre in una serie di vincoli strutturali che possono avere diversa natura: la bean validation copre i casi più generici come la possibilità di specificare le dimensioni dei campi di una certa entità di dominio, piuttosto che la conformità o meno a certe espressioni regolari.
Normalmente però abbiamo bisogno di specificare vincoli sul modello ben più singolari come:
- validazione incrociata tra campi
- validazioni specifiche del dominio
Per mantenere la semplicità dichiarativa dei vincoli tramite annotazioni, la bean validation ci mette a disposizione gli strumenti per creare la nostra annotazione a cui associare la nostra logica applicativa.
Immaginiamo quindi di dover gestire il caso di validazione incrociata, per esempio tra un intervallo di date, molto frequente nelle pagine di ricerca. Avremo quindi una classe per gestire il componente “date range” del tipo:
@TemporalRangeConstraint public class DateRangeSearchParams { private Date from; private Date to; ... }
La nostra nuova annotazione @TemporalRangeConstraint
servirà per aggiungere il vincolo che la data from
deve essere minore o uguale alla data to
. Essendo un tipo di validazione che coinvolge più campi va dichiarata a livello di classe.
Com’è fatta questa annotazione?
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = TemporalRangeValidator.class) @Documented public @interface TemporalRangeConstraint { String message() default "{it.cosenonjaviste.temporal.range}"; Class>[] groups() default {}; Class extends Payload>[] payload() default {}; }
La specifica richiede che:
- un attributo
message
che ritorni la chiave del messaggio di default in caso di validazione fallita; - un attributo
groups
che permette di specificare il gruppo di validazione. Cos’è un gruppo? Si tratta di definire appunto dei gruppi di attributi da validare insieme tramite la tecnica delle “tag interfaces” (ovvero delle interfacce vuote che servono solo per marcare un certo gruppo) - un attributo
payload
, non usato direttamente dalla API, che può servire per passare dei parametri al vincolo (in realtà ho da capire a cosa serve veramente…)
Tra le annotazioni ammonticchiate sulla definizione dell’@interface
, @Constraint
è quella che associa la logica di validazione all’annotazione stessa. Questa deve essere una classe che implementa ConstraintValidator
:
public class TemporalRangeValidator implements ConstraintValidator{ @Override public void initialize(TemporalRangeConstraint annotation) { } @Override public boolean isValid(DateRangeSearchParams searchParams, ConstraintValidatorContext context) { return !isFromAfterTo(searchParams); } private boolean isFromAfterTo(DateRangeSearchParams searchParams) { return searchParams.getFrom() != null && searchParams.getTo() != null && searchParams.getFrom().after(searchParams.getTo()); } }
- il metodo
initialize
può service per salvarsi l’istanza dell’annotazione che ha scaturito la validazione. Può essere utile se l’annotazione ha un proprio stato. - come è facile immaginarsi, il metodo
isValid
determina la condizione di validità del vincolo.
Gestione dei messaggi di errore
Se la validazione fallisce, viene cercata la chiave it.cosenonjaviste.temporal.range nel file di properties chiamato ValidationMessages.properties per caricare il corrispettivo messaggio. E importante ricordare che:
- questo file segue le regole dei resource bundle di Java per il supporto all’internazionalizzazione: questo significa che se il contesto della lingua è italiano (per esempio in risposta alla chiamata
Locale#getDefault()
), la chiave verrà cercata, in ordine, nei seguenti file:- ValidationMessages_it_IT.properties
- ValidationMessages_it.properties
- ValidationMessages.properties
- il file viene cercato nella root del progetto
Interpolazione dei messaggi
Un ultima particolarità riguardante quella che viene definita message interpolation: nel caso in cui la nostra validazione fosse stata applicata ad un solo campo, poteva essere interessante poter riportare nel messaggio il valore che ha generato l’errore di validazione oppure i valori associati all’istanza dell’annotazione che rappresentano il vincolo. Facciamo un esempio concreto, creando una annotazione che valida la data minima ammessa, applicata ad entrambe i campi del nostro oggetto:
public class DateRangeSearchParams { @MinDate("2010-01-01") private Date from; @MinDate("2010-01-01") private Date to; ... }
e definita come:
@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = MinDateValidator.class) @Documented public @interface MinDate { String message() default "{it.cosenonjaviste.min.date}"; Class>[] groups() default {}; Class extends Payload>[] payload() default {}; String pattern() default "yyyy-MM-dd"; String value(); }
Fino alla bean validation 1.0 (introdotta con Java EE 6), la specifica permetteva solo di gestire nei messaggi i valori degli attributi dell’annotazione. Si poteva cioè scrivere un file di properties:
it.cosenonjaviste.min.date=La data deve essere minore di {value}
interpolato come:
La data deve essere minore di 2010-01-01
Nella bean validation 1.1 invece è stato introdotto l’uso della Unified Expression Language nell’interpolazione dei messaggi distinguendo tra:
-
message parameters rappresentate da stringhe racchiuse tra
{ }
, come nel caso di{value}
-
message expressions rappresentate espressioni racchiuse tra
${ }
. Con le expressions è possibile far riferimento al valore validati con${validatedValue}
oppure usare operatori ternari o formatters. Per maggiori dettagli consultare la guida di Hibernate Validator (implementazione di riferimento della specifica)
Cosa c’è di nuovo nella Bean Validation 1.1?
Oltre all’uso dell’expression language, la nuova minor release della specifica, uscita insieme a Java EE 7, comprende alcune piccole migliorie:
- Integrazione con CDI: nei validatori adesso è possibile iniettare bean di CDI.
- Validazione a livello di metodo: tramite interceptors trasparenti all’utente, è possibile validare in modo dichiarativo gli argomenti di un metodo (utile per esempio per validare gli input dei servizi della business logic).
- Integrazione con JAX-RS: la validazione a livello di metodo funziona adesso anche per quei metodi esposti come servizi REST: gli input dei servizi sono così validati automaticamente
Conclusioni
La creazione di un validatore custom è estremamente semplice e a mio avviso anche molto elegante con la Bean Validation. La nuova release 1.1 sicuramente ha portato semplificazioni allo sviluppo soprattutto per quanto riguarda l’integrazione con CDI e JAX-RS.
Prossimamente vedremo come attivare l’integrazione con JAX-RS anche se lavoriamo ancora in Java EE 6.