La validation des données avec GWT 2.5 (Côté client)

GWT 2.5 intègre désormais la validation des données selon la JSR303. Je propose donc de s’intéresser à cette nouvelle fonctionnalité en commençant par annoter un bean et à effectuer une validation côté client (javascript).

Pour réaliser cette petite POC, je vais partir du projet MyModule du post précédent.

Configuration de Maven

Le framework de validation de GWT 2.5 est basé sur Hibernate-Validator. Il va donc falloir ajouter cette librairie et ses sources à notre projet. Pourquoi les sources ? Parce que GWT a besoin des sources pour convertir du code Java en Javascript.
Si vous n’utilisez pas Maven, ça se passe ici
Sinon, dans le fichier pom.xml, on commence donc par ajouter les dépendances ci-dessous :

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
    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-validator</artifactId>
        <version>4.1.0.Final</version>
    </dependency>
    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-validator</artifactId>
        <version>4.1.0.Final</version>
        <classifier>sources</classifier>
    </dependency>
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.16</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>1.6.1</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>jcl-over-slf4j</artifactId>
        <version>1.6.1</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>1.6.1</version>
    </dependency>

Remarque :

Si le fichier pom.xml a été généré automatiquement via l’archetype « gwt-maven-plugin », on va obtenir, au moment de la compilation, un message contenant « package javax.validation.constraints does not exist ». Cela est dû au fait que le projet de démo importe une version antérieure de l’API de validation au niveau des tests. Eclipse importe les bons fichiers, mais Maven plante lorsqu’il compile les classes de « test »… Une solution consiste donc à supprimer les lignes ci-dessous dans le fichier pom.xlm :

1
2
3
4
5
6
7
8
9
10
11
12
13
    <dependency>
      <groupId>javax.validation</groupId>
      <artifactId>validation-api</artifactId>
      <version>1.0.0.GA</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>javax.validation</groupId>
      <artifactId>validation-api</artifactId>
      <version>1.0.0.GA</version>
      <classifier>sources</classifier>
      <scope>test</scope>
    </dependency>

La validation des données avec GWT 2.5

Pour utiliser le framework de validation fourni par GWT, nous allons procéder en trois étapes :

  1. définir les contraintes à vérifier sur les Java Beans
  2. implémenter l’usine abstraite AbstractGwtValidatorFactory
  3. lancer la validation lorsque l’utilisateur soumet une nouvelle instance de la classe Client

Dans la  classe Client, commençons donc par rajouter une contrainte simple à l’attribut « nom » afin que celui-ci contienne au moins 4 caractères :

1
2
3
4
5
6
7
public class Client implements Serializable  {
    //////////// ATTRIBUTES ////////////
    private Integer id;
    @Size(min = 4)
    private String nom;
    ...
}

Voilà le première étape terminée. Passons maintenant à cette implémentation qui est très simple avec le mécanisme de differed binding de GWT : on commence par créer la classe ci-dessous dans la partie client et je la commente ensuite :

1
2
3
4
5
6
7
8
9
10
11
public final class SimpleValidatorFactory extends AbstractGwtValidatorFactory {

  @GwtValidation(Client.class)
  public interface GwtValidator extends Validator {
  }

  @Override
  public AbstractGwtValidator createValidator() {
    return GWT.create(GwtValidator.class);
  }
}

C’est toujours un peu le même mécanisme que l’on retrouve dans GWT : on crée une interface, les annotations vont permettre à GWT de déterminer certaines options pour générer les implémentations automatiques de ces interfaces, puis la méthode GWT.create va lier le bon morceau de code Javascript au moment adéquat.

On n’oublie pas de rajouter les lignes ci-dessous dans MyModule.gwt.xml :

1
2
3
4
5
    <inherits name="org.hibernate.validator.HibernateValidator" />
    <replace-with
        class="com.acme.gwt.demo.client.validator.SimpleValidatorFactory">
        <when-type-is class="javax.validation.ValidatorFactory" />
    </replace-with>

