Niveau : | Supérieur |
La validation des données est une tâche commune, nécessaire et importante dans chaque application. De plus, ces validations peuvent être faites dans les différentes couches d'une application :
Certains frameworks notamment pour les couches présentation et DAO proposent leurs propres solutions de validations de données. Pour les autres couches, soit un autre framework soit une solution maison sont utilisées. Toutes ces solutions proposent des implémentations différentes pour déclarer et valider des contraintes mais aussi pour signaler les violations de contraintes (Exception, objets dédiés, ...).
Ceci entraîne fréquemment une duplication du code et/ou une redondance des contrôles effectués avec les risques que cela peut engendrer :
Il y a aussi le risque d'oublier la déclaration de contraintes dans une couche.
Une solution est de mettre ces traitements de validation dans les entités du domaine ce qui les complexifie.
De plus, certaines de ces validations sont fréquemment utilisées et sont donc standard (vérifier la présence d'une valeur, vérifier une taille, vérifier la valeur sur une plage de dates ou une plage numérique, vérifier la valeur sur une expression régulière, ...).
Il est aussi généralement nécessaire de développer des validations spécifiques.
Pour répondre à ces différents besoins, des frameworks ont été développés pour :
Ce chapitre contient plusieurs sections :
Les validations de données sont utiles dans plusieurs endroits d'une application et ce quel que soit le type d'applications :
Il est important de valider les contraintes le plus tôt possible dans les couches de l'application pour éviter des appels inutiles, fréquemment au travers du réseau, aux fonctionnalités de la couche en amont.
Il est aussi très important de répéter les validations de données dans les couches sous-jacentes car il ne faut pas présumer de ce que fait la couche en amont. Par exemple, même si les données dont validées dans l'IHM d'une application invoquant un service, il faut revalider ces données dans la couche service car si le service est invoqué par une application qui ne fait pas le contrôle, les données risquent d'être non valides. Même si cela augmente les traitements cela rend les applications plus sûres.
Dans tous les cas, les contrôles doivent être faits dans la couche la plus basse possible, ce qui comprend aussi les contrôles d'intégrité dans la base de données.
Il existe des contrôles spécifiques à la couche présentation : par exemple, la double saisie d'un mot de passe et la comparaison des deux valeurs saisies.
Il existe une frontière très mince entre les règles métiers, les traitements métiers et la validation des données. Il peut être tentant de mettre certaines de ces fonctionnalités dans la validation des données mais il ne faut pas tout mettre dans la validation et maintenir un rôle à la couche métier. Les traitements de validation des données doivent rester simples et ne pas devenir trop complexes ni nécessiter plusieurs entités (propriétés, objets, ressources, ...).
L'API Bean Validation est issue des travaux de la JSR 303 : https://jcp.org/en/jsr/detail?id=303
Cette JSR 303 propose de standardiser un framework de validation des données d'un bean.
L'intérêt de cette API est de proposer une approche cohérente sous la forme d'un standard pour la validation des données d'un bean.
Il y a besoin d'un standard pour plusieurs raisons :
Généralement ces validations ont lieu avec plus ou moins de redondance dans les différentes couches d'une application.
Fréquemment, les contraintes sont exprimées sur les entités du domaine ainsi la JSR propose de déclarer les contraintes dans les beans qui encapsulent les entités du domaine.
Inclure ces validations dans les entités du domaine permet de centraliser ces traitements plutôt que de les dupliquer ou les répartir dans les différentes couches.
L'API Bean Validation standardise la définition, la déclaration et la validation de contraintes sur les données d'un ou plusieurs beans.
La déclaration de contraintes se fait dans le bean qui encapsule les données. L'expression de ces contraintes se fait à l'aide d'annotations ou d'un descripteur au format XML ce qui permet de réduire la quantité de code à produire. La manière privilégiée pour déclarer les contraintes est d'utiliser les annotations mais il est aussi possible d'utiliser un descripteur au format XML.
L'API propose une ensemble de contraintes communes fournies en standard et permet de définir ses propres contraintes.
La validation de ces contraintes se fait grâce à un valideur fourni par l'API.
Elle propose aussi des fonctionnalités avancées comme la composition de contraintes, la validation partielle en utilisant la notion de groupes de contraintes, la définition de contraintes personnalisées et la recherche des contraintes définies.
La JSR 303 tente de combiner les meilleures fonctionnalités de différents frameworks dans une spécification qui peut être implémentée par différents fournisseurs et qui propose :
Le but de cette JSR est de standardiser les fonctionnalités de validation des données des beans en utilisant des annotations plutôt que d'avoir à écrire du code pour réaliser ces validations.
Il ne s'agit pas de fournir une solution permettant de définir des contraintes dans toutes les couches de l'application (notamment elle ne couvre pas directement les contraintes dans la base de données car celles-ci sont spécifiques) mais de proposer des contraintes au niveau des entités du domaine. Ce choix repose sur le fait que ces contraintes sont généralement liées à l'entité elle-même.
La JSR 303 a plusieurs objectifs :
L'API Java Bean Validation utilise plusieurs éléments et concepts lors de sa mise en oeuvre :
La définition d'une contrainte se fait par une annotation en précisant le type sur lequel elle s'applique, ses attributs et la classe qui encapsule les traitements de validation.
Les groupes permettent de n'appliquer qu'un sous-ensemble des contraintes d'un bean. La déclaration d'une contrainte peut être associée à un ou plusieurs groupes.
La validation d'une contrainte applique les traitements de validation d'une données sur l'instance courante.
Les spécifications doivent être implémentées par un fournisseur pour pouvoir être utilisées. Le projet Hibernate Validator est l'implémentation de référence de ces spécifications.
L'interpolation du message contient les traitements pour créer le message d'erreur fourni à l'utilisateur.
Une séquence permet de définir l'ordre dans lequel les contraintes de validation vont être évaluées.
Les spécifications proposent une API qui permet d'obtenir des métadonnées sur les contraintes d'un type. Cette API est particulièrement utile pour l'intégration dans d'autres frameworks.
Les spécifications proposent aussi une API nommée BootStrap qui fournit des mécanismes pour obtenir une instance de la fabrique de type ValidatorFactory. Cette API permet notamment de choisir l'implémentation à utiliser.
Une contrainte est composée de deux éléments :
La JSR-303 définit un ensemble de contraintes que chaque implémentation doit fournir. Cependant, cet ensemble ne concerne que des contraintes standard qui ne sont en général pas suffisantes pour répondre à tous les besoins. La spécification prévoit donc la possibilité de développer ses propres contraintes personnalisées.
Ceci peut se faire de deux façons :
La validation des contraintes se fait par introspection à la recherche des annotations du type des contraintes utilisées dans le bean. Pour chaque annotation, la classe de type Validator associée est instanciée et utilisée par le framework pour valider la valeur de la donnée.
L'API peut prendre en charge, à la demande lors de la validation du bean, le parcours des objets dépendant de ce bean pour les valider également si des contraintes leur sont associées.
La validation des données peut être invoquée automatiquement par les frameworks qui proposent un support pour l'API Bean Validator : c'est notamment le cas pour JSF 2.0 et JPA 2.0.
La JSR 303 propose de standardiser les validations avec les spécifications d'une API composée de plusieurs parties :
La plupart des frameworks de validations sont relatifs à un framework particulier pour une ou deux couches données : Struts, Hibernate, ... L'API Bean Validator est conçue pour être utilisée dans toutes les couches écrites en Java d'une application.
L'API Bean Validation est incluse dans Java EE 6 car elle est utilisée par JSF 2.0 et JPA 2.0. L'API peut cependant être utilisée dans Java SE à partir de la version 5.
L'API Bean Validation est conçue pour être indépendante de la technologie qui l'utilise aussi bien côté client (Swing, ...) que serveur (JPA, JSF, ...).
Le package de cette API est javax.validation.
L'implémentation de référence est proposée par Hibernate Validator 4.
Pour mettre en oeuvre l'API, il n'est pas nécessaire d'utiliser des classes de l'implémentation : seules les classes et interfaces de l'API doivent être importées dans le code source. Ceci rend l'utilisation d'une autre implémentation très facile.
Il est nécessaire d'ajouter au classpath les dépendances de l'implémentation utilisée : par exemple, avec l'implémentation de référence.
Cet exemple va définir un bean, ajouter une contrainte de type non null sur un champ et créer une petite application de test qui va instancier le bean avec un champ null et appliquer les validations des contraintes sur le bean.
La JSR 303 permet d'annoter une classe ou un attribut d'une classe ou le getter de cet attribut.
Exemple : |
package com.jmdoudoux.test.validation;
import java.util.Date;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Past;
import javax.validation.constraints.Size;
public class PersonneBean {
private String nom;
private String prenom;
private Date dateNaissance;
public PersonneBean(String nom, String prenom, Date dateNaissance) {
super();
this.nom = nom;
prenom = prenom;
this.dateNaissance = dateNaissance;
}
@NotNull
@Size(max=50)
public String getNom() {
return nom;
}
public void setNom(String nom) {
this.nom = nom;
}
@NotNull
@Size(max=50)
public String getPrenom() {
return prenom;
}
public void setPrenom(String prenom) {
prenom = prenom;
}
@Past
public Date getDateNaissance() {
return dateNaissance;
}
public void setDateNaissance(Date dateNaissance) {
this.dateNaissance = dateNaissance;
}
}
L'API propose aussi un mécanisme pour valider les contraintes et exploiter les éventuelles violations.
Exemple : |
package com.jmdoudoux.test.validation;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
public class TestValidation {
public static void main(String[] args) {
PersonneBean personne = new PersonneBean(null, null, new GregorianCalendar(
2065, Calendar.JANUARY, 18).getTime());
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Set<ConstraintViolation<PersonneBean>> constraintViolations =
validator.validate(personne);
if (constraintViolations.size() > 0 ) {
System.out.println("Impossible de valider les donnees du bean : ");
for (ConstraintViolation<PersonneBean> contraintes : constraintViolations) {
System.out.println(contraintes.getRootBeanClass().getSimpleName()+
"." + contraintes.getPropertyPath() + " " + contraintes.getMessage());
}
} else {
System.out.println("Les donnees du bean sont valides");
}
}
}
Résultat : |
Impossible de valider les donnees du bean :
PersonneBean.dateNaissance doit être dans le passé
PersonneBean.nom ne peut pas être nul
PersonneBean.prenom ne peut pas être nul
La déclaration de contraintes se fait dans des classes ou des interfaces avec des annotations ce qui est la manière recommandée ou par une description dans un fichier XML.
Une contrainte peut être appliquée sur un type (classe ou interface), un champ ou une propriété respectant les conventions des Java beans.
Remarque : les champs statiques ne peuvent pas être validés en utilisant l'API.
La valeur fournie à l'objet de type ConstraintValidator qui va valider les contraintes dépend de l'entité annotée avec la contrainte :
Remarque : il faut définir les contraintes soit sur le champ soit sur la propriété correspondante mais pas sur les deux à la fois sinon la validation se fera deux fois. Il est préférable de rester consistant et d'utiliser les annotations toujours sur les champs ou toujours sur les getter.
Chaque déclaration d'une contrainte peut redéfinir le message fourni en cas de violation.
L'application de contraintes sur un champ permet de réaliser la validation de la donnée par l'implémentation de l'API de façon indépendante de la forme d'accès à ce champ.
Exemple : |
package com.jmdoudoux.test.validation;
import java.util.Date;
import javax.validation.constraints.NotNull;
public class PersonneBean {
@NotNull
private String nom;
@NotNull
private String prenom;
@Past
private Date dateNaissance;
public PersonneBean(String nom, String prenom, Date dateNaissance) {
super();
this.nom = nom;
prenom = prenom;
this.dateNaissance = dateNaissance;
}
public String getNom() {
return nom;
}
public void setNom(String nom) {
this.nom = nom;
}
public String getPrenom() {
return prenom;
}
public void setPrenom(String prenom) {
prenom = prenom;
}
public Date getDateNaissance() {
return dateNaissance;
}
public void setDateNaissance(Date dateNaissance) {
this.dateNaissance = dateNaissance;
}
}
L'application de ces contraintes peut se faire sur un champ quel que soit sa visibilité (private, protected ou public) mais ne peut pas se faire sur un champ static.
Il est possible de définir les contraintes sur une propriété : dans ce cas, seul le getter doit être annoté.
Exemple : |
package com.jmdoudoux.test.validation;
import java.util.Date;
import javax.validation.constraints.NotNull;
public class PersonneBean {
private String nom;
private String Prenom;
private Date dateNaissance;
public PersonneBean(String nom, String prenom, Date dateNaissance) {
super();
this.nom = nom;
Prenom = prenom;
this.dateNaissance = dateNaissance;
}
@NotNull
public String getNom() {
return nom;
}
public void setNom(String nom) {
this.nom = nom;
}
@NotNull
public String getPrenom() {
return Prenom;
}
public void setPrenom(String prenom) {
Prenom = prenom;
}
@Past
public Date getDateNaissance() {
return dateNaissance;
}
public void setDateNaissance(Date dateNaissance) {
this.dateNaissance = dateNaissance;
}
}
La validation de la donnée par l'implémentation de l'API utilise alors obligatoirement le getter pour obtenir la valeur de la donnée.
La déclaration d'une contrainte peut être faite sur une classe ou une interface. Dans ce cas, la validation se fait sur l'état de la classe ou de la classe qui implémente l'interface.
C'est l'instance de la classe qui sera fournie comme valeur à valider au ConstraintValidator.
Une telle validation peut être requise si elle nécessite l'état de plusieurs données de la classe pour être réalisée.
Lorsqu'un bean hérite d'un autre bean qui contient une définition de contraintes, celles-ci sont héritées.
Exemple : |
package com.jmdoudoux.test.validation;
import java.util.Date;
import javax.validation.constraints.Min;
public class DeveloppeurSeniorBean extends PersonneBean {
private int experience;
public DeveloppeurSeniorBean(String nom, String prenom, Date dateNaissance, int experience) {
super(nom, prenom, dateNaissance);
this.experience = experience;
}
@Min(value=5)
public int getExperience() {
return experience;
}
public void setExperience(int experience) {
this.experience = experience;
}
}
Lors de la validation du bean, les contraintes du bean sont vérifiées mais aussi celles de la classe mère.
Exemple : |
package com.jmdoudoux.test.validation;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
public class TestValidation {
public static void main(String[] args) {
DeveloppeurSeniorBean personne = new DeveloppeurSeniorBean(null, "", new GregorianCalendar(
1965, Calendar.JANUARY, 18).getTime(), 3);
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Set<ConstraintViolation<DeveloppeurSeniorBean>> constraintViolations =
validator.validate(personne);
if (constraintViolations.size() > 0 ) {
System.out.println("Impossible de valider les donnees du bean : ");
for (ConstraintViolation<DeveloppeurSeniorBean> contraintes : constraintViolations) {
System.out.println(contraintes.getRootBeanClass().getSimpleName()+
"." + contraintes.getPropertyPath() + " " + contraintes.getMessage());
}
} else {
System.out.println("Les donnees du bean sont valides");
}
}
}
Résultat : |
Impossible de valider les donnees du bean :
DeveloppeurSeniorBean.experience doit être plus grand que 5
DeveloppeurSeniorBean.nom ne peut pas être nul
Les contraintes sont héritées d'une classe mère mais elles peuvent être redéfinies. Si une méthode est redéfinie, les contraintes de la méthode de la classe mère s'appliquent aussi sauf si une contrainte existante est aussi redéfinie.
L'API propose une validation d'un objet mais permet aussi la validation d'un graphe d'objets composé de l'objet et de tout ou partie de ses objets dépendants.
L'annotation @Valid utilisée sur une dépendance d'un bean permet de demander au moteur de validation de valider aussi la dépendance lors de la validation du bean.
Ce mécanisme est récursif : une dépendance annotée avec @Valid peut elle-même contenir des dépendances annotées avec @Valid. Ainsi, l'ensemble des beans dépendants qui seront validés en même temps que le bean est défini en utilisant l'annotation @Valid sur chacune des dépendances concernées.
Une dépendance annotée avec @Valid est ignorée par le moteur si sa valeur est nulle.
Exemple : |
package com.jmdoudoux.test.validation;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
public class Comite {
@NotNull
@Valid
private PersonneBean president;
@Valid
private PersonneBean tresorier;
@Valid
private PersonneBean secretaire;
public Comite(PersonneBean president, PersonneBean tresorier,
PersonneBean secretaire) {
super();
this.president = president;
this.tresorier = tresorier;
this.secretaire = secretaire;
}
public PersonneBean getPresident() {
return president;
}
public PersonneBean getTresorier() {
return tresorier;
}
public PersonneBean getSecretaire() {
return secretaire;
}
}
La validation du bean échoue si la validation d'une de ses dépendances échoue.
La dépendance peut aussi être une collection typée de beans. Cette collection peut être :
Exemple : |
package com.jmdoudoux.test.validation;
import java.util.ArrayList;
import java.util.List;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
public class Groupe {
@NotNull
private String nom;
private List<PersonneBean> membres = new ArrayList<PersonneBean>();
public Groupe(String nom) {
super();
this.nom = nom;
membres = new ArrayList<PersonneBean>();
}
public String getNom() {
return nom;
}
@NotNull
@Valid
public List<PersonneBean> getMembres() {
return membres;
}
public void ajouter(PersonneBean personne) {
membres.add(personne);
}
public void supprimer(PersonneBean personne) {
membres.remove(personne);
}
}
Si une telle collection est marquée avec l'annotation @Valid, alors toutes les occurrences de la collection seront validées lorsque le bean qui encapsule la collection sera validé.
Exemple : |
package com.jmdoudoux.test.validation;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
public class TestValidationGroupe {
public static void main(String[] args) {
Groupe groupe = new Groupe("Mon groupe");
PersonneBean personne = new PersonneBean(null, null, new GregorianCalendar(
2065, Calendar.JANUARY, 18).getTime());
groupe.ajouter(personne);
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Set<ConstraintViolation<Groupe>> constraintViolations =
validator.validate(groupe);
if (constraintViolations.size() > 0 ) {
System.out.println("Impossible de valider les donnees du bean : ");
for (ConstraintViolation<Groupe> contraintes : constraintViolations) {
System.out.println(contraintes.getRootBeanClass().getSimpleName()+
"." + contraintes.getPropertyPath() + " " + contraintes.getMessage());
}
} else {
System.out.println("Les donnees du groupe sont valides");
}
}
}
Résultat : |
Impossible de valider les donnees du bean :
Groupe.membres[0].nom ne peut pas être nul
Groupe.membres[0].dateNaissance doit être dans le passé
Groupe.membres[0].prenom ne peut pas être nul
Les occurrences null dans une collection sont ignorées lors de la validation.
Dans le cas d'une collection de type Map, seules les valeurs sont validées (Map.Entry) : les clés ne le sont pas.
Lors de la validation, l'annotation @Valid est traitée récursivement dans les dépendances tant que cela ne provoque pas une boucle infinie : le moteur de validation doit ignorer une instance qui a déjà été validée lors du traitement d'un même graphe d'objets.
Bean Validation propose une API pour permettre la validation des contraintes sur les données de façon indépendante de la couche dans laquelle elle est mise en oeuvre.
L'interface Validator définit les fonctionnalités d'un valideur.
Pour valider les contraintes sur les données d'un bean, il faut obtenir une instance de l'interface Validator, utiliser cette instance pour valider les données d'un bean. Les éventuelles erreurs détectées par cette validation sont retournées sous la forme d'un Set d'objets de type ConstraintViolation.
Exemple : |
package com.jmdoudoux.test.validation;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
public class TestValidation {
public static void main(String[] args) {
PersonneBean personne = new PersonneBean("nom1", "prenom1", new GregorianCalendar(
1965, Calendar.JANUARY, 18).getTime());
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Set<ConstraintViolation<PersonneBean>> constraintViolations =
validator.validate(personne);
if (constraintViolations.size() > 0 ) {
System.out.println("Impossible de valider les donnees du bean : ");
for (ConstraintViolation<PersonneBean> contraintes : constraintViolations) {
System.out.println(contraintes.getRootBeanClass().getSimpleName()+
"." + contraintes.getPropertyPath() + " " + contraintes.getMessage());
}
} else {
System.out.println("Les donnees du bean sont valides");
}
}
}
Pour obtenir une instance de Validator fournie par une implémentation de l'API, il faut utiliser une fabrique de type ValidatorFactory.
Le plus simple pour obtenir une instance de cette fabrique est d'utiliser la méthode statique buildDefaultValidatorFactory() de la classe Validation.
Il est alors possible d'utiliser la méthode getValidator() de la fabrique pour obtenir une instance de type Validator.
Exemple : |
package com.jmdoudoux.test.validation;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
public class TestValidation {
public static void main(String[] args) {
...
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
...
}
}
L'interface javax.Validation.Validator est l'élément principal de l'API de validation des contraintes.
L'interface Validator propose des méthodes pour demander la validation de données notamment :
Méthode |
Rôle |
Set<ConstraintViolation<T>> validate(T, Class< ?>...) |
Demander la validation des données d'un bean et éventuellement de ses dépendances |
Set<ConstraintViolation<T>> validateProperty(T, String, Class< ?>...) |
Demander la validation de la valeur d'une propriété d'un bean. Cette méthode est utile pour la validation partielle d'un bean |
Set<ConstraintViolation<T>> validateValue(T, String, Object, Class< ?>...) |
Demander la validation d'une valeur par rapport à une propriété particulière d'un bean |
Si la collection est vide, c'est que la validation a réussi sinon la validation a échoué et la collection contient alors la ou les raisons de l'échec sous la forme d'une occurrence pour chaque contrainte qui n'a pas été validée.
Toutes les méthodes attendent aussi un paramètre de type varargs qui peut être utilisé pour préciser les groupes à valider. Si aucun groupe n'est précisé, c'est le groupe par défaut (javax.validation.Default) qui est utilisé.
La méthode validate() permet de demander la validation des données d'un bean et éventuellement de ses dépendances.
Exemple : |
package com.jmdoudoux.test.validation;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
public class TestValidationGroupe {
public static void main(String[] args) {
Groupe groupe = new Groupe("Mon groupe");
PersonneBean personne = new PersonneBean(null, null, new GregorianCalendar(
2065, Calendar.JANUARY, 18).getTime());
groupe.ajouter(personne);
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Set<ConstraintViolation<Groupe>> constraintViolations =
validator.validate(groupe);
if (constraintViolations.size() > 0 ) {
System.out.println("Impossible de valider les donnees du bean : ");
for (ConstraintViolation<Groupe> contraintes : constraintViolations) {
System.out.println(contraintes.getRootBeanClass().getSimpleName()+
"." + contraintes.getPropertyPath() + " " + contraintes.getMessage());
}
} else {
System.out.println("Les donnees du groupe sont valides");
}
}
}
La méthode validateProperty() permet de valider la valeur d'une propriété d'un bean.
Exemple : |
package com.jmdoudoux.test.validation;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
public class TestValidationProperty {
public static void main(String[] args) {
MonBean monBean = new MonBean(new GregorianCalendar(1980,
Calendar.DECEMBER,
25).getTime());
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Set<ConstraintViolation<MonBean>> constraintViolations =
validator.validateProperty(monBean,
"maValeur");
validator.validate(monBean);
if (constraintViolations.size() > 0) {
System.out.println("Impossible de valider les donnees du bean : ");
for (ConstraintViolation<MonBean> contraintes : constraintViolations) {
System.out.println(" "
+ contraintes.getRootBeanClass().getSimpleName() + "."
+ contraintes.getPropertyPath() + " " + contraintes.getMessage());
}
} else {
System.out.println("Les donnees du bean sont validees");
}
}
}
La méthode validateValue() permet de valider la valeur d'une propriété particulière d'un bean.
Exemple : |
package com.jmdoudoux.test.validation;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
public class TestValidationValue {
public static void main(String[] args) {
Date valeur = new GregorianCalendar(1980, Calendar.DECEMBER, 25).getTime();
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Set<ConstraintViolation<MonBean>> constraintViolations =
validator.validateValue(MonBean.class,
"maValeur",
valeur);
if (constraintViolations.size() > 0) {
System.out.println("Impossible de valider la valeur de la donnee du bean : ");
for (ConstraintViolation<MonBean> contraintes : constraintViolations) {
System.out.println(" "
+ contraintes.getRootBeanClass().getSimpleName() + "."
+ contraintes.getPropertyPath() + " " + contraintes.getMessage());
}
} else {
System.out.println("La valeur de la donnees du bean est validee");
}
}
}
Remarque : la validation des dépendances déclarées avec l'annotation @Valid n'est effective qu'avec la méthode validate().
L'interface ConstraintViolation<T> encapsule les informations relatives à l'échec de la validation d'une contrainte.
Elle propose plusieurs méthodes pour obtenir ces données :
Méthode |
Rôle |
String getMessage() |
Renvoie le message d'erreur interpolé |
String getMessageTemplate() |
Renvoie le message d'erreur non interpolé (généralement la valeur de l'attribut message de la contrainte) |
T getRootBean() |
Renvoie le bean racine qui a été validé (c'est l'objet qui a été passé en paramètre de la méthode validate() de la classe Validator) |
Class<T> getRootBeanClass() |
Renvoie la classe du bean racine qui a été validé |
Object getLeafBean() |
Renvoie l'objet sur lequel la contrainte est appliquée |
Object getInvalidValue() |
Renvoie la valeur qui a fait échouer la contrainte (la valeur passée en paramètre de la méthode isValid() de la classe ConstraintValidator) |
ConstraintDescriptor<?> getConstraintDesrcriptor() |
Renvoie un objet qui encapsule la contrainte |
Comme les contraintes sont définies au niveau des entités du domaine et que les validations peuvent se faire dans toutes les couches de l'application, il faut que les contraintes puissent s'appliquer partout.
Ce n'est pas toujours le cas : c'est possible pour des contrôles de surface mais pour des contraintes plus compliquées ce n'est pas toujours réalisable (par exemple si un accès à la base de données est nécessaire, ...).
De plus, toutes les contraintes d'un bean ne peuvent pas être validée en même temps. Par exemple, un bean qui encapsule les données d'un assistant comportant plusieurs pages. Pour valider les données de la première page avant de passer à la seconde, une validation de l'intégralité des contraintes du bean n'est pas possible puisque les données des autres pages ne sont pas encore renseignées.
Enfin, certaines contraintes ne peuvent être réalisées dans toutes les couches car elles sont trop coûteuses par exemple en ressources ou en temps de traitement.
L'API Bean Validation résoud ces problématiques au travers de la notion de groupes qui contiennent les contraintes à valider.
Les groupes (groups) permettent de restreindre l'ensemble des contraintes qui seront testées durant une validation.
Les groupes sont des types (interfaces ou classes) ce qui permet un typage fort, de faire de l'héritage, de les documenter avec Javadoc et autorise le refactoring grâce à un IDE. Il est possible de définir une hiérarchie de groupes, le plus simple étant d'utiliser une interface de type marqueur.
Exemple : |
package com.jmdoudoux.test.validation;
public interface AssistantEtape1 {
}
package com.jmdoudoux.test.validation;
public interface AssistantEtape2 {
}
package com.jmdoudoux.test.validation;
public interface AssistantEtape3 {
}
La notion de groupe permet de donner une flexibilité à la validation en proposant d'indiquer quelles contraintes doivent être vérifiées lors de la validation. Ainsi, le ou les groupes à valider sont précisés au moment de la demande de validation des contraintes du bean.
L'attribut groups de l'annotation d'une contrainte permet de faire une validation partielle du bean : elle précise le ou les groupes qui sont concernés lors d'une validation.
Exemple : |
package com.jmdoudoux.test.validation;
import javax.validation.constraints.NotNull;
public class DonneesAssistantBean {
/**
* donnees saisies à l'étape 1 de l'assitant
*/
private String donnees1;
/**
* donnees saisies à l'étape 2 de l'assitant
*/
private String donnees2;
/**
* donnees saisie à l'étape 3 de l'assitant
*/
private String donnees3;
@NotNull(groups={AssistantEtape1.class, AssistantEtape2.class, AssistantEtape3.class})
public String getDonnees1() {
return donnees1;
}
public void setDonnees1(String donnees1) {
this.donnees1 = donnees1;
}
@NotNull(groups={AssistantEtape2.class, AssistantEtape3.class})
public String getDonnees2() {
return donnees2;
}
public void setDonnees2(String donnees2) {
this.donnees2 = donnees2;
}
@NotNull(groups={AssistantEtape3.class})
public String getDonnees3() {
return donnees3;
}
public void setDonnees3(String donnees3) {
this.donnees3 = donnees3;
}
}
Les groupes qui doivent être utilisés lors de la validation sont précisés grâce au paramètre parameter de type varargs des méthodes validate(), validateProperty() et validateValue() de la classe Validator.
Exemple : |
package com.jmdoudoux.test.validation;
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
public class TestValidationDonneesAssistantBean {
public static void main(String[] args) {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
DonneesAssistantBean donnees = new DonneesAssistantBean();
donnees.setDonnees1("valeur donnees1");
System.out.println("Validation des données de l'étape 1");
validerDonnees(validator, donnees, AssistantEtape1.class);
donnees.setDonnees2("valeur donnees2");
System.out.println("Validation des données de l'étape 2");
validerDonnees(validator, donnees, AssistantEtape2.class);
donnees.setDonnees3("valeur donnees3");
System.out.println("Validation des données de l'étape 3");
validerDonnees(validator, donnees, AssistantEtape3.class);
}
private static void validerDonnees(Validator validator,
DonneesAssistantBean donnees,
Class<?>... groupes) {
Set<ConstraintViolation<DonneesAssistantBean>> constraintViolations;
constraintViolations = validator.validate(donnees, groupes);
if (constraintViolations.size() > 0) {
System.out.println("Impossible de valider les donnees du bean : ");
for (ConstraintViolation<DonneesAssistantBean> contrainte : constraintViolations) {
System.out.println(" " + contrainte.getRootBeanClass().getSimpleName()
+ "." + contrainte.getPropertyPath() + " "
+ contrainte.getMessage());
}
} else {
System.out.println("Les donnees du bean sont validees");
}
}
}
Chaque contrainte qui n'a pas de groupe explicite est associée au groupe par défaut (javax.validation.Default).
Il est aussi possible d'utiliser les groupes pour les évaluer un par un en conditionnant l'évaluation du suivant au succès de l'évaluation du précédent.
Il est possible d'associer plusieurs contraintes à un groupe sans avoir à déclarer le groupe explicitement dans la déclaration de chaque contrainte.
Chaque contrainte du groupe par défaut contenue dans une interface I est automatiquement associée au groupe I.
Exemple : |
package com.jmdoudoux.test.validation;
import java.util.Date;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Past;
public interface Tracabilite {
@NotNull
@Past
Date getDateCreation();
@NotNull
@Past
Date getDateModif();
@NotNull
Long getUtilisateur();
}
Exemple : |
package com.jmdoudoux.test.validation;
import java.util.Date;
import javax.validation.constraints.NotNull;
public class Operation implements Tracabilite {
private Date dateCreation;
private Date dateModification;
private Long utilisateur;
private String designation;
public Operation(Date dateCreation, Date dateModification, Long utilisateur,
String designation) {
super();
this.dateCreation = dateCreation;
this.dateModification = dateModification;
this.utilisateur = utilisateur;
this.designation = designation;
}
@NotNull
public String getDesignation() {
return this.designation;
}
@Override
public Date getDateCreation() {
return this.dateCreation;
}
@Override
public Date getDateModif() {
return this.dateModification;
}
@Override
public Long getUtilisateur() {
return utilisateur;
}
}
Ceci est pratique pour permettre la validation partielle d'un bean basée sur les fonctionnalités définies dans une interface.
Exemple : |
package com.jmdoudoux.test.validation;
import java.util.Date;
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
public class TestValidationOperation {
public static void main(String[] args) {
Operation operation = new Operation(new Date(), new Date(), 1234l, null);
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
System.out.println("Validation sur le groupe par defaut");
Set<ConstraintViolation<Operation>> constraintViolations =
validator.validate(operation);
if (constraintViolations.size() > 0 ) {
System.out.println("Impossible de valider les donnees du bean : ");
for (ConstraintViolation<Operation> contraintes : constraintViolations) {
System.out.println(contraintes.getRootBeanClass().getSimpleName()+
"." + contraintes.getPropertyPath() + " " + contraintes.getMessage());
}
} else {
System.out.println("Les donnees du groupe sont valides");
}
constraintViolations = validator.validate(operation, Tracabilite.class);
System.out.println("Validation sur le groupe Tracabilite : ");
if (constraintViolations.size() > 0 ) {
System.out.println("Impossible de valider les donnees du bean : ");
for (ConstraintViolation<Operation> contraintes : constraintViolations) {
System.out.println(contraintes.getRootBeanClass().getSimpleName()+
"." + contraintes.getPropertyPath() + " " + contraintes.getMessage());
}
} else {
System.out.println("Les donnees du groupe sont valides");
}
}
}
Résultat : |
Validation sur le groupe par defaut
Impossible de valider les donnees du bean :
Operation.designation ne peut pas être nul
Validation sur le groupe Tracabilite :
Les donnees du groupe sont valides
Par défaut, une donnée est validée sans tenir compte d'un ordre vis-à-vis des groupes auxquelles la contrainte est associée.
Il peut cependant être utile de vouloir contrôler l'ordre d'évaluation des contraintes : par exemple s'il est utile de voir évaluer certaines contraintes avant d'autres.
Pour définir cet ordre particulier dans la validation des groupes, il faut créer un groupe qui va définir une séquence ordonnée d'autres groupes. La définition de cette séquence ce fait avec l'annotation @GroupSequence
Exemple : |
package com.jmdoudoux.test.validation;
import javax.validation.GroupSequence;
@GroupSequence( { MaContrainte1.class, MaContrainte2.class, MaContrainte2.class })
public @interface MonGroupeDeSequence {
}
Lors de l'évaluation des groupes de la séquence, dès que la validation d'un groupe échoue, les autres groupes de la séquence ne sont pas évalués.
Il faut faire attention de ne pas créer une dépendance cyclique entre la définition d'une séquence et les groupes qui composent cette séquence aussi bien directement qu'indirectement sinon une exception de type GroupDefinitionException est levée.
L'interface d'un groupe de séquences ne devra pas avoir de superinterface.
L'annotation @GroupSequence sert aussi à redéfinir le groupe par défaut d'une classe. Il suffit de l'utiliser sur une classe pour remplacer le groupe par défaut (Default.class).
Comme les séquences ne peuvent pas avoir de dépendances circulaires, il n'est pas possible d'inclure le groupe Default dans une séquence.
Par contre, comme les contraintes d'une classe sont associées automatiquement au groupe, il faut obligatoirement ajouter le groupe (la classe elle-même) dans la séquence car les contraintes contenues dans la classe doivent être incluses dans la séquence qui redéfinit le groupe par défaut. Si ce n'est pas le cas, une exception de type GroupDefinitionException est levée lors de la validation de la classe ou de la recherche des contraintes qu'elle contient.
Les spécifications de la JSR 303 définissent un petit ensemble de contraintes que chaque implémentation doit fournir. Celles-ci peuvent être utilisées telles quelles ou dans une composition.
Il est aussi possible pour une implémentation de fournir d'autres contraintes.
La JSR 303 propose en standard plusieurs annotations pour des actions de validations communes.
Annotation |
Rôle |
@Null |
Vérifier que la valeur du type concerné soit null |
@NotNull |
Vérifier que la valeur du type concerné soit non null |
@AssertTrue |
Vérifier que la valeur soit true |
@AssertFalse |
Vérifier que la valeur soit false |
@DecimalMin |
Vérifier que la valeur soit supérieure ou égale à celle fournie sous la forme d'une chaîne de caractères encapsulant un BigDecimal |
@DecimalMax |
Vérifier que la valeur soit inférieure ou égale à celle fournie sous la forme d'une chaîne de caractères encapsulant un BigDecimal |
@Digits |
Vérifier qu'un nombre n'a pas plus de chiffres avant et après la virgule que ceux précisés en paramètre |
@Size |
Vérifier que la taille de la donnée soit comprise en les valeurs min et max fournies |
@Min |
Vérifier que la valeur du type soit un nombre entier dont la valeur doit être supérieure ou égale à la valeur fournie en paramètre |
@Max |
Vérifier que la valeur du type soit un nombre entier dont la valeur doit être inférieure ou égale à la valeur fournie en paramètre |
@Pattern |
Vérifier la conformité d'une chaîne de caractères avec une expression régulière |
@Valid |
Demander la validation des objets dépendant de l'objet à valider |
@Future |
Vérifier que la date soit dans le futur (postérieure à la date courante) |
@Past |
Vérifier que la date soit dans le passé (antérieure à la date courante) |
Ces contraintes sont dans le package javax.validation.constraints.
Les exemples des sections suivantes vont utiliser la classe ci-dessous pour valider les données du bean d'exemple.
Exemple : |
package com.jmdoudoux.test.validation;
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
public class TestValidationMonBean {
public static void main(String[] args) {
MonBean monBean = new MonBean("test");
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Set<ConstraintViolation<MonBean>> constraintViolations =
validator.validate(monBean);
if (constraintViolations.size() > 0 ) {
System.out.println("Impossible de valider les donnees du bean : ");
for (ConstraintViolation<MonBean> contraintes : constraintViolations) {
System.out.println(" "+contraintes.getRootBeanClass().getSimpleName()+
"." + contraintes.getPropertyPath() + " " + contraintes.getMessage());
}
} else {
System.out.println("Les donnees du bean sont validees");
}
}
}
Cette contrainte impose que la valeur du type concerné soit null. Elle peut s'appliquer sur n'importe quel type d'objet.
Exemple : |
package com.jmdoudoux.test.validation;
import javax.validation.constraints.Null;
public class MonBean {
@Null
private String maValeur;
public MonBean(String maValeur) {
super();
this.maValeur = maValeur;
}
public String getMaValeur() {
return maValeur;
}
public void setMaValeur(String maValeur) {
this.maValeur = maValeur;
}
}
Cette contrainte impose que la valeur du type concerné ne soit pas null. Elle peut s'appliquer sur n'importe quel type d'objet.
Exemple : |
package com.jmdoudoux.test.validation;
import javax.validation.constraints.NotNull;
public class MonBean {
@NotNull
private String maValeur;
public MonBean(String maValeur) {
super();
this.maValeur = maValeur;
}
public String getMaValeur() {
return maValeur;
}
public void setMaValeur(String maValeur) {
this.maValeur = maValeur;
}
}
Cette contrainte impose que la valeur du type concerné soit true ou null (la donnée est valide si sa valeur est null).
Exemple : |
package com.jmdoudoux.test.validation;
import javax.validation.constraints.AssertTrue;
public class MonBean {
@AssertTrue
private boolean maValeur;
public MonBean(boolean maValeur) {
super();
this.maValeur = maValeur;
}
public boolean getMaValeur() {
return maValeur;
}
public void setMaValeur(boolean maValeur) {
this.maValeur = maValeur;
}
}
Elle ne peut s'appliquer que sur un type booléen (Boolean et boolean) sinon une exception de type UnexpectedTypeException est levée à la validation ou lors de la recherche des métadonnées.
Exemple : |
package com.jmdoudoux.test.validation;
import javax.validation.constraints.AssertTrue;
public class MonBean {
@AssertTrue
private String maValeur;
public MonBean(String maValeur) {
super();
this.maValeur = maValeur;
}
public String getMaValeur() {
return maValeur;
}
public void setMaValeur(String maValeur) {
this.maValeur = maValeur;
}
}
Résultat : |
Exception in thread "main" javax.validation.UnexpectedTypeException: No validator could
be found for type: java.lang.String
at org.hibernate.validator.engine.ConstraintTree.verifyResolveWasUnique(ConstraintTree.
java:236)
at org.hibernate.validator.engine.ConstraintTree.findMatchingValidatorClass(ConstraintT
ree.java:219)
at org.hibernate.validator.engine.ConstraintTree.getInitializedValidator(ConstraintTree
.java:167)
at org.hibernate.validator.engine.ConstraintTree.validateConstraints(ConstraintTree.jav
a:113)
at org.hibernate.validator.metadata.MetaConstraint.validateConstraint(MetaConstraint.ja
va:121)
at org.hibernate.validator.engine.ValidatorImpl.validateConstraint(ValidatorImpl.java:3
34)
at org.hibernate.validator.engine.ValidatorImpl.validateConstraintsForRedefinedDefaultG
roup(ValidatorImpl.java:278)
at org.hibernate.validator.engine.ValidatorImpl.validateConstraintsForCurrentGroup(Vali
datorImpl.java:260)
at org.hibernate.validator.engine.ValidatorImpl.validateInContext(ValidatorImpl.java:21
3)
at org.hibernate.validator.engine.ValidatorImpl.validate(ValidatorImpl.java:119)
at com.jmdoudoux.test.validation.TestValidationMonBean.main(TestValidationMonBean.java:
20)
Cette contrainte impose que la valeur du type concerné soit false ou null (la donnée est valide si sa valeur est null).
Exemple : |
package com.jmdoudoux.test.validation;
import javax.validation.constraints.AssertTrue;
public class MonBean {
@AssertFalse
private boolean maValeur;
public MonBean(boolean maValeur) {
super();
this.maValeur = maValeur;
}
public boolean getMaValeur() {
return maValeur;
}
public void setMaValeur(boolean maValeur) {
this.maValeur = maValeur;
}
}
Elle ne peut s'appliquer que sur un type booléen (Boolean et boolean) sinon une exception de type UnexpectedTypeException est levée à la validation ou lors de la recherche des métadonnées.
Cette contrainte impose que la valeur du type soit un nombre dont la valeur doit être supérieure ou égale à la valeur fournie en paramètre sous la forme d'un entier de type long. La donnée est valide si sa valeur est null.
Elle ne peut s'appliquer que sur les types : BigDecimal, BigInteger, byte, short, int, long et leurs wrappers respectifs. Le fournisseur n'a pas l'obligation de proposer une implémentation du valideur de la contrainte pour les types double et float.
Exemple : |
package com.jmdoudoux.test.validation;
import javax.validation.constraints.Min;
public class MonBean {
@Min(value=10)
private int maValeur;
public MonBean(int maValeur) {
super();
this.maValeur = maValeur;
}
public int getMaValeur() {
return maValeur;
}
public void setMaValeur(int maValeur) {
this.maValeur = maValeur;
}
}
Cette contrainte impose que la valeur du type soit un nombre dont la valeur doit être inférieure ou égale à la valeur fournie en paramètre sous la forme d'un entier de type long. La donnée est valide si sa valeur est null.
Elle ne peut s'appliquer que sur les types : BigDecimal, BigInteger, byte, short, int, long et leurs wrappers respectifs. Le fournisseur n'a pas l'obligation de proposer une implémentation du valideur de la contrainte pour les types double et float.
Exemple : |
package com.jmdoudoux.test.validation;
import javax.validation.constraints.Min;
public class MonBean {
@Max(value=20)
private int maValeur;
public MonBean(int maValeur) {
super();
this.maValeur = maValeur;
}
public int getMaValeur() {
return maValeur;
}
public void setMaValeur(int maValeur) {
this.maValeur = maValeur;
}
}
Cette contrainte impose que la valeur du type soit un nombre dont la valeur doit être supérieure ou égale à la valeur fournie en paramètre sous la forme d'une chaîne de caractères qui puisse être transformée en BigDecimal. La donnée est valide si sa valeur est null.
Elle ne peut s'appliquer que sur les types : BigDecimal, BigInteger, String, byte, short, int, long et leurs wrappers respectifs. Les types double et float ne sont pas obligatoirement supportés à cause des problèmes d'arrondis mais une implémentation peut proposer une solution par approximation de la valeur selon des règles qui lui sont propres.
Exemple : |
package com.jmdoudoux.test.validation;
import javax.validation.constraints.DecimalMin;
public class MonBean {
@DecimalMin(value="10.5")
private int maValeur;
public MonBean(int maValeur) {
super();
this.maValeur = maValeur;
}
public int getMaValeur() {
return maValeur;
}
public void setMaValeur(int maValeur) {
this.maValeur = maValeur;
}
}
Si le type de données est String alors la valeur contenue doit pouvoir être convertie en BigDecimal sinon la validation échoue.
Exemple : |
package com.jmdoudoux.test.validation;
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
public class TestValidationMonBean {
public static void main(String[] args) {
MonBean monBean = new MonBean("test");
...
}
}
Exemple : |
package com.jmdoudoux.test.validation;
import javax.validation.constraints.DecimalMin;
public class MonBean {
@DecimalMin(value="10.5")
private String maValeur;
public MonBean(String maValeur) {
super();
this.maValeur = maValeur;
}
public String getMaValeur() {
return maValeur;
}
public void setMaValeur(String maValeur) {
this.maValeur = maValeur;
}
}
Résultat : |
Impossible de valider les donnees du bean :
MonBean.maValeur doit être plus grand que 10.5
Cette contrainte impose que la valeur du type soit un nombre dont la valeur doit être supérieure ou égale à la valeur fournie en paramètre sous la forme d'une chaîne de caractères qui puisse être transformée en BigDecimal. La donnée est valide si sa valeur est null.
Elle ne peut s'appliquer que sur les types : BigDecimal, BigInteger, String, byte, short, int, long et leurs wrappers respectifs. Les types double et float ne sont pas obligatoirement supportés à cause des problèmes d'arrondis mais une implémentation peut proposer une solution par approximation de la valeur selon des règles qui lui sont propres.
Exemple : |
package com.jmdoudoux.test.validation;
import javax.validation.constraints.DecimalMax;
public class MonBean {
@DecimalMin(value="99.9")
private int maValeur;
public MonBean(int maValeur) {
super();
this.maValeur = maValeur;
}
public int getMaValeur() {
return maValeur;
}
public void setMaValeur(int maValeur) {
this.maValeur = maValeur;
}
}
Si le type de données est String alors la valeur contenue doit pouvoir être convertie en BigDecimal sinon la validation échoue.
Cette contrainte impose que la taille du type soit un nombre dont la valeur doit être comprise entre les valeurs de type int fournies aux attributs min (valeur par défaut 0) et max (valeur par défaut Integer.MAX_VALUE) incluses.
Les types supportés sont :
La donnée est valide si sa valeur est null.
Exemple : |
package com.jmdoudoux.test.validation;
import javax.validation.constraints.Size;
public class MonBean {
@Size(min=10, max=20)
private String maValeur;
public MonBean(String maValeur) {
super();
this.maValeur = maValeur;
}
public String getMaValeur() {
return maValeur;
}
public void setMaValeur(String maValeur) {
this.maValeur = maValeur;
}
}
L'élément annoté doît être la représentation d'un nombre dont la partie entière et la mantisse ne dépassent pas le nombre maximum de chiffres imposé par les attributs integer et fraction.
Exemple : |
package com.jmdoudoux.test.validation;
import javax.validation.constraints.Digits;
public class MonBean {
@Digits(integer=5, fraction=2)
private String maValeur;
public MonBean(String maValeur) {
super();
this.maValeur = maValeur;
}
public String getMaValeur() {
return maValeur;
}
public void setMaValeur(String maValeur) {
this.maValeur = maValeur;
}
}
Cette contrainte impose que la date vérifiée soit dans le passé.
La date actuelle est celle de la JVM. Le calendrier utilisé est celui correspondant au TimeZone et à la Locale courante.
Les types de données utilisables avec cette annotation sont Date et Calendar.
Exemple : |
package com.jmdoudoux.test.validation;
import java.util.Date;
import javax.validation.constraints.Past;
public class MonBean {
@Past
private Date maValeur;
public MonBean(Date maValeur) {
super();
this.maValeur = maValeur;
}
public Date getMaValeur() {
return maValeur;
}
public void setMaValeur(Date maValeur) {
this.maValeur = maValeur;
}
}
Exemple : |
package com.jmdoudoux.test.validation;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
public class TestValidationMonBean {
public static void main(String[] args) {
MonBean monBean = new MonBean(new GregorianCalendar(1980,
Calendar.DECEMBER, 25).getTime());
...
}
}
Cette contrainte impose que la date vérifiée soit dans le futur.
La date actuelle est celle de la JVM. Le calendrier utilisé est celui correspondant au TimeZone et à la Locale courante.
Les types de données utilisables avec cette annotation sont Date et Calendar.
Exemple : |
package com.jmdoudoux.test.validation;
import java.util.Date;
import javax.validation.constraints.Past;
public class MonBean {
@Futur
private Date maValeur;
public MonBean(Date maValeur) {
super();
this.maValeur = maValeur;
}
public Date getMaValeur() {
return maValeur;
}
public void setMaValeur(Date maValeur) {
this.maValeur = maValeur;
}
}
Cette contrainte permet de valider une valeur par rapport à une expression régulière. La donnée est valide si sa valeur est null. Le format de l'expression régulière est celui utilisé par la classe java.util.regex.Pattern.
Elle ne peut s'appliquer que sur une donnée de type String.
Exemple : |
package com.jmdoudoux.test.validation;
import java.util.Date;
import javax.validation.constraints.Pattern;
public class UtilisateurBean extends PersonneBean {
private String digiCode;
public UtilisateurBean(String nom, String prenom, Date dateNaissance, String digiCode) {
super(nom, prenom, dateNaissance);
this.digiCode = digiCode;
}
@Pattern(regexp="\\d\\d\\d[A-F]",
message="Le digicode doit contenir 3 chiffres et une lettre entre A et F")
public String getDigiCode() {
return digiCode;
}
public void setDigiCode(String digiCode) {
this.digiCode = digiCode;
}
}
L'attribut regex permet de préciser l'expression régulière sur laquelle la donnée sera validée.
L'attribut flags est un tableau de l'énumération Flag qui précise les options à utiliser par la classe Pattern. Les valeurs de l'énumération sont : UNIX_LINES, CASE_INSENSITIVE, COMMENTS, MULTILINE, DOTALL, UNICODE_CASE et CANON_EQ.
L'API Bean Validation propose des contraintes standard mais celles-ci ne peuvent pas répondre à tous les besoins en particulier pour des contraintes spécifiques. L'API propose donc de pouvoir développer et utiliser ses propres contraintes personnalisées.
La création d'une contrainte requiert plusieurs étapes :
Une annotation est considérée comme une contrainte de validation si elle est annotée avec l'annotation javax.validation.Constraint et si sa retention policy est RUNTIME.
L'annotation est définie comme n'importe quelle annotation en utilisant l'annotation @interface et un définissant une méthode pour chaque attribut.
L'annotation de la contrainte doit être annotée avec des méta-annotations comme pour la définition de toutes annotations :
L'utilisation des 3 premières méta-annotations est obligatoire selon les spécifications de l'API Java Bean Validation.
Exemple : |
@java.lang.annotation.Documented
@ConstraintValidator(value = CarteBleueValidator.class)
@java.lang.annotation.Target(value = {java.lang.annotation.ElementType.FIELD})
@java.lang.annotation.Retention(value = java.lang.annotation.RetentionPolicy.RUNTIME)
public @interface CarteBleue
{
String message() default "";
String[] groups() default {};
String bankName() default "";
}
Les annotations standards @Target et @Retention permettent respectivement de préciser le type sur lequel l'annotation peut s'appliquer et la portée d'application de l'annotation qui doit obligatoirement être RUNTIME pour permettre à l'API de fonctionner à l'exécution.
Une annotation relative à une contrainte doit obligatoirement être annotée avec l'annotation @Constraint. Son attribut validatedBy permet de préciser la ou les classes de type ConstraintValidator qui lui sont associées et qui contiennent les traitements de validation à instancier.
La spécification de l'API Bean Validation impose que chaque annotation d'une contrainte définisse obligatoirement trois attributs :
L'attribut message de type String permet de créer le message qui indiquera pourquoi la validation a échouée.
Il est préférable d'utiliser un ResourceBundle pour stocker les messages. Dans ce cas, la valeur de l'attribut message doit contenir la clé entourée d'accolades. Par convention, le nom de la clé doit être composé du nom pleinement qualifié de la classe concaténé avec .message.
Exemple : |
String message() default "{com.acme.constraint.MyConstraint.message}";
L'attribut groups de type Class< ?>[] permet de définir les groupes de contraintes qui seront utilisés lors de la validation. La valeur par défaut doit être un tableau vide : dans ce cas c'est le groupe par défaut qui est utilisé.
Exemple : |
Class<?>[] groups() default {};
Les groupes ont deux utilités principales :
L'attribut payload de type Class< ? extends Payload>[] permet de déclarer des types qui seront associés à la contrainte. La valeur par défaut est un tableau vide.
Exemple : |
Class<? extends Payload>[] payload() default {};
Chaque classe qui est fournie en tant que payload doit implémenter l'interface Payload. Ces données sont typiquement non portables. L'utilisation d'un type permet un typage fort de l'information. Un exemple d'utilisation de ces données peut être un niveau de gravité qui permettra à la couche présentation de préciser la sévérité de la contrainte violée, chaque gravité étant représentée dans sa propre classe.
Exemple : |
package com.jmdoudoux.test.validation;
import javax.validation.Payload;
public class Gravite {
public static class Info implements Payload {};
public static class Attention implements Payload {};
public static class Erreur implements Payload {};
}
Il suffit alors de préciser la ou les classes comme valeur de l'attribut payload
Exemple : |
package com.jmdoudoux.test.validation;
import javax.validation.constraints.NotNull;
public class DonneesBean {
private String valeur1;
private String valeur2;
public DonneesBean(String valeur1, String valeur2) {
super();
this.valeur1 = valeur1;
this.valeur2 = valeur2;
}
@NotNull(message="La saisie de la valeur est obligatoire", payload=Gravite.Erreur.class)
public String getValeur1() {
return valeur1;
}
public void setValeur1(String valeur1) {
this.valeur1 = valeur1;
}
@NotNull(message="La saisie de la valeur est recommandée", payload=Gravite.Info.class)
public String getValeur2() {
return valeur2;
}
public void setValeur2(String valeur2) {
this.valeur2 = valeur2;
}
}
Ces classes peuvent être retrouvées dans un objet de type ConstraintDescriptor encapsulé dans les objets de type ConstraintViolation.
Exemple : |
package com.jmdoudoux.test.validation;
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.Payload;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
public class TestValidationDonneesBean {
public static void main(String[] args) {
DonneesBean donneesBean = new DonneesBean(null, null);
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Set<ConstraintViolation<DonneesBean>> constraintViolations =
validator.validate(donneesBean);
if (constraintViolations.size() > 0 ) {
System.out.println("Impossible de valider les donnees du bean : ");
for (ConstraintViolation<DonneesBean> contrainte : constraintViolations) {
String severite = "";
for (Class<? extends Payload> gravite : contrainte.getConstraintDescriptor()
.getPayload()) {
severite = gravite.getSimpleName();
break;
}
System.out.println(severite + "\t "+contrainte.getRootBeanClass().getSimpleName()+
"." + contrainte.getPropertyPath() + " " + contrainte.getMessage());
}
} else {
System.out.println("Les donnees du bean sont validees");
}
}
}
Résultat : |
Impossible de valider les donnees du bean :
Info DonneesBean.valeur2 La saisie de la valeur est recommandée
Erreur DonneesBean.valeur1 La saisie de la valeur est obligatoire
Il est possible de définir des attributs spécifiques aux besoins de la contrainte.
Le nom des attributs de l'annotation d'une contrainte est soumis à des restrictions :
Exemple : la définition d'une contrainte avec un attribut avec une valeur par défaut |
package com.jmdoudoux.test.validation;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
@Target( { METHOD, FIELD, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = CasseValidator.class)
@Documented
public @interface Casse {
String message() default "La casse de la donnée est erronée";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
boolean majuscule() default false;
}
Exemple : définition d'une contrainte avec un attribut obligatoire |
package com.jmdoudoux.test.validation;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
@Target( { METHOD, FIELD, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = CasseValidator.class)
@Documented
public @interface Casse {
String message() default "La casse de la donnée est erronée";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
boolean majuscule();
}
Chaque contrainte doit être associée avec au moins une classe qui encapsule la logique de validation de la valeur de la donnée associée à la contrainte. Cette association est précisée grâce à l'attribut validatedBy de l'annotation @Constraint que chaque annotation d'une contrainte utilise.
Cette classe doit implémenter l'interface ConstraintValidator qui requiert deux types paramétrés avec des generics :
Si une annotation peut être utilisée sur différents types d'éléments alors il faut créer une implémentation de type ConstraintValidator pour chacun de ces types puisque le type de la donnée à valider est fourni en tant que paramètre générique.
Cette interface définit deux méthodes :
La méthode initialize() est invoquée une fois que le validateur est instancié : elle permet de l'initialiser. Elle reçoit en paramètre l'annotation de la contrainte ce qui permet notamment d'extraire les valeurs des attributs à utiliser pour la validation. L'implémentation doit garantir que cette méthode est invoquée avant toute utilisation de la contrainte.
La méthode isValid() contient les traitements de validation de la valeur de la donnée. Le paramètre value contient la valeur de l'objet à valider. Le paramètre context encapsule les informations sur le contexte dans lequel la validation se fait. Le code de cette méthode doit être thread-safe (elle doit obligatoirement fonctionner dans un environnement multithread) et ne doit pas modifier la valeur de l'objet fourni en paramètre. Elle renvoie un booléen qui précise si la validation a réussie ou non.
Si une exception est levée dans les méthodes initialize() ou isValid() alors celle-ci est propagée sous la forme d'une exception de type ValidationException.
La spécification recommande comme une bonne pratique dans le traitement de validation de considérer la valeur null comme valide. Ceci permet de ne pas faire double emploi avec la contrainte @NotNull qui doit être utilisée si la valeur est invalide lorsqu'elle est null.
Exemple : |
package com.jmdoudoux.test.validation;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class CasseValidator implements ConstraintValidator<Casse, String> {
private boolean majuscule;
public void initialize(Casse constraintAnnotation) {
this.majuscule = constraintAnnotation.majuscule();
}
public boolean isValid(String object,
ConstraintValidatorContext constraintContext) {
if (object == null)
return true;
if (majuscule) {
return object.equals(object.toUpperCase());
} else {
return object.equals(object.toLowerCase());
}
}
}
L'interface ConstraintValidationContext encapsule des données relatives au contexte qui peuvent être exploitées lors de la validation de la contrainte sur une valeur donnée. Un objet de type ConstraintViolation peut ainsi être généré dans le cas où la donnée est invalide. Cette interface définit plusieurs méthodes notamment :
Méthode |
Rôle |
void disableDefaultConstraintViolation() |
Désactiver la génération par défaut de l'objet de type ConstraintViolation |
String getDefaultConstraintMessageTemplate() |
Retourner le message par défaut non interpolé |
ConstraintViolationBuilder buildConstraintViolationWithTemplate(String messageTemplate) |
Une contrainte est associée à une ou plusieurs implémentations de l'interface ConstraintValidator. Une implémentation doit être fournie pour chaque type de données sur lequel la contrainte peut être appliquée. Lors de l'évaluation d'une contrainte, la seule implémentation utilisée est celle correspondant au type de la donnée à valider.
La contrainte doit proposer une implémentation pour le type de l'entité sur laquelle elle est appliquée (classe ou interface, type de la donnée ou renvoyé par le getter). L'implémentation à utiliser est déterminée dynamiquement par le moteur de validation : une exception de type UnexpectedTypeException est levée si l'implémentation correspondante n'est pas trouvée ou si plusieurs le sont.
Toutes les implémentations utilisables lors de la validation de la contrainte doivent être déclarées dans l'attribut validatedBy
Exemple : |
@Target({ METHOD, FIELD, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = {MaContrainteValidatorPourString.class,
MaContrainteValidatorPourDate.class})
@Documented
public @interface MaContrainte {
String message() default "{com.jmdoudoux.test.validation.MaContrainte.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String parametre();
@Target({ METHOD, FIELD, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Documented
@interface List {
MaContrainte[] value();
}
}
Exemple : |
package com.jmdoudoux.test.validation;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class MaContrainteValidatorPourString implements
ConstraintValidator<MaContrainte, String> {
@Override
public void initialize(MaContrainte arg0) {
// TODO A coder
}
@Override
public boolean isValid(String arg0, ConstraintValidatorContext arg1) {
// TODO A coder
return false;
}
}
Exemple : |
package com.jmdoudoux.test.validation;
import java.util.Date;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class MaContrainteValidatorPourDate implements
ConstraintValidator<MaContrainte, Date> {
@Override
public void initialize(MaContrainte arg0) {
// TODO A coder
}
@Override
public boolean isValid(Date arg0, ConstraintValidatorContext arg1) {
// TODO A coder
return false;
}
}
Il est nécessaire de définir un message d'erreur par défaut qui sera utilisé s'il y a une violation de la contrainte lors de la validation d'une valeur d'un bean.
La valeur du message peut être en dur mais il est recommandé d'utiliser un ResourceBundle pour permettre notamment d'internationaliser le message.
L'API propose un mécanisme d'interpolation pour permettre une détermination dynamique du message grâce à :
Pour que l'API recherche le message dans un ResourceBundle, il faut mettre comme valeur de message la clé correspondante entourée par des accolades. Par défaut, l'API recherche les messages dans un fichier nommé ValidationMessages.properties dans le classpath.
Par défaut, le fichier de ce ResourceBundle se nomme ValidationMessages.properties et doit être placé dans un répertoire du classpath. Par convention, il est recommandé que la clé soit composée du nom pleinement qualifié de la contrainte suivi de « .message ».
Exemple : |
com.jmdoudoux.test.validation.Casse.message=La casse de la donnée est erronée
Pour préciser la clé du ResourceBundle à utiliser il suffit, dans la valeur la propriété message, de mettre la clé entourée par des accolades.
Exemple : |
package com.jmdoudoux.test.validation;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
@Target( { METHOD, FIELD, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = CasseValidator.class)
@Documented
public @interface Casse {
String message() default "{com.jmdoudoux.test.validation.Casse.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
boolean majuscule() default false;
}
L'utilisation d'une contrainte personnalisée se fait comme avec des annotations standard : il suffit d'utiliser l'annotation de la contrainte dans le bean sur une des entités sur laquelle elle peut s'appliquer (classe, méthode ou champ).
Exemple : |
package com.jmdoudoux.test.validation;
public class TestBean {
private String codePays;
public TestBean(String codePays) {
super();
this.codePays = codePays;
}
@Casse(majuscule=true)
public String getCodePays() {
return codePays;
}
public void setCodePays(String codePays) {
this.codePays = codePays;
}
}
La validation des beans annotées se fait avec la même API pour les annotations standard ou personnalisées. L'implémentation par défaut de l'API instancie les classes de type ConstraintValidators en utilisant l'introspection.
Exemple : |
package com.jmdoudoux.test.validation;
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
public class TestValidationTestBean {
public static void main(String[] args) {
TestBean bean = new TestBean("fr");
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Set<ConstraintViolation<TestBean>> constraintViolations =
validator.validate(bean);
if (constraintViolations.size() > 0 ) {
System.out.println("Impossible de valider les donnees du bean : ");
for (ConstraintViolation<TestBean> contraintes : constraintViolations) {
System.out.println(contraintes.getRootBeanClass().getSimpleName()
+ "." + contraintes.getPropertyPath()
+ " " + contraintes.getMessage());
}
} else {
System.out.println("Les donnees du groupe sont valides");
}
}
}
Si une contrainte est appliquée sur une entité différente, une exception de type UnexpectedTypeException est levée.
Si la définition d'une contrainte est invalide, une exception de type ConstraintDefinitionException est levée lors de la validation ou lors de la recherche des métadonnées.
Il peut être utile de vouloir appliquer plusieurs fois la même contrainte sur une même donnée avec des propriétés différentes. Ce n'est bien sûr pas utile sur les contraintes @Null ou @NotNull mais cela peut être utile sur la contrainte @Pattern par exemple.
L'utilisation d'une même contrainte plusieurs fois sur une même entité peut aussi être utile par exemple pour appliquer la contrainte sur différents groupes avec différentes propriétés.
C'est une recommandation de la spécification d'associer à une contrainte une annotation correspondante qui gère une version multiusage de l'annotation. L'implémentation de cette recommandation devrait se faire au travers de la définition d'un annotation interne nommée List.
Exemple : |
package com.jmdoudoux.test.validation;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
@Target({ METHOD, FIELD, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = MaContrainteValidator.class)
@Documented
public @interface MaContrainte {
String message() default "{com.jmdoudoux.test.validation.MaContrainte.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String parametre();
@Target({ METHOD, FIELD, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Documented
@interface List {
MaContrainte[] value();
}
}
Pour appliquer plusieurs fois la même contrainte, il faut utiliser l'annotation interne List en lui passant comme valeur d'attribut un tableau des contraintes à appliquer.
Exemple : |
package com.jmdoudoux.test.validation;
public class TestBean {
private String codePays;
public TestBean(String codePays) {
super();
this.codePays = codePays;
}
@MaContrainte.List( {
@MaContrainte(parametre="param1", message="message d'erreur concernant param1"),
@MaContrainte(parametre="param2", message="message d'erreur concernant param2"),
@MaContrainte(parametre="param3", message="message d'erreur concernant param3")
})
public String getCodePays() {
return codePays;
}
public void setCodePays(String codePays) {
this.codePays = codePays;
}
}
Il est fréquemment utile de pouvoir regrouper un ensemble de contraintes sous la forme d'une composition réutilisable. Par exemple, lorsqu'un même champ est utilisé dans deux beans distincts, il n'est pas souhaitable d'avoir à dupliquer toutes les contraintes sur les champs des deux beans pour des raisons évidentes de facilité de maintenance.
Il est possible de définir des contraintes composées (Compound Constraints).
La composition de contraintes permet de rassembler plusieurs contraintes pour en former une seule. La composition de contraintes peut avoir plusieurs utilités :
Pour créer une composition, il faut annoter la composition avec les annotations des contraintes qui vont la composer et l'annotation @Constraint.
Une composition doit aussi définir les attributs message, groups et payload et des attributs dédiés.
Exemple : |
package com.jmdoudoux.test.validation;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
@NotNull
@Size(min = 11, max = 11,
message="La taille du numéro de sécurité sociale est invalide")
@Pattern(regexp = "[12]\\d\\d[01]\\d\\d\\d\\d\\d\\d\\d",
message="Le format du numéro de sécurité sociale est invalide")
@Target( { METHOD, FIELD, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = {})
@Documented
public @interface NumeroSecuriteSociale {
String message() default "Le numéro de sécurité sociale est invalide";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Lorsque la contrainte sera évaluée, toutes les contraintes qui la composent le seront aussi.
Par défaut, chaque contrainte dont la validation échoue génère une erreur.
Exemple : |
package com.jmdoudoux.test.validation;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
public class TestValidationAssureBean {
public static void main(String[] args) {
AssureBean assureBean = new AssureBean("nom1",
"prenom1",
new GregorianCalendar(1964, Calendar.FEBRUARY, 5).getTime(),
"3650900000");
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Set<ConstraintViolation<AssureBean>> constraintViolations =
validator.validate(assureBean);
if (constraintViolations.size() > 0 ) {
System.out.println("Impossible de valider les donnees du bean : ");
for (ConstraintViolation<AssureBean> contraintes : constraintViolations) {
System.out.println(" "+contraintes.getRootBeanClass().getSimpleName()+
"." + contraintes.getPropertyPath() + " " + contraintes.getMessage());
}
} else {
System.out.println("Les donnees du bean sont validees");
}
}
}
Résultat : |
Impossible de valider les donnees du
bean :
AssureBean.numSecSoc Le format du numéro de sécurité sociale est invalide
AssureBean.numSecSoc La taille du numéro de sécurité sociale est invalide
Il peut être souhaité de n'avoir qu'une seul message d'erreur si au moins une contrainte n'est pas validée. L'annotation @ReportAsSingleViolation permet de préciser que si au moins une contrainte de la composition n'est pas validée alors une seule violation est reportée et celles de la composition ne sont pas remontées.
Exemple : |
package com.jmdoudoux.test.validation;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
import javax.validation.ReportAsSingleViolation;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
@NotNull
@Size(min = 11, max = 11,
message="La taille du numéro de sécurité sociale est invalide")
@Pattern(regexp = "[12]\\d\\d[01]\\d\\d\\d\\d\\d\\d\\d",
message="Le format du numéro de sécurité sociale est invalide")
@Target( { METHOD, FIELD, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = {})
@Documented
@ReportAsSingleViolation
public @interface NumeroSecuriteSocial {
String message() default "Le numéro de sécurité sociale est invalide";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Résultat : |
Impossible de valider les donnees du
bean :
AssureBean.numSecSoc Le numéro de sécurité sociale est invalide
Il est possible qu'un attribut d'une composition redéfinisse un ou plusieurs attributs des annotations utilisées dans la composition : dans ce cas, il faut l'annoter avec @OverrideAttribute ou @OverrideAttribute.List pour un tableau d'attributs.
L'annotation redéfinie est précisée par les attributs constraint, qui définit le type, et par name qui identifie l'attribut modifié.
Les types des attributs dans la composition et dans la ou les contraintes doivent être identiques.
Une exception de type ConstraintDefinitionException est levée lors de la validation de la contrainte ou lors de la recherche de ses métadonnées si la définition n'est pas valide.
Exemple : |
package com.jmdoudoux.test.validation;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.OverridesAttribute;
import javax.validation.Payload;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
@NotNull
@Size(message="La taille du numéro de sécurité sociale est invalide")
//@Pattern(regexp = "[12]\\d\\d[01]\\d*",
// message="Le format du numéro de sécurité sociale est invalide")
@Target( { METHOD, FIELD, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = {})
@Documented
public @interface NumeroSecuriteSociale {
String message() default "Le numéro de sécurité sociale est invalide";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
@OverridesAttribute.List( {
@OverridesAttribute(constraint=Size.class, name="min"),
@OverridesAttribute(constraint=Size.class, name="max") } )
int taille() default 11;
}
Chaque contrainte doit définir un message par défaut sous la forme d'une propriété nommée message qui doit avoir une valeur par défaut et qui décrit la raison de l'échec de la validation de la contrainte.
Ce message peut être redéfini au moment de l'utilisation de la contrainte.
L'interface MessageInterpolator définit les méthodes pour transformer un message pour qu'il soit compréhensible par un utilisateur.
Une instance de MessageInterpolator se charge de faire l'interpolation du message : cette interpolation consiste à déterminer le message en effectuant une résolution des chaînes de caractères entre accolades qui font office de paramètres.
Le message est une chaîne de caractères qui peut contenir des paramètres entourés par des accolades. Les chaînes de caractères entre accolades dans le message peuvent avoir plusieurs significations :
Résultat : |
La valeur doit être comprise entre les valeurs {min} et {max}
{com.jmdoudoux.test.validation.monmessage}
Comme les accolades ont une signification particulière, elles doivent être échappées avec un caractère backslash pour être utilisées sous une forme littérale dans le message. Ce caractère lui-même doit être échappé avec un double backslash pour ne pas être interprété.
Il est possible de créer sa propre implémentation de MessageInterpolator pour des besoins spécifiques et de la fournir en paramètre lors de l'instanciation de la fabrique ValidatorFactory.
Par défaut, MessageInterpolator suit l'algorithme suivant :
Lors des recherches dans le ResourceBundle applicatif ou fourni par l'implémentation de la locale utilisée est :
Il est possible de développer et d'utiliser son propre MessageInterpolator pour par exemple prendre en compte une Locale particulière ou obtenir les valeurs des paramètres d'une ressource particulière.
Il faut créer une classe qui implémente l'interface MessageInterpolator. Cette interface définit plusieurs méthodes :
Méthode |
Rôle |
String interpolate(String messageTemplate, Context context) |
Interpoler le message final avec la locale par défaut |
String interpolate(String messageTemplate, Context context, Locale locale) |
Interpoler le message final avec la locale fournie en paramètre |
ConstraintDescriptor<?> getConstraintDescriptor() |
Renvoyer la contrainte dont le message est interpolé |
Object getValidatedValue() |
Renvoyer la valeur en cours de validation |
Un objet de type Contexte encapsule des informations relatives à l'interpolation.
La méthode interpolate() de l'instance de MessageInterpolator est invoquée pour chaque contrainte dont la validation échoue.
Une implémentation de MessageInterpolator devrait être thread safe.
Pour associer un MessageInterpolator spécifique à un Validator, il faut utiliser la méthode messageInterpolator() de la classe Configuration en lui passant l'instance de MessageInterpolator à utiliser. Cet objet Configuration doit ensuite être fourni pour obtenir l'instance de ValidatorFactory.
Il n'y a qu'une seule instance de MessageInterpolator pour un Validator. Il est possible de remplacer cette instance pour une instance de Validator donnée en utilisant la méthode ValidatorFactory.usingContext().messageInterpolator().
Pour obtenir le MessageInterpolator par défaut, il faut invoquer la méthode Configuration.getDefaultMessageInterpolator().
Le bootstrapping propose plusieurs solutions pour obtenir une instance d'une fabrique de type ValidatorFactory qui va permettre de créer une instance de type Validator. Ces mécanismes permettent de découpler l'application de l'implémentation de l'API Bean Validation du fournisseur utilisé.
Le bootstrapping permet de :
Les mécanismes de bootstrap mettent en oeuvre plusieurs interfaces :
Le fichier META-INF/validation.xml peut aussi contenir des données de configuration pour le mécanisme de bootstrap.
La façon la plus facile pour obtenir une instance de la classe Validator est d'utiliser la méthode statique buidDefaultValidatorFactory() de la classe Validation et d'utiliser la fabrique pour créer une instance du type Validator.
Il existe plusieurs autres façons pour obtenir la fabrique :
Une implémentation de l'API Bean Validation peut être découverte grâce à l'utilisation du Java Service Provider.
Pour cela le fournisseur doit fournir un fichier nommé javax.validation.spi.ValidationProvider dans le répertoire META-INF/services du package (jar, war, ...) de l'implémentation.
Ce fichier doit contenir le nom pleinement qualifié de la classe de l'implémentation de ValidationProvider proposée par le fournisseur.
La classe Validation est le point d'entrée pour utiliser l'API bootstraping. Elle propose plusieurs méthodes pour obtenir de façon plus ou moins directe une instance du type ValidatorFactory.
La méthode buildDefaultValidatorFactory() permet d'obtenir l'instance de type ValidatorFactory() par défaut. Elle utilise l'implémentation par défaut de la classe ValidationProviderResolver pour déterminer l'ensemble des implémentations présentes dans le classpath.
Exemple : |
package com.jmdoudoux.test.validation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
public class TestBootstrapBuildDefault {
public static void main(String[] args) {
ValidatorFactory fabrique = Validation.buildDefaultValidatorFactory();
Validator validator = fabrique.getValidator();
...
}
}
Si plusieurs implémentations sont présentes dans le classpath, il n'y a aucune garantie sur celle qui sera choisie lors de l'utilisation de la méthode buildDefaultValidatorFactory() de la classe Validation.
La méthode byDefaultProvider() permet de configurer la création d'une instance de la classe ValidatorFactory personnalisée. Cette méthode renvoie une instance de l'interface GenericBootstrap.
La méthode providerResolver() de l'interface GenericBootstrap permet éventuellement de préciser l'instance de type ValidationProviderResolver fournie en paramètre qui sera utilisée pour déterminer le ValidationProvider à utiliser.
La méthode configure() de l'interface GenericBootstrap crée une instance générique de l'interface Configuration en utilisant la méthode createGenericConfiguration() du premier ValidationProvider trouvé.
L'interface Configuration propose plusieurs méthodes pour préciser une instance des différents types d'interfaces qui seront utilisés par la fabrique (MessageInterpolator, TraversableResolver et ConstraintValidatorFactory).
Exemple : |
package com.jmdoudoux.test.validation;
import javax.validation.Configuration;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import javax.validation.bootstrap.GenericBootstrap;
public class TestBootstratByDefaultProvider {
public static void main(String[] args) {
GenericBootstrap bootstrap = Validation.byDefaultProvider();
Configuration<?> configuration = bootstrap.configure();
ValidatorFactory factory = configuration.buildValidatorFactory();
Validator validator = factory.getValidator();
}
}
Il est possible de fournir sa propre instance de ValidationProviderResolver.
La méthode buildDefaultValidatorFactory() est équivalente à une invocation de Validation.byDefaultProvider().configure().buildValidatorFactory().
La méthode byProvider() permet d'obtenir une instance de l'interface ProviderSpecificBootstrap pour une instance de configuration qui soit spécifique à l'implémentation précise fournie en paramètre. Sa méthode configure() permet d'obtenir une instance de l'interface Configuration typée avec l'implémentation en utilisant la méthode createSpecializedConfiguration() de l'instance de ValidationProvider.
Cette méthode est particulièrement utile pour obtenir une instance d'une implémentation particulière alors que plusieurs implémentations sont présentes dans le classpath.
Exemple : obtenir une instance de ValidatorFactory de l'implémentation de référence
Exemple : |
package com.jmdoudoux.test.validation;
import javax.validation.Configuration;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import javax.validation.bootstrap.ProviderSpecificBootstrap;
import org.hibernate.validator.HibernateValidator;
import org.hibernate.validator.HibernateValidatorConfiguration;
public class TestBootstrapByProvider {
public static void main(String[] args) {
ProviderSpecificBootstrap<HibernateValidatorConfiguration> psb =
Validation.byProvider(HibernateValidator.class);
Configuration configuration = psb.configure();
ValidatorFactory fabrique = configuration.buildValidatorFactory();
Validator validator = fabrique.getValidator();
}
}
L'instance de l'interface Validator obtenue de la fabrique doit être thread safe et peut donc être mise en cache.
L'interface ValidationProvider définit les fonctionnalités qu'une implémentation doit fournir pour être utilisée par l'API de bootstrap.
L'interface ValidationProviderResolver définit les fonctionnalités pour rechercher les implémentations de l'API Bean Validation.
Par défaut, les implémentations sont déterminées en utilisant le mécanisme du Java Service Provider. Chaque fournisseur doit fournir un fichier javax.validation.spi.ValidationProvider dans le sous-répertoire META-INF/services du jar qui contient le nom pleinement qualifié de la classe implémentant l'interface javax.validation.spi.ValidationProvider.
L'implémentation par défaut de l'interface ValidationProviderResolver recherche dans le classpath toutes les implémentations qui définissent un service.
L'interface ValidationProviderResolver définit une seule méthode : getValidationProviders() qui renvoie une collection de type List<ValidationProvider<?>> contennant la liste des implémentations utilisables.
Pour des cas spécifiques (utilisation d'un classloader spécifique comme avec OSGi, impossibilité d'utiliser le Java Service Provider, ...), il est possible de développer sa propre implémentation de l'interface ValidationProviderResolver.
Cette interface a pour rôle de lier l'API de bootstrap et l'implémentation.
La signature de cette interface est typée avec un generic dont le type doit hériter de Configuration :
public interface ValidationProvider<T extends Configuration<T>>
Cette interface définit plusieurs méthodes :
Méthodes |
Rôle |
T createSpecializedConfiguration( BootstrapState state) |
Renvoie une instance spécifique à l'implémentation |
Configuration<?> createGenericConfiguration(BootstrapState state) |
Renvoie une instance de Configuration générique qui n'est donc pas liée à l'implémentation |
ValidatorFactory buildValidatorFactory(ConfigurationState configurationState) |
Renvoie une instance initialisée du type ValidatorFactory |
Une instance de cette interface permet d'identifier une implémentation particulière de l'API Bean Validation.
Une implémentation de l'API doit fournir une classe implémentant cette interface avec un constructeur sans argument et fournir un fichier javax.validation.spi.ValidationProvider dans le répertoire META-INF/services qui doit avoir le nom pleinement qualifié de cette classe.
Il est possible d'avoir besoin de sa propre implémentation de l'interface MessageInterpolator.
Il faut fournir une instance de cette classe à la méthode messageInterpolator() de l'instance de Configuration utilisée pour obtenir une instance de la ValidationFactory. Ainsi, toutes les instances de Validator créées par la fabrique utiliseront le MessageInterpolator personnalisé.
Il est recommandé qu'une implémentation délègue à la fin de ses traitements une invocation du MessageInterpolator par défaut pour garantir que les règles par défaut soient prises en compte. Pour obtenir une instance du MessageInterpolator par défaut il faut utiliser la méthode Configuration.getDefaultMessageInterpolator().
L'interface TraversableResolver a pour but de restreindre l'accès à certaines propriétés lors de la validation d'un bean. Un exemple d'utilisation peut être le besoin de ne pas valider les données d'une propriété d'un bean de type entité dont le chargement est tardif (lazy loading). Au moment de la validation du bean, les données de cette propriété peuvent ne pas être chargée, rendant leur validation erronée.
Cette interface définit plusieurs méthodes :
Méthode |
Rôle |
boolean isReachable(Object traversableObject, Path.Node traversableProperty, Class<?> rootBeanType, Path pathToTraversableObject, ElementType elementType); |
Permet de déterminer si le moteur de validation peut accéder à la valeur de la propriété |
boolean isCascadable(Object traversableObject, Path.Node traversableProperty, Class<?> rootBeanType, Path pathToTraversableObject, ElementType elementType); |
Permet de déterminer si le moteur de validation peut valider la propriété marquée avec l'annotation @Valid. Cette méthode n'est invoquée que si l'invocation de la méthode isReachable() pour la propriété a renvoyé true |
Pour créer la fabrique de type ValidatorFactory, il est possible de définir sa propre implémentation de l'interface TrasersableResolver et de fournir cette instance en paramètre de la méthode traversableResolver() de la Configuration utilisée.
Une implémentation de l'interface TraversableResolver doit être thread safe.
Une instance d'un valideur de contraintes est créée par une fabrique de type ConstraintValidatorFactory.
L'interface ConstraintValidatorFactory ne définit qu'une seule méthode :
Méthode |
Rôle |
<T extends ConstraintValidator<?,?>> T getInstance(Class<T> key) |
renvoie une instance de l'interface ConstraintValidator |
Il est recommandé que la fabrique utilise le constructeur par défaut pour créer l'instance. Elle ne devrait pas mettre en cache les instances créées.
Si une exception est levée dans la méthode getInstance(), celle-ci est propagée sous la forme d'une exception de type ValidationException.
Le but d'une instance de l'interface ValidatorFactory est de proposer une fabrique pour créer et initialiser des objets de type Validator. Chaque instance de type Validator est créée pour un MessageInterpolator, un TraversableResolver et un ConstraintValidatorFactory donnés.
L'interface ValidatorFactory définit plusieurs méthodes.
La méthode getValidator() renvoie l'instance créée par la fabrique : celle-ci peut être stockée dans un pool.
La méthode getMessageInterpolator() renvoie l'instance de type MessageInterpolator utilisée par la fabrique.
La méthode getTraversalResolver() renvoie l'instance de type TraversalResolver utilisée par la fabrique.
La méthode getConstraintValidatorFactory() renvoie l'instance de type ConstraintValidatorFactory utilisée par la fabrique.
La méthode unwrap() permet un accès à un objet spécifique à l'implémentation qui peut encapsuler des données complémentaires. Son utilisation rend le code non portable.
La méthode usingContext() renvoie un objet de type ValidatorContext qui peut encapsuler des informations de configuration. Lors de la création des instances de type Validator notamment une instance de type MessageInterpolator, TraversableResolver ou ConstraintValidatorFactory, ces informations seront utilisées à la place de celles de la fabrique.
Un objet de type ValidatorFactory est créé par un objet de type Configuration.
Une implémentation de ValidatorFactory doit être thread safe.
Le but d'une instance de Configuration est de définir les différentes entités utiles à une instance de ValidatorFactory et de permettre de créer une telle instance en ayant sélectionné l'implémentation de l'API à utiliser.
Par défaut, l'implémentation à utiliser est déterminée par :
La signature de l'interface est :
public interface Configuration<T extends Configuration<T>>
Cette interface propose plusieurs méthodes notamment :
Méthodes |
Rôle |
T messageInterpolator(MessageInterpolator interpolator) |
Définir l'instance de type MessageInterpolator qui sera utilisée par la fabrique. Si cette méthode n'est pas utilisée ou que la valeur null lui est passée en paramètre alors c'est l'instance par défaut de l'implémentation qui sera utilisée |
T constraintValidatorFactory(ConstraintValidatorFactory constraintValidatorFactory) |
Définir l'instance de type ConstraintValidatorFactory qui sera utilisée par la fabrique. Si cette méthode n'est pas utilisée ou que la valeur null lui est passée en paramètre alors c'est l'instance par défaut de l'implémentation qui sera utilisée |
ValidatorFactory buildValidatorFactory() |
Créer une instance de type ValidatorFactory. Le fournisseur à utiliser est déterminé et la méthode buildValidatorFactory de son instance de type ValidationProvider est invoquée |
T ignoreXmlConfiguration() |
Demander de ne pas tenir compte du contenu du fichier META-INF/validation.xml |
T traversableResolver(TraversableResolver resolver) |
Définir l'instance de type TraversableResolver qui sera utilisée par la fabrique. Si cette méthode n'est pas utilisée ou que la valeur null lui est passée en paramètre alors c'est l'instance par défaut de l'implémentation qui sera utilisée |
T addProperty(String name, String value) |
Définir une propriété spécifique à l'implémentation |
MessageInterpolator getDefaultMessageInterpolator() |
Renvoyer l'implémentation par défaut du type MessageInterpolator |
ConstraintValidatorFactory getDefaultConstraintValidatorFactory() |
Renvoyer l'implémentation par défaut du type ConstraintValidatorFactory |
Les méthodes qui permettent de définir des données renvoient le type en paramètre du generic pour permettre de chaîner leurs invocations.
La détermination de l'implémentation à utiliser suit plusieurs règles ordonnées :
Une instance de type Configuration est créée grâce à la classe ValidationProvider.
Une instance de Configuration est utilisée par la classe Validation.
L'utilisation de ce fichier est ignorée si la méthode ignoreXMLConfiguration() de l'instance de type Configuration est invoquée.
L'utilisation du fichier META-INF/validation.xml est optionnelle mais elle permet de facilement définir l'implémentation de l'API Bean Validation qui doit être utilisée et permet de la configurer.
Un seul fichier META-INF/validation.xml doit être présent dans le classpath sinon une exception de type ValidationException est levée.
Ce fichier au format XML possède un tag racine nommé validation-config qui peut avoir plusieurs tags fils.
Tag |
Rôle |
default-provider |
Indiquer le nom pleinement qualifié de l'implémentation du type ValidationProvider du fournisseur à utiliser |
message-interpolator |
Indiquer le nom pleinement qualifié de l'implémentation du type MessageInterpolator à utiliser (optionnel) |
traversable-resolver |
Indiquer le nom pleinement qualifié de l'implémentation du type TraversableResolver à utiliser (optionnel) |
constraint-validator-factory |
Indiquer le nom pleinement qualifié de l'implémentation du type ConstraintValidatorFactory à utiliser (optionnel) |
constraint-mapping |
Indiquer le chemin d'un fichier XML de mapping (optionnel) |
property |
Indiquer une propriété spécifique à une implémentation sous la forme d'une paire clé/valeur (optionnel) |
Exemple : demander l'utilisation de l'implémentation de référence (Hibernate Validator)
Exemple : |
<?xml version="1.0" encoding="UTF-8"?>
<validation-config
xmlns="http://jboss.org/xml/ns/javax/validation/configuration"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation=
"http://jboss.org/xml/ns/javax/validation/configuration validation-configuration-1.0.xsd">
<default-provider>org.hibernate.validator.HibernateValidator</default-provider>
</validation-config>
La suite de cette section sera développée dans une version future de ce document |
Les spécifications de l'API Bean Validation proposent une API qui permet de rechercher les métadonnées relatives aux contraintes définies dans les beans stockés dans un repository.
Cette API est destinée à être utilisée par des outils ou pour l'intégration dans des frameworks ou des bibliothèques.
L'interface Validator propose la méthode getConstraintsForClass() qui attend en paramètre un objet de type Class<?> et renvoie un objet immuable de type BeanDescriptor contenant une description des contraintes de la classe concernée. La méthode getContraintsForProperty() attend en paramètre le nom de la propriété et renvoie un objet immuable de type BeanDescriptor qui contient une description des contraintes de la propriété concernée.
Une instance de type BeanDescriptor encapsule une description des contraintes du bean et propose un accès au métadonnées de ces contraintes.
Si la définition ou la déclaration d'une contrainte contenue dans la classe est invalide alors une exception de type ValidationException ou une de ses sous-classes telles que ConstraintDefinitionException, ConstraintDeclarationException ou UnexpectedTypeException est levée.
L'interface javax.validation.metadata.ElementDescriptor encapsule la description d'un élément de la classe qui possède une ou plusieurs contraintes.
La méthode hasConstraint() renvoie un booléen qui précise si l'élément (classe, champ ou getter) est soumis à au moins une contrainte.
La méthode Set<ConstraintDescriptor<?>> getConstraintDescriptors() renvoie une collection des descriptions des contraintes associées à l'élément.
Un objet de type ConstraintDescriptor encapsule les informations relatives à une contrainte.
La méthode findConstraints() qui renvoie une instance de type ConstraintFinder permet de rechercher des contraintes selon certains critères.
L'interface ConstraintFinder définit plusieurs méthodes qui permettent de préciser les critères de recherche :
Méthode |
Rôle |
ConstraintFinder unorderedAndMatchingGroup(Class<?> ...) |
Filtre sur les groupes déclarés dans la contrainte |
ConstraintFinder declaredOn(ElementType ...) |
Filtre sur les types d'éléments sur lesquels la contrainte est appliquée (ElementType.FIELD, ElementType.METHOD, ElementType.TYPE) |
ConstraintFinder lookingAt(Scope) |
Filtre sur la portée de la recherche (Scope.LOCAL_ELEMENT ou Scope.HIERARCHY) |
L'interface javax.validation.meta.BeanDescriptor qui hérite de l'interface ElementDescriptor encapsule un bean présentant une ou plusieurs contraintes.
Méthode |
Rôle |
boolean isBeanConstrained() |
renvoie un booléen qui précise si le bean contient une contrainte sur lui-même, sur une de ses propriétés ou si une de ses propriétés est marquée avec l'annotation @valid. Lorsqu'elle renvoie false, le moteur de validation ignore ce bean lors de ses traitements |
PropertyDescriptor getConstraintsForProperty(String propertyName) |
renvoie un objet qui contient la description de la propriété dont le nom est fourni en paramètre. Renvoie null si la propriété n'existe pas, si elle n'a pas de contrainte ou si elle n'est pas marquée avec @Valid. |
Set<PropertyDescriptor> getConstrainedProperties() |
renvoie une collection des descripteurs de propriétés qui présentent au moins une contrainte ou qui sont marquées avec l'annotation @Valid. |
L'interface javax.validation.metadata.PropertyDescriptor qui hérite de l'interface ElementDescriptor encapsule une propriété qui présente au moins une contrainte.
Méthode |
Rôle |
boolean isCascaded() |
Renvoie true si la propriété est marquée avec l'annotation @Valid |
String getPropertyName() |
Renvoie le nom de la propriété |
L'interface javax.validation.metadata.ConstraintDescriptor<T extends Annotation> décrit une annotation d'une contrainte.
Méthode |
Rôle |
T getAnnotation() |
Renvoie l'annotation de la contrainte |
Set<Class< ?>> getGroups |
Renvoie une collection des groupes définis dans l'annotation. Si aucun groupe n'est défini alors c'est le groupe par défaut Default qui est retourné |
SetClass< ? extends Payload>> getPayload() |
Renvoie une collection des données supplémentaires définies dans l'annotation |
List<Class< ? extends ConstraintValidator<T, ?>>> getConstraintValidatorClasses() |
Renvoie une collection des classes qui implémentent la logique de validation des données |
Map<String, Object> getAttributes |
Renvoie une collection de type Map qui contient les attributs de l'annotation : la clé contient le nom de l'attribut, la valeur contient sa valeur |
Set<ConstraintDescriptor<?>> getComposingConstraints() |
Renvoie une collection des contraintes qui sont dans la composition ou un ensemble vide si la contrainte n'est pas composée |
boolean isReportAsSingleViolation() |
Renvoie true si la contrainte est annotée avec @ReportAsSingleViolation |
L'exemple de cette section va rechercher les contraintes déclarées sur une propriété d'un bean.
Exemple : |
package com.jmdoudoux.test.validation;
import java.lang.annotation.ElementType;
import java.util.Set;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import javax.validation.groups.Default;
import javax.validation.metadata.ConstraintDescriptor;
import javax.validation.metadata.PropertyDescriptor;
import javax.validation.metadata.Scope;
public class TestMetaData {
public static void main(String[] args) {
Set<ConstraintDescriptor<?>> contraintes = null;
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
// obtenir le descripteur de la propriété
PropertyDescriptor pd = validator.getConstraintsForClass(PersonneBean.class)
.getConstraintsForProperty("nom");
// affichage de toutes les contraintes
contraintes = pd.getConstraintDescriptors();
System.out.println("Nombre de contraintes=" + contraintes.size());
afficher(contraintes);
// recherche des contraintes
contraintes = pd.findConstraints()
.declaredOn(ElementType.METHOD)
.unorderedAndMatchingGroups(Default.class)
.lookingAt(Scope.LOCAL_ELEMENT)
.getConstraintDescriptors();
System.out.println("Nombre de contraintes trouvees=" + contraintes.size());
afficher(contraintes);
}
private static void afficher(Set<ConstraintDescriptor<?>> constraintes) {
for (ConstraintDescriptor<?> contrainte : constraintes) {
System.out.println(" " + contrainte.getAnnotation().toString());
}
}
}
Résultat : |
Nombre de contraintes=2
@javax.validation.constraints.NotNull(message={javax.validation.constraints.NotNull.message},
payload=[], groups=[])
@javax.validation.constraints.Size(message={javax.validation.constraints.Size.message},
min=0, max=50, payload=[], groups=[])
Nombre de contraintes trouvees=2
@javax.validation.constraints.NotNull(message={javax.validation.constraints.NotNull.message},
payload=[], groups=[])
@javax.validation.constraints.Size(message={javax.validation.constraints.Size.message},
min=0, max=50, payload=[], groups=[])
Les spécifications proposent une extension, dont l'implémentation par un fournisseur est optionnelle, qui permet d'utiliser les mécanismes de définition, de déclaration et de validation des contraintes au niveau des paramètres d'une méthode.
Cette extension peut être exploitée par exemple dans des aspects ou dans un interceptor.
Elle peut être utilisée pour valider les paramètres en entrée ou la valeur de retour d'une méthode lorsque celle-ci est invoquée : la validation doit être appliquée autour de l'invocation de la méthode.
La spécification définit plusieurs méthodes dans l'interface Validator pour permettre la validation des paramètres.
Méthode |
Rôle |
<T> Set<ConstraintViolation<T>> validateParameters(Class<T> class, Method method, Object[] parameterValues, Class<?> ... groups); |
Valider chacune des valeurs passées selon les contraintes des paramètres de la méthode |
<T> Set<ConstraintViolation> validateParameter(Class<T> class, Method method, Object parameterValue, int parameterIndex, Class<?>... groups); |
Valider la valeur d'un paramètre selon les contraintes de celui dont l'index est fourni |
<T> Set<ConstraintViolation> validateReturnedValue(Class<T> class, Method method, Object returnedValue, Class<?>... groups); |
Valider la valeur de retour de la méthode |
<T> Set<ConstraintViolation> validateParameters(Class<T> class, Constructor constructor, Object[] parameterValues, Class<?> ... groups); |
Valider chacune des valeurs passées selon les contraintes des paramètres du constructeur |
<T> Set<ConstraintViolation> validateParameter(Class<T> class, Constructor constructor, Object parameterValue, int parameterIndex, Class<?>... groups); |
Valider la valeur d'un paramètre selon les contraintes définies sur celui dont l'index est fourni |
Les contraintes appliquées sur un paramètre de la méthode ou d'un constructeur seront évaluées. Si l'annotation @Valid est utilisée sur un paramètre alors ce sont les contraintes contenues dans la classe du paramètre qui seront évaluées lors de la validation.
La version 4.0 du projet Hibernate Validator est l'implémentation de référence de la JSR 303.
Pour mettre en oeuvre Hibernate Validator, il faut :
Les avantages sont :
Les inconvénients sont :
Il existe aussi plusieurs manques dans la JSR 303 notamment :
Il existe plusieurs autres frameworks pour la validation des données.
Framework |
Description |
Commons-Validator |
http://commons.apache.org/validator/ Ce framework du projet Apache Commons propose un moteur de validation et des routines de validation standard |
Oval |
Ce framework open source utilise les annotations pour la déclaration de contraintes sur n'importe quel objet Java. |
iScreen |
http://iscreen.sourceforge.net/docs/index.phpl Ce framework open source utilise les annotations pour la déclaration de contraintes sur n'importe quel objet Java. |
agimatec-validation |
http://code.google.com/p/agimatec-validation/ Ce framework open source implémente la JSR 303 |