IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Développons en Java v 2.20   Copyright (C) 1999-2021 Jean-Michel DOUDOUX.   
[ Précédent ] [ Sommaire ] [ Suivant ] [ Télécharger ]      [ Accueil ] [ Commentez ]


 

103. La validation des données

 

chapitre 1 0 3

 

Niveau : niveau 4 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 :

  • Présentation
  • Service
  • Métier
  • DAO
  • Dans la base de données par des contraintes d'intégrités

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 :

  • temps requis
  • source d'erreurs
  • difficultés de maintenance

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 :

  • fournir des validateurs classiques
  • permettre de définir ses propres validateurs
  • faciliter l'application de ces validateurs sur des données.

Ce chapitre contient plusieurs sections :

 

103.1. Quelques recommandations sur la validation des données

Les validations de données sont utiles dans plusieurs endroits d'une application et ce quel que soit le type d'applications :

  • couche présentation : pour informer le plus rapidement possible l'utilisateur de la saisie d'une donnée erronée, généralement ce sont des contrôles de surface
  • couche service : validation des données reçues et qui n'ont pas été forcément validées par une IHM
  • couche métier : validation des données traitées qui peuvent nécessiter un accès aux données
  • couche accès aux données (DAO) : validation des données avant leur envoi dans la base de données

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, ...).

 

103.2. L'API Bean Validation (JSR 303)

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 :

  • il existe déjà plusieurs frameworks open source de validation de données
  • les validations se font dans toutes les couches, pas toujours de façon cohérente et fréquemment par duplication de code
  • plusieurs technologies des plate-formes Java ont besoin d'un framework de validation : JPA, JSF, ...

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.

 

103.2.1. La présentation de l'API

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.

 

103.2.1.1. Les objectifs de l'API

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 :

  • de fournir un ensemble de contraintes standard
  • de déclarer les contraintes sans avoir à écrire de code explicitement
  • de valider un objet par rapport à ses contraintes et à ses valeurs grâce à un moteur de validation
  • de définir des contraintes personnalisées pour rendre le framework extensible
  • de fournir une API de recherche des contraintes sur un type

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 :

  • Proposer une spécification relative à la validation de données dans les applications Java
  • Fournir une API qui soit indépendante d'une architecture
  • Etre utilisable dans toutes les couches Java d'une application : elle est utilisable côté client ou serveur
  • Standardiser la déclaration des contraintes en privilégiant les annotations au niveau de la classe (généralement un bean qui encapsule une entité du domaine)
  • Définir des contraintes communes fournies en standard
  • Fournir un mécanisme standard pour valider les contraintes
  • Etre facile à utiliser et extensible
  • Fournir une API qui permette de rechercher les contraintes exploitables notamment par des frameworks

 

103.2.1.2. Les éléments et concepts utilisés par l'API

L'API Java Bean Validation utilise plusieurs éléments et concepts lors de sa mise en oeuvre :

  • une contrainte est une restriction appliquée sur la valeur d'un champ ou d'une propriété d'une instance d'un bean.
  • la déclaration d'une contrainte assigne une contrainte à un bean, un champ ou une propriété en utilisant une annotation ou grâce à un fichier XML.
  • une implémentation de l'interface ConstraintValidator encapsule les traitements de validation d'une donnée ou du bean.

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.

 

103.2.1.3. Les contraintes et leur validation avec l'API

Une contrainte est composée de deux éléments :

  • une annotation : utilisée par le développeur pour déclarer ses contraintes
  • une classe de type Validator : utilisée par l'API pour valider les données selon les annotations utilisées

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 :

  • par combinaison d'autres contraintes sous la forme d'une composition de contraintes qui est un ensemble de contraintes utilisées comme une seule
  • par la définition de ses propres contraintes

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.

 

103.2.1.4. La mise en oeuvre générale de l'API

