Bean Validation 1.1 e i validatori personalizzati

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:
    1. ValidationMessages_it_IT.properties
    2. ValidationMessages_it.properties
    3. 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.

Andrea Como

Sono un software engineer focalizzato nella progettazione e sviluppo di applicazioni web in Java. Presso OmniaGroup ricopro il ruolo di Tech Leader sulle tecnologie legate alla piattaforma Java EE 5 (come WebSphere 7.0, EJB3, JPA 1 (EclipseLink), JSF 1.2 (Mojarra) e RichFaces 3) e Java EE 6 con JBoss AS 7, in particolare di CDI, JAX-RS, nonché di EJB 3.1, JPA2, JSF2 e RichFaces 4. Al momento mi occupo di ECM, in particolar modo sulla customizzazione di Alfresco 4 e sulla sua installazione con tecnologie da devops come Vagrant e Chef. In passato ho lavorato con la piattaforma alternativa alla enterprise per lo sviluppo web: Java SE 6, Tomcat 6, Hibernate 3 e Spring 2.5. Nei ritagli di tempo sviluppo siti web in PHP e ASP. Per maggiori informazioni consulta il mio . - -