Skip to content

Validation des objets

But

  • Les applications Java EE doivent manipuler des objets dont les attributs sont renseignés par des humains depuis l'IHM ou des services externes.
  • Les humains font NÉCESSAIREMENT des erreurs.
  • Les services externes sont potentiellement buggés.
  • Il est nécessaire de valider ses données tout au long de leur parcours :

    • saisie HTML/javascript, SWING, ou autre
    • stockage base
  • C'est une opération normalement fastidieuse, répétitive, sujette à erreur
  • MAIS standardisée (Bean Validation (JSR 303)1) et relativement facile à utiliser à l'aide de quelques annotations:

    • @AssertFalse, @AssertTrue
    • @DecimalMax, @DecimalMin
    • @Future, @Past
    • @Max, @Min
    • @Null, @NotNull
    • @Pattern
    • @Digits
    • @Size

Le chapitre «Declaring and validating bean constraints» de la documentation contient la liste des contraintes disponibles dans la section 2.3.1 : https://docs.jboss.org/hibernate/validator/5.0/reference/en-US/html/chapter-bean-constraints.html.

Utilisation d'une annotation de validation

  • Les contraintes de validation sont exprimées sous la forme d'annotations
  • Appliquées sur :

    • types
    • méthodes
    • champs
    • autres contraintes
    • Prise en compte à l'exécution et pas à la compilation.
  • Exemple :
import jakarta.validation.constraints.*;

public class PersonToday {
  @NotNull
  private String name;
  @NotNull
  private String surname;
  @NotNull @Min(0)
  private int age;
  @Size(max = 2000)
  private String currentJob;
}

Composition de contraintes existantes

  • La composition de contraintes permet de créer des contraintes complexes composées de plusieurs contraintes validées individuellement
  • S'exprime sous la forme d'une annotation
  • Il n'y a pas d'implémentation à écrire
  • Une annotation est considérée comme étant une contrainte si :

    • sa politique de rétention contient RUNTIME
    • l'annotation est elle-même annotée avec jakarta.validation.Constraint
@NotNull // Contrainte existante
@Size(min=7) // Contrainte existante
@Pattern(regexp = "...") // Contrainte existante
@Constraint(validatedBy={}) // Pas besoin d'implémenter du code de validation
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface Email{
  String message() default "Email incorrect"; // Message si pas valide
  Class<?>[] groups() default {}; // Groupe de validation partiel
  Class<? extends Payload>[] payload() default{}; // Metadonnée de la contrainte

}

Création d'une nouvelle contrainte

Contrainte NotNull

Déclaration de la contrainte :

// Cibles de l'annotation
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER}) 
//Quand l'annotation est utilisée
@Retention({RUNTIME}) 
// Inclusion dans la javadoc
@Documented 
// Classe qui contient l'algo de validation
@Constraint(validatedBy = NotNullValidator.class) 
public @interface NotNull {
  // Message si pas valide
  String message() default "{jakarta.validation.constraints.NotNull.message}";
  // Groupe de validation partiel
  Class<?>[] groups() default {};
  // Metadonnée de la contrainte
  Class<? extends Payload>[] payload() default {};
}

Définition de la contrainte :

// <Nom annotation, type de paramètre (pour spécialiser la contrainte)>
public class NotNullValidator implements ConstraintValidator<NotNull, Object>{
  public void initialize(NotNull parameters){
  } // appelé avant d'utiliser la contrainte
  public boolean isValid(Object object, 
                         ConstraintValidatorContext context){
    return object != null;
  } // Code de validation
}

Documentation de ConstraintValidator :

public interface ConstraintValidator<A extends Annotation,T>

Defines the logic to validate a given constraint A for a given object type T.

Implementations must comply to the following restriction:

    T must resolve to a non parameterized type
    or generic parameters of T must be unbounded wildcard types

Autre exemple de contrainte

Cette contrainte est à comparer avec la contrainte NotNull.

Déclaration de la contrainte :

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = AddressValidator.class)

public @interface Address {
    String  message() default "{address should be less than 500 aphanumerics chars}";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default{};
}

Définition de la contrainte :