La JSR 303 propose de standardiser les validations avec les spécifications d'une API composée de plusieurs parties :

  • des annotations sur les entités du domaine ce qui permet de centraliser ces validations sans alourdir les beans avec beaucoup de code
  • une API pour valider les contraintes
  • une API dédiée à l'obtention des métadonnées des contraintes

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.

 

103.2.1.5. Un exemple simple de mise en oeuvre

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

 

103.2.2. La déclaration des contraintes

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 :

  • si la contrainte est définie sur la classe ou une interface de la classe alors c'est l'instance de classe qui est fournie comme valeur
  • si la contrainte est définie sur un champ alors c'est la valeur du champ qui est fournie
  • si la contrainte est définie sur le getter d'une propriété alors c'est la valeur de retour de ce getter qui est fournie

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.

 

103.2.2.1. La déclaration des contraintes sur les champs

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.

 

103.2.2.2. La déclaration des contraintes sur les propriétés

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.

 

103.2.2.3. La déclaration des contraintes sur une classe

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.

 

103.2.2.4. L'héritage de contraintes

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.

 

103.2.2.5. Les contraintes de validation d'un ensemble d'objets

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 :

  • un tableau
  • une implémentation de java.lang.Iterable (collection, List, Set, ...)
  • une implémentation de java.util.Map
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.

 

103.2.3. La validation des contraintes

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");
    }
  }
}

 

103.2.3.1. L'obtention d'un valideur

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();

...
  }
}

 

103.2.3.2. L'interface Validator

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é.

 

103.2.3.3. L'utilisation d'un valideur

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().

 

103.2.3.4. L'interface ConstraintViolation

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

 

103.2.3.5. La mise en oeuvre des groupes

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.

 

103.2.3.6. Définir et utiliser un groupe implicite

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

 

103.2.3.7. La définition de l'ordre des validations

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.

 

103.2.3.8. La redéfinition du groupe par défaut

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.

 

103.2.4. Les contraintes standard

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");
    }
  }
}

 

103.2.4.1. L'annotation @Null

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;
  }
}

 

103.2.4.2. L'annotation @NotNull

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;
  }
}

 

103.2.4.3. L'annotation @AssertTrue

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)

 

103.2.4.4. L'annotation @AssertFalse

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.

 

103.2.4.5. L'annotation @Min

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;
  }
}

 

103.2.4.6. L'annotation @Max

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;
  }
}

 

103.2.4.7. L'annotation @DecimalMin

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

 

103.2.4.8. L'annotation @DecimalMax

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.

 

103.2.4.9. L'annotation @Size

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 :

  • String : c'est la taille de la chaîne qui est évaluée
  • Tableau : c'est la taille du tableau qui est évaluée
  • Collection : c'est la taille de la collection qui est évaluée
  • Map : c'est la taille de la Map qui est évaluée

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;
  }
}

 

103.2.4.10. L'annotation @Digits

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;
  }
}

 

103.2.4.11. L'annotation @Past

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());
...

  }
}

 

103.2.4.12. L'annotation @Future

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;
  }
}

 

103.2.4.13. L'annotation @Pattern

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.

 

103.2.5. Le développement de contraintes personnalisées

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 :

  • créer l'annotation pour la contrainte
  • implémenter la contrainte sous la forme d'un Validator
  • définir le message d'erreur par défaut

 

103.2.5.1. La création de l'annotation

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 :

  • @Target({METHOD, FIELD, ANNOTATION_TYPE }) : permet de préciser le type d'entité sur lequel l'annotation peut être utilisée
  • @Retention(RUNTIME) : permet de préciser comment sera exploitée l'annotation. Dans le cas d'une contrainte de l'API Bean Validation, il faut obligatoirement utiliser la valeur RUNTIME pour permettre à l'API d'utiliser l'introspection à l'exécution
  • @Constraint(validatedBy = CheckCaseValidator.class): permet de préciser la ou les classes de type Validator qui encapsulent les traitements de validation grâce à l'attribut validatedBy
  • @Documented : Permet de préciser que l'utilisation de cette annotation sera incluse dans la Javadoc

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 :

  • message : préciser le message d'erreur en cas de violation de la contrainte (type String)
  • groups : définir le ou les groupes de validation auxquels la contrainte appartient. La valeur par défaut doit être un tableau vide de type Class<?>
  • payload : fournir des données complémentaires généralement utilisées lors de l'exploitation des violations de contraintes

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 :

  • réaliser une validation partielle en précisant quelles seront les contraintes à utiliser
  • définir l'ordre dans lequel les contraintes seront validées

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 :

  • les noms message, groups et payload sont réservés et ne peuvent donc pas être utilisés pour des attributs personnalisés
  • les noms ne peuvent pas commencer par valid
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();
}

 

