De la validation en veux-tu en voilà !

Marre de trimballer de la vérification de champs à travers vos couches ? Dites bonjour à Pampers hibernate-validator !

Pour la petite histoire, hibernate-validator a été créé à l’origine pour la validation des objets Entity d’hibernate en amont de l’appel à la base de donnée, à la fois pour éviter un appel distant potentiellement inutile et pour éviter de recevoir et traiter des exceptions peu interprétables de la base de données cible. Il a ensuite été élargi pour permettre la validation d’un bean quelconque, et son créateur, Emmanuel Bernard, a établi la JSR 303 dont hibernate-validator est l’implémentation de référence.

Voyons maintenant comment mettre en oeuvre hibernate-validator avec un exemple simple. Et vous allez voir qu’il se passe aujourd’hui très bien d’Hibernate pour fonctionner, comme son nom l’indique. C’est parti, on prend un archetype de base et on ajoute deux dépendances dans le pom.xml.


1
2
3
4
5
6
7
8
9
10
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>4.1.0.Final</version>
</dependency>
<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>1.0.0.GA</version>
</dependency>

On va pour l’exercice créer une classe User que l’on va faire toute belle avec tout plein d’annotations.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class User {

    // ... Attributs

    // Getters

    @NotNull
    @NotEmpty
    public String getFullname() {
        return fullname;
    }

    @NotNull
    @Length(min = 3, max = 20)
    @Pattern(regexp = "[a-zA-Z]*")
    public String getLogin() {
        return login;
    }

    @NotNull
    @UserPassword(conditions = {
            @Condition(target = LOWERCASE, min = 3),
            @Condition(target = UPPERCASE, min = 1),
            @Condition(target = NUMBER, min = 1) })
    @Length(min = 9, max = 12)
    @Pattern(regexp = "[a-zA-Z0-9]*")
    public String getPassword() {
        return password;
    }

    @Email
    @NotNull
    public String getEmail() {
        return email;
    }

    // ... Setters
}

On devine plutôt bien ce que font ces annotations. Vous trouverez une liste complète des annotations possibles dans la documentation. Et puisque vous avez lu attentivement mon code et cette même documentation vous remarquez probablement que j’en utilise une qui n’est pas dans la liste. Je vous aide ?

1
2
3
4
    @UserPassword(conditions = {
            @Condition(target = LOWERCASE, min = 3),
            @Condition(target = UPPERCASE, min = 1),
            @Condition(target = NUMBER, min = 1) })

Quel œil de Lynx ! @UserPassword et @Condition ne sont effectivement pas dans la doc. Si vous devinez ce que ça fait, c’est que j’ai bien bossé : cette annotation vérifie que le mot de passe contient au moins trois caractères minuscules, une majuscule et un chiffre.

Super ! T’as pompé quel tuto ?
Que nenni ! Une partie de la documentation est consacrée à la création de contraintes personnalisées. Regardons un peu l’annotation @UserPassword.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Comme le disait Marty, suivez le doc' !
@Target({ METHOD, FIELD, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = UserPasswordValidator.class)
@Documented
public @interface UserPassword {
    String message() default "Invalid password";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};

    // Et là on ajoute notre liste de conditions
    Condition[] conditions() default {};
}

// Conditions qui sont définies ainsi
public @interface Condition {
    static enum Type {
        LOWERCASE("a-z"), UPPERCASE("A-Z"), NUMBER("0-9");
        String pattern;
        Type(String pattern) {
            this.pattern = pattern;
        }
    }
    Type target();
    int min();
}

Il faut ensuite créer la classe qui implémente l’interface ConstraintValidator, j’ai nommé UserPasswordValidator :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class UserPasswordValidator implements
        ConstraintValidator<UserPassword, String> {

    private Condition[] conditions;

    // On récupère les conditions à l'initialisation
    @Override
    public void initialize(UserPassword constraintAnnotation) {
        conditions = constraintAnnotation.conditions();
    }

    // Et c'est ci que se passe l'évaluation
    @Override
    public boolean isValid(final String password,
            ConstraintValidatorContext constraintContext) {
        if (password == null)
            return false;

        // On passe simplement en revue chaque condition
        for (Condition c : conditions) {
            // Je vous passe l'implémentation de 'count()'
            if (count(c.target().pattern, password) < c.min())
                return false;
        }
        return true;
    }
}

