Con la BeanValidation, introdotta a partire dalla piattaforma Java EE 6, la rappresentazione del modello diventa sufficiente ad esprimere i concetti e quei vincoli che lo rendono valido una volta per tutte. Ma che cosa è nel dettaglio la BeanValidation? A che cosa serve? E perché ne abbiamo bisogno. In questo articolo vediamo di rispondere a tutti questi interrogativi.
Introduzione
La modellazione a oggetti di un dominio è sicuramente uno dei primi passi quando si affronta un nuovo progetto. Spesso però la sola rappresentazione dei concetti in classi non è sufficiente: per esempio il nome di un utente o la sua mail sicuramente saranno rappresentati da stringhe, ma l’e-mail ha un formato ben preciso per essere definita tale. Prima di Java EE 6 era quindi difficile avere un modello di dominio a oggetti che fosse capace in modo coerente di rappresentare anche l’aspetto dei vincoli che naturalmente sussistono sul modello stesso. Con la piattaforma Java EE 6, aggiornata di recente alla 7, è stata introdotta la BeanValidation (nota come JSR-303): cerchiamo di capire meglio perchè ne avevamo bisogno.
Perchè abbiamo bisogno della BeanValidation?
Quando si modellano i concetti di un dominio, che sia poi rappresentato in un modello a oggetti o entità-relazione è puramente un fatto implementativo. I database relazionali danno nativamente la possibilità di rappresentare dei concetti, le relazioni tra essi e i vincoli degli attributi che compongono i concetti stessi (dimensione dei campi, foreign key, constraint sui valori…).
Lato programmazione a oggetti, invece (e in particolare in Java), questa potenzialità di esprimere vincoli in modo dichiarativo era praticamente assente: bisognava ricorrere a della logica di business per esprimere certi vincoli. Spesso questi sono stati implementati a livello di interfaccia (come i limiti sulla lunghezza dei campi di una form). Ne consegue che, se lo stesso oggetto era usato in un servizio REST, per esempio, il vincolo andava nuovamente riscritto.
Con la BeanValidation, la rappresentazione del modello diventa sufficiente a esprimere iconcetti e quei vincoli che lo rendono valido una volta per tutte!
Validazioni dichiarative
La specifica obbliga l’implementazione di 13 tipi di validazioni, riassunti nella tabella del Java EE Tutorial [1]. Sarebbe inutile riportarle qua; vediamole invece subito in azione: un po’ di codice vale molto più di mille parole!
Immaginiamo di avere l’oggetto “patente“:
public class Patente { @NotNull @Past private Date dataEmissione; @NotNull @Future private Date dataScadenza; @NotNull @Size(min = 10, max=10) @Pattern(regexp = "[A-Z]{2}d{8}") private String numero; @Min(0) private int punti; @Valid @NotNull private InformazioniPersonali informazioniPersonali; //Getters & Setters }
Gli attributi, le informazioni, i vincoli
Immaginiamo quindi di avere una patente caratterizzata da alcuni attributi come la data di scadenza, di emissione, il numero patente e i punti scalabili. Un oggetto separato raccoglie invece le informazioni relative all’intestatario. Affinchè un oggetto di questo tipo possa ritenersi valido, deve rispettare una serie di vincoli, che vediamo di seguito.
- @NotNull: il numero, le informazioni personali e le date di emissione e scadenza sono obbligatorie;
- @Past: la data di emissione deve essere nel passato;
- @Future: la data di scadenza deve essere nel futuro
- @Size: il numero della patente deve essere di 10 caratteri (min e max sono uguali). Se applicata alle collezioni, questa annotazione convalida le sue dimensioni;
- @Pattern: il numero della patente deve essere formato da 2 lettere e 8 numeri;
- @Min: i punti della patente non possono essere negativi;
- @Valid: la patente è valida se anche le informazioni personali sono valide (qui non riportate); in pratica, questa annotazione propaga la validazione all’oggetto annotato (questa operazione è chiamata object graph validation).
Tutte le annotazioni di default possono essere applicate sia a livello di metodo che di attributo. Occhio solo a non mischiare gli approcci all’interno della stessa classe per evitare comportamenti inattesi (come doppie validazioni). Con questo approccio, però, rimangono escluse le validazioni incrociate tra campi della stessa classe. Come fare allora? La specifica dà la possibilità di creare dei validatori personalizzati che possono essere applicati anche a livello di classe, ed è proprio quello che serve per risolvere il problema della validazione incrociata.
Chi si prende la briga di validare?
Finora ci siamo preoccupati di illustrare perchè e come si dichiarano le validazioni; ma come sappiamo, se non le interpreta nessuno, le annotation sono “lettera morta”.
Validazione programmatica
Il processo di validazione è gestito dall’oggetto javax.validation.Validator che si ottiene in modo semplice dalla factory:
ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); Validator validator = factory.getValidator();
Quest’oggetto permette di validare:
tutti i vincoli di un’istanza di una classe
validator.validate(patente)
un singolo attributo
validator.validateProperty(patente, "numero")
il valore di un attributo
validator.validateValue(Patente.class, "numero", "AZ12345678")
In ogni caso, il risultato della validazione restituisce un Set di ConstraintViolation. Se l’insieme è vuoto, la validazione è avvenuta con successo, altrimenti questo oggetto contiene tutte le informazioni necessarie a notificare l’errore di validazione.
Validazione con JPA
Se si usa JPA è naturale aspettarsi che i vincoli dichiarati nel bean (che è un’entità in questo caso) in realtà corrispondano ai constraint delle tabelle del database mappate dalle entità. Altrettanto naturale è aspettarsi che sia JPA stesso ad integrarsi con la bean validation e generare errori di validazione senza il nostro intervento programmatico! Cosa dobbiamo fare allora? Quasi niente… quando passeremo il nostro oggetto all’EntityManager, questo verrà validato. L’unica cosa che possiamo fare è gestire l’eventuale eccezione ConstraintViolationException.
Quando usata con JPA, la validazione scatta nelle INSERT, negli UPDATE, ma non nelle DELETE. E se avessi bisogno di sovrascrivere questo comportamento e attivare la validazione anche nella cancellazione? Nella validazione programmatica potevamo scegliere se validare tutta la classe o alcune sue proprietà. In questo caso invece, per avere un controllo così fine dobbiamo avvalerci dei validation groups. Ogni annotazione infatti ha un attributo groups nel quale possiamo definire un gruppo di attributi da validare insieme.
Ammettiamo quindi di impedire la cancellazione delle patenti ancora attive. Significa cioè che solo quando la data scadenza è nel passato posso procedere alla cancellazione. Possiamo attivare la validazione in cancellazione solo per l’attributo dataScadenza.
Per fare questo, abbiamo anzitutto bisogno di definire un gruppo tramite una interfaccia Java (vuota, detta anche tag interface, come la Serializable per intendersi):
public interface CancellazioneSicura {}
Poi, è necessario associare l’interfaccia al nostro attributo nella classe Patente:
@NotNull @Future @Past(groups = CancellazioneSicura.class) private Date dataScadenza;
Occorre infine modificare il persistence.xml per associare l’interfaccia tag all’evento di cancellazione, aggiungendo la property:
value="it.cosenonjaviste.validation.CancellazioneSicura" />
Se non specificato, la validazione è associata al gruppo di default javax.validation.groups.Default: tutto quindi continuerà a funzionare come prima. In cancellazione però verrà attivato il gruppo CancellazioneSicura, impendendo la cancellazione per le patenti ancora non scadute.
Gli eventi JPA che si possono personalizzare sono:
- javax.persistence.validation.group.pre-persist
- javax.persistence.validation.group.pre-update
- javax.persistence.validation.group.pre-remove
I valori (tag interfaces), se più di uno, sono separati da virgola.
Nel caso si voglia disattivare completamente la validazione lato JPA, basta aggiungere nel persistence.xml le seguenti righe:
NONE ...
Validazione con JSF
In un applicativo web, aspettare che la validazione scatti praticamente quando siamo a due passi dal database (leggi JPA) può non essere la scelta giusta, sia dal punto di vista dell’interazione utente che per questioni di performance. JSF fortunatamente interagisce con la bean validation durante il ciclo di vita di una richiesta al server. Durante la fase di validazione definita dal ciclo di vita JSF, se sono presenti annotazioni JSR-303, vengono usate per validare la richiesta.
A differenza di quanto visto finora, la validazione scatta per i soli campi legati agli UIInput JSF (cioè ai campi di input dell’interfaccia grafica), ignorando gruppi e annotazioni a livello di classe eventualmente specificati.
Bean validation e i18n
In caso di errori di validazione, i validatori definiti dalla specifica rispondono con messaggi predefiniti in nove lingue… ovviamente ad esclusione dell’italiano. Per tradurli nella nostra lingua possiamo partire dal file di properties inglese che contiene tutti i messaggi di errore. Se la vostra implementazine è Hibernate Validator, recuperiamo dal JAR il file
/org/hibernate/validator/ValidationMessages.properties
e copiamolo nella root del nostro progetto. Per mantenere l’internazionalizzazione, ossia non sovrascrivere i messaggi in inglese, è bene seguire la convenzione classica che si usa nei resource bundle in Java. Che significa? Dare al nuovo file il nome ValidationMessages_it.properties.
BeanValidation: come averla a disposizione?
Fin qui abbiamo visto come usarla, ma come si fa ad avere la BeanValidation a disposizione? Tutti gli application server compliant Java EE 6 hanno il supporto nativo alla BeanValidation 1.0, ognuno con la propria implementazione. Un esempio? JBoss 6 e 7 oppure Websphere 8.
Per i servlet container, invece, come Tomcat o Jetty, o più in generale in Java SE, è necessario includerla manualmente, scegliendo tra le implementazioni più famose come Apache Bean Validation [2] o Hibernate Validator [3].
Java EE 7 ha aggiunto qualche leggera modifica alla specifica, portandola alla versione 1.1. La novità più interessante è la validazione sugli argomenti dei metodi e sui tipi di ritorno. Se non volete/potete aggiornare la vostra installazione, per ottenere lo stesso risultato si può lavorare con un po’ di AOP…
Conclusioni
Con le potenzialità offerte dalla bean validation, la piattaforma Java fa ancora un passo avanti verso l’approccio dichiarativo introdotto con Java 5 e le annotation, semplificando notevolmente il modo di modellare il dominio di un applicativo e sue validazioni di business.
Riferimenti
[1] Java EE Tutorial
http://docs.oracle.com/javaee/6/tutorial/doc/gircz.html
[2] Apache Bean Validation
[3] Hibernate Validator
http://hibernate.org/validator/