103.2.5.2. La création de la classe de validation

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 :

  • Le premier type est l'interface de l'annotation qui est utilisée pour invoquer le valideur
  • Le second précise le type de la valeur qui sera validée par la contrainte

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 :

  • void initialize(A constraintAnnotation)
  • boolean isValid(T value, ConstraintValidatorContext context) 

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;
  }
}

 

103.2.5.3. Le message d'erreur

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 à :

  • L'utilisation d'un ResourceBundle
  • Des placeholders qui seront remplacés par la valeur correspondante d'un attribut de la contrainte

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;
}

 

103.2.5.4. L'utilisation d'une contrainte

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.

 

103.2.5.5. Application multiple d'une contrainte

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;
  }
}

 

103.2.6. Les contraintes composées

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 :

  • créer une version spécifique de la contrainte
  • créer une combinaison de plusieurs annotations
  • exposer plusieurs contraintes sous forme d'une seule
  • faciliter la réutilisation de contraintes en évitant ainsi la duplication

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;
} 

 

103.2.7. L'interpolation des messages

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 :

  • être la clé d'un message dans le ResourceBundle : le contenu entre accolades sera remplacé par la valeur correspondante à la clé
  • être le nom d'un attribut de l'annotation : le contenu entre accolades sera remplacé par la valeur correspondant à l'attribut
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.

 

103.2.7.1. L'algorithme d'une interpolation par défaut

Par défaut, MessageInterpolator suit l'algorithme suivant :

  • étape 1 : les paramètres sont recherchés dans un ResourceBundle applicatif nommé ValidationMessage.properties. Si la propriété est trouvée alors elle est remplacée dans le message. Cette étape est effectuée récursivement (car un paramètre peut contenir un paramètre) jusqu'à ce que plus aucun paramètre ne soit trouvé
  • étape 2 : les paramètres sont recherchés dans un ResourceBundle fournit par l'implémentation. Si la propriété est trouvée alors elle est remplacée dans le message. Cette étape n'est pas effectuée récursivement
  • étape 3 : si l'étape 2 a effectué un remplacement alors l'étape 1 est de nouveau effectuée
  • étape 4 : les paramètres sont extraits et ceux dont la valeur correspond à un des attributs de la contrainte sont remplacés par la valeur de cet attribut

Lors des recherches dans le ResourceBundle applicatif ou fourni par l'implémentation de la locale utilisée est :

  • soit la locale fournie en paramètre de la méthode interpolate()
  • soit la locale par défaut retournée par la méthode getDefault() de la classe Locale.

 

103.2.7.2. Le développement d'un MessageInterpolator spécifique

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().

 

103.2.8. Bootstrapping

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 :

  • gérer plusieurs implémentations
  • choisir l'implémentation à utiliser
  • configurer l'implémentation utilisée
  • s'intégrer dans les conteneurs notamment ceux de Java EE 6

Les mécanismes de bootstrap mettent en oeuvre plusieurs interfaces :

  • Validation : c'est le point d'entrée de l'API pour utiliser une implémentation
  • ValidationProvider : interface qui définit les fonctionnalités utilisables lors du bootstrap
  • ValidationProviderResolver : permet de rechercher la liste des implémentations utilisables dans le contexte d'exécution
  • Configuration : encapsule les données de configuration lors de la création de l'instance de type ValidatorFactory
  • ValidatorFactory : fabrique qui est instanciée par le mécanisme de bootstrap. Le rôle de cette fabrique est de créer des instances de type Validator

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 :

  • Utilisation du mécanisme proposé par le Java Service Provider mis en oeuvre par le fournisseur de l'implémentation
  • Utilisation des méthodes statiques de la classe Validation

 