Et comme on fait du TDD, on a bien sûr écrit une classe de test en amont pour tester ce qu’on voulait ! Ou pas… hem.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public class UserTest {

    private static Validator validator;

    @BeforeClass
    public static void init() {
        // Crée le validateur
        ValidatorFactory factory =
            Validation.buildDefaultValidatorFactory();
        validator = factory.getValidator();
    }

    @Test
    public void validUser() {
        User user = createValidUser();
        Assert.assertEquals(0, // Pas d'erreur
            validator.validate(user).size());
    }
   
    @Test
    public void password1() {
        User user = createValidUser();
        user.setPassword("bla!BLA888"); // ! interdit
        checkOneError(user, "password");
    }
   
    @Test
    public void password2() {
        User user = createValidUser();
        user.setPassword("blaBLAbla"); // pas de chiffre
        checkOneError(user, "password");
    }
   
    @Test
    public void email() {
        User user = createValidUser();
        user.setEmail("bla@bla..com"); // deux points
        checkOneError(user, "email");
    }

    private void checkOneError(User user, String field) {
        Set<ConstraintViolation<User>> violations
            = validator.validate(user);
        assertEquals(1, violations.size());
        for (ConstraintViolation<User> violation : violations)
            assertEquals(field, violation.getPropertyPath().toString());
    }

    private User createValidUser() {
        User user = new User();
        user.setLogin("sone");
        user.setPassword("lowUPP333a");
        user.setFullname("Some One");
        user.setEmail("someone@someone.com");
        return user;
    }
}

Ça paaaasse !

Magnifique ! Et étrangement, hibernate-validator s’entend très bien avec… Hibernate ! Pas besoin de vous embêter avec un objet Validator pour utiliser ces annotations avec Hibernate, ajoutez-les simplement sur vos objets Entity, et une RuntimeException sera levée au moment du persist(entity) en cas de violation, et bien sûr avant un quelconque appel à la BDD.

A suivre dans un prochain billet, comment valider un bean côté présentation (sans appel aux services) dans une architecture 3-tiers, avec internationalisation des messages émis, et sans toucher au modèle, toujours avec hibernate-validator !

VN:R_U [1.9.22_1171]
Rating: +1 (from 1 vote)
Share
Ce contenu a été publié dans Outils, avec comme mot(s)-clef(s) , , , . Vous pouvez le mettre en favoris avec ce permalien.

8 réponses à De la validation en veux-tu en voilà !

  1. Ce blog m’étonne. A chaque fois que j’ai un problème sur mon projet actuel, il y a un article qui parait le lendemain pour expliquer comment le résoudre.

    Si je comprends bien, on peut tout à fait utiliser cette lib avec iBatis par exemple ? (un mapper relationnel) Si l’objet n’est pas valide, Validator se contentera de lever une exception ? Est-ce qu’il y a possibilité de connaitre la liste des champs non valides ?

    VN:R_U [1.9.22_1171]
    Rating: 0 (from 0 votes)
  2. Joan Zapata dit :

    A voir si iBatis peut prendre en compte hibernate-validator automatiquement. Mais si tu utilises iBatis à travers Spring, il y a certainement moyen d’automatiser, et dans ce cas c’est effectivement une exception (non checkée) qui est levée au moment du persist(), et sur laquelle on peut récupérer la liste de ConstraintViolation.

    Cela dit, hibernate-validator étant totalement indépendant de tout ORM, il peut tout à fait être utilisé dans le DAO ou ailleurs en amont d’iBatis, pour vérifier la validité de l’objet. C’est d’ailleurs ce que je fais dans ma classe de test.

    Pour les champs non valides, on peut les parcourir comme ça :

    1
    2
    for (ConstraintViolation<User> violation : violations)
              violation.getPropertyPath();

    On peut aussi lier des messages aux contraintes, je couvrirai sûrement ce point dans un prochain post ! :)

    VN:R_U [1.9.22_1171]
    Rating: 0 (from 0 votes)
  3. Effectivement on utilise iBatis via Spring. J’attends avec impatience la suite de l’histoire, ça m’évitera d’avoir à chercher moi-même comment faire ;)
    (Comment ça je suis fainéant ?)
    Et j’aurais dû lire la classe de tests, j’aurais eu ma réponse directement…

    VN:R_U [1.9.22_1171]
    Rating: 0 (from 0 votes)
  4. dvilleneuve@excilys.com dit :

    Ca me parait pas mal tout ça.

    Par contre, juste pour donner une critique ( :D ), tu devrais remplacer l’attribut “nb” de ton annotation Condition par “min”. Ce serait beaucoup plus clair.

    VN:R_U [1.9.22_1171]
    Rating: 0 (from 0 votes)
  5. Sympa le tuto :-)

    A votre avis, ça vaudrait le coup d’utiliser Hibernate Validator sur Android pour valider des champs d’input ?

    VN:R_U [1.9.22_1171]
    Rating: 0 (from 0 votes)
  6. mgrenonville@excilys.com dit :

    Un petit bémol à l’utilisation de cette technique directement dans le modèle : Parfois (qui a dit souvent ?), le moment de valider au persist est un peu trop tard. A mon sens, il est préférable de l’utiliser dans les commandes de formulaires, mais c’est ce que tu développera au second article, si on en croit ta conclusion ;-)

    VN:R_U [1.9.22_1171]
    Rating: 0 (from 0 votes)
  7. Ping : Plusieurs versions d’une application simultanément sur un terminal Android | Excilys Labs

Laisser un commentaire