Puis on rajoute un petit algorithme de validation des données et d’affichage d’erreurs dans la classe ListClient :

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
    @UiHandler("clientPopup")
    void onHide(HideEvent event) {
        //Si on a cliqué sur OK : on sauvegarde le nouveau client
        if (clientPopup.getHideButton() == clientPopup.getButtonById(PredefinedButton.OK.name())){
            Client client = new Client(null, name.getCurrentValue(), date.getCurrentValue(), email.getCurrentValue());
            //validation des données
            Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
            Set<ConstraintViolation<Client>> violations = validator.validate(client);
            if(!violations.isEmpty()){
                clientPopup.show(); //empeche la popup de se fermer
                StringBuilder builder = new StringBuilder();
                for (ConstraintViolation<Client> violation : violations) {
                    builder.append(violation.getMessage());
                    builder.append(" : <i>(");
                    builder.append(violation.getPropertyPath().toString());
                    builder.append(" = ");
                    builder.append("" + violation.getInvalidValue());
                    builder.append(")</i>");
                    builder.append("<br/>");
                }
                //Affiche un message d'erreur avec les contraintes non respectées
                Info.display("Contraintes non respectées !", builder.toString());
            } else {
                //persistance du nouveau client
                service.ajouterClient(client, new AsyncCallback<Client>() {
                    public void onSuccess(Client clientPersistant) {
                        //Ajout du nouveau client dans la grille
                        store.add(clientPersistant);
                        //réinitialisation de la popup
                        name.reset();
                        date.reset();
                        email.reset();
                    }
                    public void onFailure(Throwable e) {
                        e.printStackTrace();
                    }
                });
            }
        }
    }

Le message d’erreur est généré automatiquement par GWT, et en français, s’il vous plait :

Personnalisation des contraintes et des messages

Comme on peut le constater dans la capture ci-dessus, le message n’est pas approprié puisqu’il affiche une valeur maximum que l’on n’a pas définit. On doit donc le modifier :

1
2
@Size(min = 4, message="Le nom doit contenir au moins {min} lettres !")
private String nom;

Ça marche (faites le test)… mais c’est moche ! Une chaîne de texte passée en dur comme ça c’est bien pour tester le framework de validation, mais en production, il va falloir pouvoir manipuler ça avec un fichier properties afin d’obtenir un joli code comme ça :

1
2
@Size(min = 4, message="{custom.name.size.message}")
private String nom;

Pour une raison que j’expliquerai plus bas, nous allons commencer par ajouter les messages personnalisés dans les fichiers ValidationMessages.properties

1
custom.name.size.message=The property must be between {min}

et ValidationMessages_fr.properties

1
custom.name.size.message=Le nom doit contenir au moins {min} caracteres

On place ces deux fichiers dans le package com.acme.gwt.demo.client.validator, puis on va laisser Maven générer le bundle i18n en rajoutant les lignes ci-dessous dans le fichier pom.xml :

1
2
3
<i18nConstantsWithLookupBundle>
  com.acme.gwt.demo.client.validator.ValidationMessages
</i18nConstantsWithLookupBundle>

et en lançant la commande :

mvn gwt:i18n

L’interface est bien générée dans le package com.acme.gwt.demo.client.validator. Au passage, je rappelle la différence entre l’interface com.google.gwt.i18n.client.ConstantsWithLookup et l’interface com.google.gwt.i18n.client.Messages : la première génère des constantes javascript de manière dynamique, c’est-à-dire que les paramètres vont être lus une fois pour toute à la compilation, tandis que la seconde génère des méthodes javascript qui vont pouvoir recevoir des paramètres au moment de leur appel.

Toujours dans le même package, nous allons créer la classe CustomValidationMessagesResolver afin d’implémenter l’interface com.google.gwt.validation.client.UserValidationMessagesResolver.

1
2
3
4
5
6
7
public class CustomValidationMessagesResolver extends
        AbstractValidationMessageResolver implements
        UserValidationMessagesResolver {
    protected CustomValidationMessagesResolver() {
        super((ConstantsWithLookup) GWT.create(ValidationMessages.class));
    }
}

Le code source de la classe que l’on étend ici (AbstractValidationMessageResolver) se trouve ici. Attention à ce que la classe ValidationMessages soit bien celle que l’on vient de créer. C’est pour cela que j’ai souhaité démarré par cette étape. Il n’y aura pas de message d’erreur si, par exemple, on a importé la classe org.hibernate.validator.ValidationMessages

Pour que notre implémentation de l’interface soit utilisée à la place de l’implémentation par défaut, on doit le préciser dans le fichier MyModule.gwt.xml :

1
2
3
4
5
6
7
8
9
10
11
    <!-- Validation -->
    <inherits name="org.hibernate.validator.HibernateValidator" />
    <replace-with
        class="com.acme.gwt.demo.client.validator.SimpleValidatorFactory">
        <when-type-is />
    </replace-with>
    <replace-with
        class="com.google.gwt.sample.validation.client.CustomValidationMessagesResolver">
        <when-type-is
            class="com.google.gwt.validation.client.UserValidationMessagesResolver" />
    </replace-with>

On lance la compilation, et on obtient notre message personnalisé :

mvn gwt:run

Liste des annotations commentées dans l’excellent article de J.M. Doudoux : http://jmdoudoux.developpez.com/cours/developpons/java/chap-validation_donnees.php#validation_donnees-2-4.