103.2.8.1. L'utilisation du Java Service Provider

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.

 

103.2.8.2. L'utilisation de la classe Validation

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.

 

103.2.8.3. Les interfaces ValidationProvider et ValidationProviderResolver

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.

 

103.2.8.3.1. L'interface  ValidationProviderResolver

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.

 

103.2.8.3.2. L'interface ValidationProvider

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.

 

103.2.8.4. L'interface MessageInterpolator

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().

 

103.2.8.5. L'interface TraversableResolver

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.

 

103.2.8.6. L'interface ConstraintValidatorFactory

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.

 

103.2.8.7. L'interface ValidatorFactory

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.

 

103.2.8.8. L'interface Configuration

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 :

  • l'instance spécifiée par l'utilisation de la méthode byProvider() de la classe Validation
  • le contenu du fichier META-INF/validation.xml
  • l'instance de type ValidatorProviderResolver fournie à la Configuration ou l'instance par défaut de cette interface

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 :

  • Utilisation de l'implémentation désignée par l'invocation de la méthode byProvider() de la classe Validation
  • Utilisation des informations contenues dans le fichier META-INF/validation.xml
  • Utilisation de la première implémentation renvoyée par la méthode getValidationProviders() de la classe ValidationProvider

Une instance de type Configuration est créée grâce à la classe ValidationProvider.

Une instance de Configuration est utilisée par la classe Validation.

 

103.2.8.9. Le fichier de configuration META-INF/validation.xml

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>

 

103.2.9. La définition de contraintes dans un fichier XML

 

en construction
La suite de cette section sera développée dans une version future de ce document

 

 

103.2.10. L'API de recherche des contraintes

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.

 

103.2.10.1. L'interface ElementDescriptor

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.

 

103.2.10.2. L'interface ConstraintFinder

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)

 

103.2.10.3. L'interface BeanDescriptor

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.

 

103.2.10.4. L'interface PropertyDescriptor

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é

 

103.2.10.5. L'interface ConstraintDescriptor

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

 

103.2.10.6. Un exemple de mise en oeuvre

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=[])

 

103.2.11. La validation des paramètres et de la valeur de retour d'une méthode

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.

 

103.2.12. L'implémentation de référence : Hibernate Validator

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 :

  • télécharger l'archive sur le site du projet
  • décompresser l'archive dans un répertoire du système
  • ajouter le fichier hibernate-validator-4.0.1.GA.jar et les fichiers *.jar du sous-répertoire lib au classpath du projet

 

103.2.13. Les avantages et les inconvénients

Les avantages sont :

  • standardisation des fonctionnalités de validation des données d'un bean
  • déclaration des contraintes simplifiée par des annotations
  • uniformisation de la déclaration des contraintes au niveau du bean
  • validation des contraintes à la discrétion des développeurs dans n'importe quelle couche Java d'une application

Les inconvénients sont :

  • requiert un Java 5 minimum
  • seuls les JavaBeans peuvent être validés

Il existe aussi plusieurs manques dans la JSR 303 notamment :

  • le support de la Locale du client côté serveur
  • le support de la validation des paramètres d'une méthode qui est proposé sous la forme d'une extension dont le support est optionnel
  • la possibilité de définir des contraintes en utilisant une expression ou un script : cette fonctionnalité peut être développée sous la forme d'une contrainte personnalisée

 

103.3. D'autres frameworks pour la validation des données

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

http://oval.sourceforge.net/

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

 


Développons en Java v 2.20   Copyright (C) 1999-2021 Jean-Michel DOUDOUX.   
[ Précédent ] [ Sommaire ] [ Suivant ] [ Télécharger ]      [ Accueil ] [ Commentez ]