Introduzione
Una delle attività più tediose ma ineluttabile nella realizzazione di applicazioni è la validazione dei dati di input. In questo articolo affrontiamo una soluzione standardizzata che utilizza framework conformi alla specifica JSR 380 altrimenti nota come Bean Validation 2.0. JSR 380 è parte della specifica JavaEE e introduce un insieme di annotazioni che consentono di specificare i criteri che le proprietà di un bean devono soddisfare per la validazione dei dati.
Dipendenze
Per descrivere le principali annotazioni della specifica utilizzeremo un progetto Maven al quale dovremo aggiungere le necessarie dipendenze. Innanzitutto dobbiamo introdurre la dipendenza al jar che definisce le API della specifica JSR 380.
1 2 3 4 5 |
<dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> <version>2.0.0.Final</version> </dependency> |
L’implementazione di riferimento della specifica è invece quella fornita da Hibernate attraverso il pacchetto hibernate-validatior, che è interamente separato ed indipendente dalle componenti di persistenza di Hibernate.
1 2 3 4 5 |
<dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>6.0.4.Final</version> </dependency> |
JSR 380 inoltre fornisce il supporto all’utilizzo delle espressioni all’interno dei messaggi di validazione. Per il parsing di tali espressioni, è necessario aggiungere le dipendenza sia dall’Expression Language API che alla sua implementazione di riferimento GlassFish.
Validazione
Molti dei principali framework java come Spring, JSF, Hibernate, Struts, etc., forniscono direttamente il supporto alle annotazioni della specifica JSR380 ed eseguono automaticamente la validazione dei bean trattati all’interno dei rispettivi processi. In generale quindi difficilmente capiterà di dover avviare la validazione manualmente. Per gli scopi dell’articolo, però, abbiamo necessità di utilizzare un approccio diretto. Per farlo dobbiamo innanzitutto ottenere un oggetto Validator
attraverso la classe factory Validatorfactory
, e procedere poi alla validazione del bean utilizzando il metodo validate()
di tale classe. Il metodo, in caso di violazioni, restituirà un set di oggetti di tipo ConstraintViolation
.
1 2 3 4 5 6 7 8 9 10 |
ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); Validator validator = factory.getValidator(); ... get the bean ... Set<ConstraintViolation<...>> violations = validator.validate( ... ); for ( ConstraintViolation<User> violation : violations ) { System.err.println( violation.getPropertyPath() + ": " + violation.getMessage() ); } |
Annotazioni
Come anticipato nell’introduzione i criteri di validazione sono espressi attraverso specifiche annotazioni con cui decorare le proprietà o i metodi di un bean. Tutte le annotazioni definite nella specifica, oltre a proprietà che sono funzionali al tipo di vincolo che esprimono, sono caratterizzate dalle tre seguenti proprietà:
message |
Stringa utilizzata per l’identificazione del messaggio di errore che è restituito in caso di violazione del vincolo. Tutti i tag sono caratterizzati da messaggi di default nella forma {javax.validation.constraints.[NAME].message} che possono essere modificati utilizzando il Resource Bundle. |
groups |
Questa proprietà può essere utilizzata per specificare l’ordine di validazione delle proprietà o per eseguire validazioni parziali del bean. Per farlo al metodo validate() , oltre al bean, può essere passato in input:
Se non specificato assume il valore di default |
payload |
Tale proprietà è utilizzata per portare informazioni specifiche associate all’errore verso il client. Non si tratta quindi di una proprietà utilizzata dal framework ma implementa un meccanismo per arricchire la descrizione dell’errore con informazioni aggiuntive. Ad esempio è possibile definire una severity level che sarà poi utilizzata dalle maschere di input per visualizzare il messaggio con colori diversi. |
Vediamo ora come esempio il bean User
e descriviamo alcune delle annotazioni principali di JSR 380:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
public class User { @NotNull(message = "Name cannot be null") private String name; @AssertTrue private boolean working; @Size(min = 10, max = 200, message = "About Me must be between 10 and 200 characters") private String aboutMe; @Min(value = 18, message = "Age should not be less than 18") @Max(value = 150, message = "Age should not be greater than 150") private int age; @Email(message = "Email should be valid") private String email; @Past private Date bornDate; @Positive private Integer rating; public boolean hasDrivingLicense; // getter and setters } |
@NotNull
: specifica che la proprietà annotata non può essere NULL;@AssertTrue
: specifica che la proprietà annotata deve assumere il valore TRUE;@Size
: applicata ai tipi String, Collenction, Map e array, specifica che il valore della proprietà annotata deve avere lunghezza tramin
andmax
;@Min
: indica che la proprietà annotata non può assumere un valore inferiore alla proprietàvalue
;@Max
: indica che la proprietà annotata non può assumere un valore superiore alla proprietàvalue
;@Email
: specifica che il valore della proprietà annotata deve essere una email di formato valido;@Past
: indica che il valore della proprietà annotata deve essere una data nel passato;@Positive
: specifica che il valore della proprietà annotata deve essere positivo.
Generiamo ora un bean di tipo User
in modo da attivare tutte le violazioni:
1 2 3 4 5 |
User user = new User(); user.setAboutMe( "I'm ok" ); user.setEmail( "56st" ); user.setBornDate( new SimpleDateFormat("yyyy-MM-dd").parse("2100-01-01") ); user.setRating( -1 ); |
Come risultato saranno stampate sullo standard error i seguenti messaggi:
1 2 3 4 5 6 7 |
rating: must be greater than 0 email: Email should be valid working: must be true age: Age should not be less than 18 aboutMe: About Me must be between 10 and 200 characters bornDate: must be a past date name: Name cannot be null |
Altre Annotazioni
Altre annotazioni utili alla validazione dei bean sono:
@NotEmpty
: applicata ai tipi String, Collection, Map e Array indica che la proprietà non può essere NULL o vuota (empty).@NotBlank
: applicata alle stringhe indica che il valore non può essere NULL o composto da soli whitespace.@PositiveOrZero
: applicato a tipi numerici indica che che il valore deve essere positivo o zero.@Negative
and@NegativeOrZero
: applicato a tipi numerici indicano che il valore deve essere strettamente negativo oppure negativo o uguale a zero.@PastOrPresent
: indica un vincolo sulle date che devono riferirsi al passato o al presente.@Future
and@FutureOrPresent
: indica un vincolo sulle date che devono riferirsi al futuro oppure al futuro incluso il presente.
Raggruppamento
Abbiamo anticipato precedentemente che tutte le annotazioni hanno la proprietà comune groups
, che è utilizzabile per modificare l’ordine di validazione delle proprietà o per eseguire validazioni parziali del bean. Supponiamo ad esempio di voler realizzare la validazione dei campi del bean User
esclusivamente ai fini di identificare utenti che posseggono una licenza di guida. Per farlo introduciamo l’interfaccia DriverChecks
ed associamola alle proprietà di interesse ovvero:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public class User { .... @Min( value = 18, message = "Age should not be less than 18", groups = DriverChecks.class) private int age; @AssertTrue( message = "You first have to pass the driving test", groups = DriverChecks.class ) public boolean hasDrivingLicense; .... } |
A questo punto possiamo validare il bean limitatamente alle proprietà che identificano un utente con licenza di guida passando l’interfaccia DriverChecks
al metodo di validazione, nel modo seguente:
1 |
Set<ConstraintViolation<User>> violations = validator.validate( user, DriverChecks.class ); |
In generale non è specificato alcun ordine di validazione dei campi che compongono il bean. Nei casi in cui l’ordine sia importante è possibile utilizzare la proprietà groups e definire una interfaccia annotata con @GroupSequence
. Ad esempio supponiamo di voler validare prima i campi di default (si ricordi che dove non specificato all’annotazione è associato il gruppo Default
) e poi quelli associati a DriverChecks
. A tale scopo definiamo l’interfaccia:
1 2 3 |
@GroupSequence({Default.class, DriverChecks.class}) public interface OrderedChecks { } |
1 |
violations = validator.validate( user, OrderedChecks.class ); |
La validazione procederà inizialmente verificando i campi associati al gruppo di default e nel caso di errore terminerà immediatamente senza verificare i vincoli associati al gruppo DriverChecks
.
Personalizzazione dei Messaggi
Il messaggio di errore associato al vincolo è configurabile attraverso la proprietà message
. In particolare abbiamo visto nell’esempio del bean User
come modificarne il valore che per default è {javax.validation.constraints.[NAME].message}
. Inserire il messaggio direttamente nelle annotazioni in generale non è auspicabile, soprattutto se vogliamo supportare l’internazionalizzazione dell’applicazione.
E’ preferibile invece creare un file ValidationMessages.properties
o ValidationMessages_[locale].properties
che è utilizzato dal Resource Bundle ValidationMessages
per l’interpolazione dei messaggi. La personalizzazione del messaggio può avvenire in due modi:
- Ridefinendo la proprietà
javax.validation.constraints.[NAME].message
ma questo implica che il messaggio sarà associato a tutti campi annotati con[NAME]
. - Definendo uno nuova messaggio ed associandone la chiave alla proprietà
message
.
Ad esempio possiamo ridefinire il messaggio di default associato all’annotazione @Past
e definire una nuovo messaggio da associare alla proprietà rating
del bean User
inserendoli nel file ValidationMessages_it_IT.properties
da collocare nella cartella resources
del progetto:
1 2 |
javax.validation.constraints.Past.message = La data di nascita non puo' essere nel futuro user.validator.message.rating = Il rating dell'utente deve essere positivo |
rating
dovrà essere così modificata:
1 2 |
@Positive(message="{user.validator.message.rating}") private Integer rating; |
Nella definizione del messaggio di errore è anche possibile utilizzare espressioni che saranno valutate prima della costruzione dello stesso. In particolare nel contesto EL il validatore inserirà i seguenti valori utilizzabili nell’espressione:
- le proprietà definite nell’annotazione (es.
value
dell’annotazione@Min
); - il valore assegnato alla proprietà identificato dalla chiave
validatedValue
.
Un esempio di messaggio associato alla proprietà age
del bena User
potrebbe essere così definito:
1 |
l'età ${validatedValue} non é valida poichè vede essere superiore a {value} |
generando un messaggio di errore del tipo:
1 |
l'età 12 non é valida poichè vede essere superiore a 18 |
Codice Sorgente
Il codice sorgente per tutti gli esempi presentati è scaricabile qui bean-validation.