public class AddressValidator implements ConstraintValidator <Address, String> {
    @Override
    public void initialize(Address address) {  }

    @Override
    public boolean isValid(String s, 
                     ConstraintValidatorContext constraintValidatorContext) {
        // TO WRITE
        return true;
    }

}

Spécialisation de contraintes

Les contraintes peuvent contenir des arguments afin de spécifier leur comportement.

Utilisation de la contrainte :

@URL 
private String resourceURL;
@NotNull @URL(protocol="http", host="www.iut.u-bordeaux1.fr");
private String httpServerUrl;
@URL(protocol="ftp", port=21)
private String ftpServerUrl;

Déclaration de la contrainte :

@Constraint(validatedBy={URLValidator.class})
@Target({ElementType.METHOD, ElementType.FIELD, 
    ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface URL{
  String message() default "URL incorrecte"; // Message si pas valide
  Class<?>[] groups() default {}; // Groupe de validation partiel
  Class<? extends Payload>[] payload() default{}; // Metadonnée de la contrainte

  String protocol() default "";
  String host() default "";
  int port() default -1;
}

Implémentation de la contrainte :

public class URLValidator implements ConstraintValidator<URL, String>{
  private String protocol;
  private String host;
  private int port;

  public void initialize(URL url){
    this.protocol = url.protocol();
    this.host = url.host();
    this.port = url.port();
  }

  public boolean isValid(String value, ConstraintValidatorContext context){
    if (value == null || value.lenght ==0){
      return true;
    }

    java.net.URL url;
    try{
      url = new java.net.URL(value);
    } catch(MalformedURLException e){
      return false;
    }

    if (protocol != null && 
        protocol.length() > 0 && 
        !url.getProtocol().equals(protocol)){
      return false;
    }
    if (host != null && 
        host.length() > 0 && 
    !url.getHost().equals(host)){
      return false;
    }
    if (port !=-1 && url.getPort() != port){
      return false;
    }

    return true;
  }
}

Utilisation

  • Configuration : META-INF/validation.xml
  • Bibliothèque : annotations.jar

Illustration

Les exemples du projet permettent de comprendre le fonctionnement de la validation de dépendances (à ouvrir avec Intellij idea) :

  • ajouter des contraintes prédéfinies (via des annotations)
  • créer des contraintes
  • tester le mécanisme de validation via des tests unitaires (classe TestConstraints.java de Exemples/Validation)

Lisez attentivement le code pour bien intégrer l'utilisation de ce mécanisme.

Exercices

Objectif

Dans le cadre de cet exercice, on se propose d'implémenter les classes qui modélisent une application de gestion de projet (voir diagramme UML plus loin). Nous ne nous préoccupons que de la validation ; pas du reste.

Organisation

À partir de maintenant, vous devez utiliser un gestionnaire de sources pour stocker vos travaux et les partager avec votre encadrant de TD. La disposition des salles ne permettant pas toujours d'avoir une machine par étudiant, les TD se font en binôme. Dans le cadre de cette matière nous utilisons un dépôt git hébergé par le serveur gitlab de l'iut https://gitlab-ce.iut.u-bordeaux.fr/ (pour les étudiants avec C. Johnen et R. Giot) et https://bitbucket.org/dashboard/ (pour les autres étudiants). Vous avez déjà créé votre dépôt en local sur votre compte. Il faut maintenant le synchroniser avec le dépôt distant et le partager avec votre binôme et enseignant.

Le rôle de l'enseignant par rapport à ce dépôt gitLab doit être reporter, pour qu'il puisse télécharger vos projets JEE (la remise des projets se fait via gitLab).

Choisissez le compte d'un membre du binôme et créez un dépôt en respectant STRICTEMENT le nommage suivant : jee_td_NOM1_NOM2 (avec NOM1 et NOM2 les noms de famille des membres du binôme) et en partageant le dépôt avec votre encadrant de TD et votre binôme. Pensez à commiter et pousser plusieurs fois par séance votre travail sur le dépôt (nous ne le répéterons pas, mais regarder l'historique du dépôt fait parti de la notation TD). Chaque étudiant travaille dans un dossier portant son nom lorque la salle permet d'avoir 1 étudiant par machine.

Avant le TD2, créez un tag TD1 (nous ne le répéterons pas, pensez à créer systématiquement un tag avant chaque nouveau TD).

$ git tag -a  "TD1" # pour ajouter le tag
$ git push --tags # pour l'envoyer sur le dépôt distant

Travail à faire et terminer pour la séance suivante

  1. Dans votre dépôt créer un dossier validation dans lequel vous allez lancer la commande mvn archetype:generate ... pour créer votre exercice validation.
  2. Créez et ajouter les classes suivantes en utilisant les fonctionnalités de votre IDE (mettez les dans un paquetage java nommé bean). C'est l'IDE qui génère le code de la classe avec ses getters/setters, pas vous (apprenez les fonctionnalités du menu Code).

Classes à générer\label{img_uml}

  1. Ajoutez les dépendances à la bibliothèque de validation dans le pom.xml :
    <dependency>
        <groupId>jakarta.validation</groupId>
        <artifactId>jakarta.validation-api</artifactId>
        <version>3.0.1</version>
    </dependency>
    
    <dependency>
        <groupId>org.hibernate.validator</groupId>
        <artifactId>hibernate-validator</artifactId>
        <version>7.0.2.Final</version>
    </dependency>
    
  2. Écrivez les classes de validation validation.Login et validation.LoginValidator qui permettent de valider un identifiant à l'aide des contraintes suivantes :

    • Taille minimum : 2 caractères
    • Taille maximum : 8 caractères
    • Utilisation uniquement des lettres de l'alphabet latin (majuscule ou minuscule)

À noter que cette validation est également implémentable à l'aide de l'annotation @Pattern que nous n'utiliserons pas pour cet exercice.

  1. Pour chaque classe du projet, écrivez les annotations nécessaires à la validation des données (elles sont triviales à trouver). Le chapitre "Declaring and validating bean constraints" de la documentation contient la liste des contraintes disponibles dans la section 2.3.1 et 2.3.2 (https://docs.jboss.org/hibernate/validator/5.0/reference/en-US/html/chapter-bean-constraints.html) :

    • Gestion temporelle des dates
    • Gestion des champs non nuls
    • Gestion d'emails
  2. Vérifiez votre implémentation en écrivant des tests unitaires :

    • Utilisez plusieurs petites fonctions
    • Regroupez les fonctions de manière thématique
private  static Validator validator;
private Set<ConstraintViolation<Person>> violations;

@BeforeAll
public static void setUp() {
   ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
   PersonConstraintTest.validator = factory.getValidator();
}
@Test
public void testNOk() {
   Person p2 = new Person();
   p2.setFirstName("21");
   p2.setLastName("a");
   p2.setEmail("21");
   p2.setLogin(null);
   violations = validator.validate(p2);
   assertEquals(4, violations.size()); 
}
  1. Vérifier votre travail avec mvn test

  2. FACULTATIF Pour utiliser l'annotation @pattern, comme dans la classe suivante :

public class Person  {
 @NotNull
 @Email(message = "email incorrect format")
 private String email;
   // firstName est un chaine de taille quelconque de lettres.
 @Pattern(regexp="[a-zA-Z]*", message="firstName syntax error") 
 private String firstName;
  // lastName est un chaine de  2 à 12 lettres.
 @Pattern(regexp="[a-zA-Z]{2,12}",  message="lastName syntax error")
 private String lastName;
 @Login
 private String login;
 private boolean isStudent;
}

Il faut inclure dans le fichier de configuration de maven (pom.xml) la dépendance suivante :

<dependency>
    <groupId>org.glassfish</groupId>
    <artifactId>jakarta.el</artifactId>
    <version>4.0.1</version>
</dependency>

À reternir (non exhaustif)

  1. Usage des annotations "Bean Validation" (JSR 303) (sauf @Pattern).
  2. Commandes de base de git (git add, git commit, git pull, git push, ...).
  3. Création de nouvelles contraintes.

  1. https://www.jcp.org/en/jsr/detail?id=303