Maintenant que nous sommes familiers avec JSF, nous allons mettre en pratique et apprendre à gérer proprement un formulaire, en ajoutant de la complexité au fur et à mesure de notre progression.
Une fois n'est pas coutume, nous allons réécrire notre fonction d'inscription d'utilisateur. Comme d'habitude, nous allons ajouter un petit plus à notre système : après la création initiale du système en suivant MVC avec des JSP et des servlets, nous y avions ajouté une base de données pour commencer, puis nous y avions intégré JPA. Eh bien cette fois nous allons bien évidemment employer JSF, mais également rendre la validation du formulaire… ajaxisée ! Appétissant, n'est-ce pas ?
Une inscription classique
Préparation du projet
Nous allons partir sur une base propre. Pour ce faire, créez un nouveau projet web dynamique sous Eclipse, et nommez-le pro_jsf. Créez-y alors un fichier de configuration /WEB-INF/glassfish-web.xml :
1 2 3 4 5 6 7 8 9 10 11 | <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE glassfish-web-app PUBLIC "-//GlassFish.org//DTD GlassFish Application Server 3.1 Servlet 3.0//EN" "http://glassfish.org/dtds/glassfish-web-app_3_0-1.dtd"> <glassfish-web-app> <context-root>/pro_jsf</context-root> <class-loader delegate="true"/> <jsp-config> <property name="keepgenerated" value="true"> <description>Conserve une copie du code des servlets auto-générées.</description> </property> </jsp-config> </glassfish-web-app> |
/WEB-INF/glassfish-web.xml
Copiez-y ensuite le fichier web.xml que nous avions mis en place dans le projet test_jsf, fichier que nous pouvons, comme je vous l'avais expliqué, réutiliser tel quel.
Avant de poursuivre, je vous conseille de mettre en place une petite configuration particulière, afin d'éviter de futurs ennuis. Par défaut, le contenu d'un champ laissé vide dans vos formulaires sera considéré comme une chaîne vide. Et vous devez le savoir, les chaînes vides sont l'ennemi du développeur ! Heureusement, il est possible de forcer le conteneur à considérer un tel contenu comme une valeur nulle plutôt que comme une chaîne vide, en ajoutant cette section dans votre fichier web.xml :
1 2 3 4 5 6 7 8 | ... <context-param> <param-name>javax.faces.INTERPRET_EMPTY_STRING_SUBMITTED_VALUES_AS_NULL</param-name> <param-value>true</param-value> </context-param> ... |
Configuration de la gestion des chaînes vides dans /WEB-INF/web.xml
Voilà tout pour le moment, nous reviendrons sur l'intérêt pratique de cette manipulation plus loin dans ce chapitre.
Création de la couche d'accès aux données
La première étape du développement, si c'en est une, est la "création" du modèle. En réalité, nous n'avons rien à faire ici : nous allons simplement réutiliser notre entité Utilisateur telle que nous l'avions développée dans notre projet pro_jpa, ainsi que notre EJB Stateless ! Copiez donc simplement dans votre projet :
- la classe
com.sdzee.entities.Utilisateur
, en conservant le même package ; - la classe
com.sdzee.dao.DAOException
, en conservant le même package ; - la classe
com.sdzee.dao.UtilisateurDao
, en conservant le même package ; - le fichier de configuration de JPA src/META-INF/persistence.xml, en conservant le même répertoire.
Voici sur la figure suivante l'arborescence que vous devez obtenir une fois arrivés à cette étape.
Création du backing bean
Nous l'avons découvert dans le chapitre précédent, un nouvel objet propre à JSF fait son apparition : le backing bean. Il s'agit en réalité d'une sorte de mini-contrôleur MVC, une sorte de glue qui relie la vue (la page JSF) au modèle de données (l'entité). Cet objet est littéralement lié à la vue, et tous les attributs de l'entité sont exposés à la vue à travers lui.
Pour faire l'analogie avec ce que nous avions développé dans nos précédents exemples, cet objet va remplacer notre ancien InscriptionForm. Toutefois, nous n'allons pour le moment pas nous encombrer avec les méthodes de validation des différents champs du formulaire, et nous nous contenterons de mettre en place une inscription sans vérifications. Nous compléterons ensuite notre système, lorsque nous aurons construit une base fonctionnelle.
Sur la forme, ce backing-bean se présente comme un bean classique, aux annotations JSF près. Puisqu'il est associé à une action, il est courant de le nommer par un verbe représentant l'action effectuée. Nous allons donc logiquement dans le cadre de notre exemple créer un bean intitulé InscrireBean :
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 | package com.sdzee.beans; import java.io.Serializable; import java.sql.Timestamp; import javax.ejb.EJB; import javax.faces.application.FacesMessage; import javax.faces.bean.ManagedBean; import javax.faces.bean.RequestScoped; import javax.faces.context.FacesContext; import com.sdzee.dao.UtilisateurDao; import com.sdzee.entities.Utilisateur; @ManagedBean @RequestScoped public class InscrireBean implements Serializable { private static final long serialVersionUID = 1L; private Utilisateur utilisateur; // Injection de notre EJB (Session Bean Stateless) @EJB private UtilisateurDao utilisateurDao; // Initialisation de l'entité utilisateur public InscrireBean() { utilisateur = new Utilisateur(); } // Méthode d'action appelée lors du clic sur le bouton du formulaire // d'inscription public void inscrire() { initialiserDateInscription(); utilisateurDao.creer( utilisateur ); FacesMessage message = new FacesMessage( "Succès de l'inscription !" ); FacesContext.getCurrentInstance().addMessage( null, message ); } public Utilisateur getUtilisateur() { return utilisateur; } private void initialiserDateInscription() { Timestamp date = new Timestamp( System.currentTimeMillis() ); utilisateur.setDateInscription( date ); } } |
com.sdzee.beans.InscrireBean
Notre objet contient tout d'abord une référence à notre entité Utilisateur à la ligne 20, à laquelle est associée une méthode getter lignes 40 à 42. Cette entité Utilisateur est initialisée depuis un simple constructeur public et sans argument aux lignes 27 à 29.
Notre DAO Utilisateur, qui pour rappel est depuis l'introduction de JPA dans notre projet un simple EJB Stateless, est injecté automatiquement via l'annotation @EJB
à la ligne 24, exactement comme nous l'avions fait depuis notre servlet dans le projet pro_jpa.
Enfin, une méthode d'action nommée inscrire() est chargée :
- d'initialiser la propriété dateInscription de l'entité Utilisateur avec la date courante, via la méthode initialiserDateInscription() que nous avons créée aux lignes 44 à 47 pour l'occasion ;
- d'enregistrer l'utilisateur en base, via un appel à la méthode
creer()
du DAO Utilisateur ; - d'initialiser un message de succès de la validation.
Dans cette dernière étape, deux nouveaux objets apparaissent :
FacesMessage
: cet objet permet simplement de définir un message de validation, que nous précisons ici en dur directement dans son constructeur. Il existe d'autres constructeurs, notamment un qui permet d'associer à un message un niveau de criticité, en précisant une catégorie qui est définie parFacesMessage.Severity
. Les niveaux existants sont représentés par des constantes que vous pouvez retrouver sur la documentation de l'objet FacesMessage. En ce qui nous concerne, nous ne spécifions qu'un message dans le constructeur, et c'est par conséquent la criticitéSeverity.INFO
qui est appliquée par défaut à notre message par JSF ;FacesContext
: vous retrouvez là l'objet dont je vous ai annoncé l'existence dans le chapitre précédent, celui qui contient l'arbre des composants d'une vue ainsi que les éventuels messages d'erreur qui leur sont associés. Eh bien ici, nous nous en servons pour mettre en place un FacesMessage dans le contexte courant via la méthodeaddMessage()
, pour que la réponse puisse ensuite l'afficher. Je vous laisse parcourir sa documentation, et nous reviendrons ensemble sur l'intérêt de passernull
en tant que premier argument de cette méthode lorsque nous développerons la vue.
Par défaut, nous avons annoté notre bean avec @RequestScoped
pour le placer dans la portée requête : en effet, notre objet ne va intervenir qu'à chaque demande d'inscription et n'a donc pas vocation à être stocké plus longtemps que le temps d'une requête.
Au passage, vous remarquez ici pourquoi il est très important d'avoir découvert comment fonctionne une application Java EE MVC sans framework. Si vous n'aviez pas conscience de ce qui se passe derrière les rideaux, notamment des différentes portées existantes dans une application et des allers-retours de paires requête/réponse qui ont lieu à chaque intervention de l'utilisateur depuis son navigateur, vous auriez beaucoup plus de mal à saisir comment manipuler vos objets avec JSF !
Si vous avez bien observé le code de ce backing-bean, et si vous vous souvenez de celui de notre ancien objet métier InscriptionForm, vous devez instantanément vous poser la question suivante :
Où sont les méthodes de récupération et conversion des valeurs envoyées depuis le formulaire ?
Eh oui, dans notre bean nous nous contentons simplement d'initialiser la date d'inscription dans notre entité Utilisateur, car ce n'est pas une information saisie par l'utilisateur. Mais en ce qui concerne toutes les autres propriétés de notre entité, nous ne faisons strictement rien. Vous retrouvez ici ce que je vous ai expliqué dans la description du processus du traitement d'une requête avec JSF, le fameux parcours en six étapes. Les étapes de récupération, conversion, validation et enregistrement dans le modèle sont entièrement automatisées ! Et c'est depuis la vue que nous allons directement effectuer les associations entre les champs du formulaire et les propriétés de notre entité.
Création de la vue
Nous devons ensuite créer la Facelet générant le formulaire d'inscription. Pour rappel, une Facelet n'est qu'une simple page XHTML contenant des balises JSF. Je vous donne dès maintenant le code de la vue dans son intégralité, prenez le temps de bien regarder les composants qui interviennent et nous en reparlons en détail ensuite.
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 | <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html"> <h:head> <meta charset="utf-8" /> <title>Inscription</title> <h:outputStylesheet library="default" name="css/form.css" /> </h:head> <h:body> <h:form> <fieldset> <legend>Inscription</legend> <h:outputLabel for="email">Adresse email <span class="requis">*</span></h:outputLabel> <h:inputText id="email" value="#{inscrireBean.utilisateur.email}" required="true" size="20" maxlength="60" /> <h:message id="emailMessage" for="email" errorClass="erreur" /> <br /> <h:outputLabel for="motdepasse">Mot de passe <span class="requis">*</span></h:outputLabel> <h:inputSecret id="motdepasse" value="#{inscrireBean.utilisateur.motDePasse}" required="true" size="20" maxlength="20" /> <h:message id="motDePasseMessage" for="motdepasse" errorClass="erreur" /> <br /> <h:outputLabel for="confirmation">Confirmation du mot de passe <span class="requis">*</span></h:outputLabel> <h:inputSecret id="confirmation" value="#{inscrireBean.utilisateur.motDePasse}" required="true" size="20" maxlength="20" /> <h:message id="confirmationMessage" for="confirmation" errorClass="erreur" /> <br /> <h:outputLabel for="nom">Nom d'utilisateur <span class="requis">*</span></h:outputLabel> <h:inputText id="nom" value="#{inscrireBean.utilisateur.nom}" required="true" size="20" maxlength="20" /> <h:message id="nomMessage" for="nom" errorClass="erreur" /> <br /> <h:messages globalOnly="true" infoClass="info" /> <h:commandButton value="Inscription" action="#{inscrireBean.inscrire}" styleClass="sansLabel" /> <br /> </fieldset> </h:form> </h:body> </html> |
/inscription.xhtml
Vous devez reconnaître la structure globale de la page, identique aux premiers exemples de Facelets que nous avons mis en place dans le chapitre précédent :
- un en-tête HTML particulier contenant la déclaration de la bibliothèque de composants, préfixés par h: ;
- une section header contenant l'inclusion de la feuille de style CSS form.css, que vous prendrez soin de recopier depuis le projet test_jsf en recréant l'arborescence /resources/default/css dans votre projet ;
- puis le corps de la page contenant un formulaire généré par le composant
<h:form>
.
Nous allons maintenant détailler comment nous procédons à la création des différents éléments du formulaire, en analysant les composants qui interviennent. Comme vous pourrez le constater en parcourant la documentation de chacune des balises, elles supportent toutes un nombre conséquent d'attributs. Nous allons par conséquent limiter notre analyse à ceux qui nous sont ici utiles. Si vous souhaitez connaître en détail toutes les possibilités offertes par chaque balise, je vous invite à parcourir leurs documentations en intégralité et à faire vos propres tests pour vérifier que vous avez bien compris.
Les labels
Pour générer un label associé à un champ de formulaire, concrétisé par la balise HTML <label>
, il faut utiliser le composant <h:outputLabel>
. Le comportement de l'attribut for est identique à celui de la balise HTML, il suffit d'y préciser l'id du champ de saisie auquel le label fait référence.
Les champs de saisie
Pour générer un champ de saisie de type texte, concrétisé par la balise HTML <input type="text" ...>
, il faut utiliser le composant <h:inputText>
. Tout comme la balise HTML, il accepte les attributs id, value, size et maxlength.
Pour générer un champ de saisie de type mot de passe, concrétisé par la balise HTML <input type="password" ...>
, il faut utiliser le composant <h:inputSecret>
. Il accepte lui aussi les attributs id, value, size et maxlength.
Petite nouveauté, nous utilisons un attribut nommé required, qui peut prendre comme valeur true
ou false
, et qui va déterminer si l'utilisateur doit obligatoirement saisir des données dans ce champ ou non. En réalité, il s'agit là d'un marqueur qui va être appliqué au composant, et qui va permettre de générer une erreur lors de la validation de la valeur du champ associé. Si un champ est marqué comme requis et qu'aucune valeur n'est entrée par l'utilisateur, alors un message d'erreur sera automatiquement placé dans le FacesContext
par JSF, et nous pourrons ensuite l'afficher à l'utilisateur dans la réponse.
Ce qu'il faut bien observer ici, c'est l'emploi d'expressions EL pour cibler la propriété utilisateur de notre backing-bean, par exemple #{inscrireBean.utilisateur.email}
à la ligne 13. Comme je vous l'ai déjà dit, tous les attributs de l'entité sont exposés à la vue à travers le backing-bean. Voilà pourquoi nous devons d'abord cibler l'entité utilisateur, qui est elle-même une propriété du backing-bean inscrireBean, puis cibler la propriété de l'entité désirée. Vous comprenez mieux maintenant pourquoi je vous ai dit que ce bean était la glue qui reliait la vue au modèle !
En outre, comprenez bien que la liaison créée par l'intermédiaire de cette expression EL placée dans l'attribut value d'un composant de saisie est bidirectionnelle :
- lors du rendu de la page HTML, la valeur contenue dans le modèle qui est retournée par cette expression EL sera affichée dans le champ de saisie ;
- lors de l'envoi des données du formulaire par l'utilisateur, la valeur contenue dans le champ de saisie sera utilisée pour mettre à jour la valeur contenue dans le modèle.
Les messages
Depuis le temps que je vous parle de ces fameux messages d'erreurs, nous y voilà. La différence la plus marquante entre la page JSP que nous avions utilisée jusqu'à présent et notre Facelet fraîchement créée est l'utilisation d'un composant JSF pour générer un message associé à un champ de saisie en particulier. Il s'agit du composant <h:message>
. Entre autres, celui-ci accepte deux attributs qui nous sont ici utiles :
- for, pour cibler l'id du champ concerné ;
- errorClass, pour permettre de donner une classe CSS particulière lors de l'affichage du message généré s'il s'agit d'une erreur.
Ce composant va donc afficher, lors du rendu de la réponse, l'éventuel message associé au champ ciblé par l'attribut for. S'il s'agit d'un message d'erreur, que JSF sait différencier des messages d'information qui peuvent éventuellement être placés dans le FacesContext
grâce au niveau de criticité associé à un message dont je vous ai parlé un peu plus tôt, alors le style erreur défini dans notre feuille CSS sera appliqué.
Nous utilisons en fin de page un autre élément responsable de l'affichage de messages à l'utilisateur : le composant <h:messages>
. Par défaut, celui-ci provoque l'affichage de tous les messages disponibles dans la vue, y compris ceux qui sont déjà affichés via un <h:message>
ailleurs dans la page. Toutefois, il est possible de n'afficher que les messages qui ne sont attachés à aucun composant défini, c'est-à-dire les messages dont l'id est null
, en utilisant l'attribut optionnel globalOnly="true"
:
1 | <h:messages globalOnly="true" /> |
Vous comprenez maintenant pourquoi dans la méthode inscrire() de notre backing-bean, nous avons passé null
en paramètre de la méthode FacesContext.addMessage()
: c'est pour pouvoir distinguer notre message à caractère général (nous nous en servons pour stocker le résultat final de l'inscription) des messages liés aux composants de la vue. Comprenez donc bien que le code suivant dans notre backing-bean attacherait le message donné au composant <h:message for="clientId">
, et que nous passons null
pour n'attacher notre message à aucun composant existant.
1 | facesContext.addMessage("clientId", facesMessage); |
Notre message a ainsi un caractère global. Voilà d'ailleurs pourquoi l'attribut de la balise <h:messages>
permettant de cibler uniquement ce type de messages s'intitule… globalOnly
!
Enfin, nous utilisons l'attribut infoClass pour donner à notre message global le style info qui est défini dans notre feuille CSS. Nous pourrions utiliser également l'attribut styleClass, mais puisque JSF permet de différencier les messages selon leur gravité, autant en profiter !
Le bouton d'envoi
Pour générer un bouton de soumission de formulaire, concrétisé par la balise HTML <input type="submit" ...>
, il faut utiliser le composant <h:commandButton>
. Nous l'avons déjà étudié dans notre premier exemple : le contenu de son attribut value est affiché en tant que texte du bouton HTML généré, et son attribut action permet de définir la navigation. À la différence de notre premier exemple cependant, où nous redirigions l'utilisateur vers une autre page, nous utilisons ici une expression EL pour appeler une méthode de notre backing-bean, en l'occurrence notre méthode d'action inscrire(). Vous retrouvez ici ce que je vous ai expliqué en vous présentant la technologie EL : les expressions se basant sur la syntaxe #{...}
permettent d'appeler n’importe quelle méthode d'un bean, et pas seulement une méthode getter comme c'était le cas avec ${...}
.
Nous utilisons enfin l'attribut styleClass, pour appliquer au bouton HTML généré la classe sansLabel définie dans notre feuille CSS.
Nous avons fait le tour de tout ce qu'il faut savoir sur notre léger système d'inscription. Pour le moment, aucun contrôle de validation n'est effectué hormis les simples required="true"
sur les champs du formulaire. De même, aucune information n'est affichée hormis un message d'erreur sur chaque champ laissé vide, et notre message de succès lorsque l'inscription fonctionne.
Tests & observations
Notre projet est maintenant prêt pour utilisation. En fin de compte, seuls une Facelet et un backing bean sont suffisants, le reste étant récupéré depuis notre projet JPA. Vérifions pour commencer le bon fonctionnement de l'application en nous rendant sur l'URL http://localhost:8088/pro_jsf/inscription.xhtml
depuis notre navigateur. Vous devez observer le formulaire d'inscription tel qu'il existait dans nos précédents exemples.
Si ce n'est pas le cas, c'est que vous avez oublié quelque chose en cours de route. Vérifiez bien que vous avez :
- copié les classes Utilisateur, UtilisateurDao et DAOException en conservant leurs packages respectifs, depuis le projet pro_jpa ;
- copié le fichier META-INF/persistence.xml depuis le projet pro_jpa ;
- copié le fichier form.css depuis le projet test_jsf, en le plaçant dans l'arborescence /resources/default/css/ ;
- copié le fichier web.xml depuis le projet test_jsf ;
- démarré votre serveur MySQL ;
- démarré votre serveur GlassFish ;
- déployé votre projet pro_jsf sur le serveur GlassFish.
Le formulaire s'affichant correctement, nous pouvons alors tester une inscription. Pour ce premier cas, nous allons essayer avec des informations valides, et qui n'existent pas déjà en base. Par exemple, avec une adresse jamais utilisée auparavant, deux mots de passe corrects et identiques et un nom d'utilisateur suffisamment long, comme indiqué à la figure suivante.
Après un clic sur le bouton d'inscription, l'inscription fonctionne et le message de succès est affiché (voir la figure suivante).
Nous constatons alors que :
- c'est la page courante qui est rechargée. Comme je vous l'ai déjà expliqué, par défaut et sans règle de navigation particulière précisée par le développeur, c'est la page courante qui est automatiquement utilisée comme action d'un formulaire JSF ;
- le contenu des champs de saisie des mots de passe n'est pas réaffiché, alors que l'expression EL est bien présente dans l'attribut value des champs. Ce comportement est voulu, car il ne faut jamais retransmettre un mot de passe après validation d'un formulaire. Nous avions d'ailleurs pris garde à ne pas le faire dans notre ancienne page JSP d'inscription, si vous vous souvenez bien. Avec JSF, le composant
<h:inputSecret>
est programmé pour ne pas renvoyer son contenu, il n'y a donc plus d'erreur d'inattention possible ; - le message de succès est bien décoré avec le style décrit par la classe info de notre feuille CSS. JSF a donc bien utilisé la classe précisée dans l'attribut infoClass de la balise
<h:messages>
, ce qui est une preuve que le framework a bien attribué par défaut le niveauSeverity.INFO
au message que nous avons construit depuis notre backing-bean.
Essayons maintenant de nous inscrire en entrant des informations invalides, comme par exemple une adresse email déjà utilisée (celle que vous venez de saisir pour réaliser l'inscription précédente ira très bien). Nous constatons alors l'échec de notre système, qui plante et affiche un joli message de debug JSF. Toutefois, pas d'inquiétude, c'était prévu : puisque nous n'avons encore mis en place aucun contrôle, l'inscription a été tentée sans vérifier auparavant si l'adresse email existait déjà en base. MySQL a retourné une exception lors de cette tentative, car il a trouvé une entrée contenant cette adresse dans la table Utilisateur.
C'est très fâcheux, mais nous n'allons pas nous occuper de ce problème tout de suite. Poursuivons nos tests, et essayons cette fois-ci de nous inscrire en laissant plusieurs champs du formulaire vides. Par exemple, retournons sur le formulaire, saisissons uniquement une adresse email et cliquons sur le bouton d'inscription (voir la figure suivante).
Nous constatons alors l'affichage de messages d'erreurs à côté de chacun des champs laissés vides : c'est le fruit du composant <h:message>
! En outre, ces messages étant spécifiés comme étant des erreurs par JSF, le framework utilise l'attribut errorClass et les décore avec la classe erreur de notre feuille CSS : voilà pourquoi ces messages apparaissent en rouge.
Par contre, nous observons que ces messages automatiquement générés par JSF sont vraiment bruts de décoffrage… Il nous faut trouver un moyen de les rendre plus user-friendly, et c'est ce à quoi nous allons nous atteler dès maintenant.
Amélioration des messages affichés lors de la validation
Les messages automatiques générés par JSF sur chaque champ de notre formulaire sont vraiment laids, et cela s'explique très simplement : par défaut, JSF fait précéder ses messages d'erreurs des identifiants des objets concernés. En l'occurrence, il a concaténé l'identifiant de notre formulaire (j_idt7) et celui de chaque champ (motdepasse, confirmation et nom) en les séparant par le caractère :.
Quand avons-nous donné cet id barbare à notre formulaire ?
Eh bien en réalité, nous ne lui avons jamais donné d'identifiant, et JSF en a donc généré un par défaut, voilà pourquoi il est si laid. Ainsi, pour rendre ces messages moins repoussants, nous pouvons donc commencer par donner un id à notre formulaire. Nous allons par exemple l'appeler "formulaire", en changeant sa déclaration dans notre Facelet de <h:form>
à <h:form id="formulaire">
. Appliquez cette modification au code, puis tentez à nouveau le test précédent.
C'est déjà mieux, mais ça reste encore brut. Si nous regardons la documentation du composant <h:inputText>
, nous remarquons qu'il présente un attribut intitulé label, qui est utilisé pour représenter un champ de manière littérale. Nous allons donc ajouter un attribut label à chacune des balises déclarant un composant <h:inputText>
ou <h:inputSecret>
dans notre Facelet :
1 2 3 4 | <h:inputText id="email" value="#{inscrireBean.utilisateur.email}" ... label="Email" /> <h:inputSecret id="motdepasse" value="#{inscrireBean.utilisateur.motDePasse}" ... label="Mot de passe" /> <h:inputSecret id="confirmation" value="#{inscrireBean.utilisateur.motDePasse}" ... label="Confirmation" /> <h:inputText id="nom" value="#{inscrireBean.utilisateur.nom}"... label="Nom" /> |
Ajout d'un label sur chaque composant
Effectuez ces modifications, puis faites à nouveau le test (voir la figure suivante).
C'est un peu mieux, mais c'est encore brut, et c'est toujours en anglais… Pour régler ce problème une fois pour toutes, nous allons utiliser l'attribut requiredMessage des composants de saisie JSF qui, d'après leur documentation, permet de définir le message utilisé lors de la vérification de la règle définie par l'attribut required. En d'autres termes, nous allons y spécifier directement le message d'erreur à afficher lorsque le champ est laissé vide ! Nous allons donc laisser tomber nos attributs label, et les remplacer par ces nouveaux attributs requiredMessage :
1 2 3 4 | <h:inputText id="email" value="#{inscrireBean.utilisateur.email}" ... requiredMessage="Veuillez saisir une adresse email" /> <h:inputSecret id="motdepasse" value="#{inscrireBean.utilisateur.motDePasse}" ... requiredMessage="Veuillez saisir un mot de passe" /> <h:inputSecret id="confirmation" value="#{inscrireBean.utilisateur.motDePasse}" ... requiredMessage="Veuillez saisir la confirmation du mot de passe" /> <h:inputText id="nom" value="#{inscrireBean.utilisateur.nom}" ... requiredMessage="Veuillez saisir un nom d'utilisateur" /> |
Ajout d'un requiredMessage sur chaque composant
Effectuez ces modifications, puis faites à nouveau le test… Nous y voilà, les messages sont finalement propres et compréhensibles pour les utilisateurs. Petit bémol toutefois, du point de vue de la qualité du code, c'est un peu sale de définir directement en dur dans la vue les messages d'erreurs à afficher… Comme le hasard fait très bien les choses, il existe justement une fonctionnalité dans JSF qui permet de définir des messages dans un fichier externe, et d'y faire référence depuis les Facelets. Ce système s'appelle un bundle, et nous allons en mettre un en place dans notre exemple.
Mise en place d'un bundle
Un bundle n'est rien d'autre qu'un fichier de type Properties, contenant une liste de messages. La première étape dans la création d'un bundle consiste donc à créer un fichier Properties et à y placer nos différents messages de validation. Nous allons par exemple nommer notre fichier messages.properties. Il faut le placer dans les sources du projet, aux côtés du code de l'application. En l'occurrence nous allons le placer dans un package nommé com.sdzee.bundle
:
1 2 3 4 | inscription.email = Veuillez saisir une adresse email inscription.motdepasse = Veuillez saisir un mot de passe inscription.confirmation = Veuillez saisir la confirmation du mot de passe inscription.nom = Veuillez saisir un nom d'utilisateur |
com.sdzee.bundle.messages
Nous avons ici placé nos quatre messages, identifiés par le nom de la Facelet qui en fait usage (inscription) suivi d'un point et du nom du champ concerné. La syntaxe à respecter est celle d'un fichier de type Properties Java classique.
Une fois ce dossier en place, il faut maintenant le charger depuis notre Facelet pour qu'elle puisse faire référence à son contenu. Pour ce faire, nous allons utiliser le composant <f:loadBundle>
. Celui-ci doit être placé dans la page avant les balises qui en feront usage, typiquement nous pouvons le mettre dans le header de notre page :
1 2 3 4 | <h:head> ... <f:loadBundle basename="com.sdzee.bundle.messages" var="msg"/> </h:head> |
Ajout du chargement du bundle dans le header de la Facelet inscription.xhtml
Cette balise attend uniquement deux attributs :
- basename, qui contient le chemin complet dans lequel est placé le fichier (le package suivi du nom du fichier) ;
- var, qui permet de définir par quel nom le bundle sera désigné dans le reste de la page.
Nous avons donc précisé le chemin com.sdzee.bundle.messages
, et nous utiliserons le nom msg pour faire référence à notre bundle. Effectuez cet ajout dans le header de votre Facelet.
Il ne nous reste maintenant plus qu'à remplacer nos messages, actuellement en dur dans les attributs requiredMessage, par une référence vers les messages présents dans le bundle. Vous vous en doutez peut-être déjà, iI suffit pour cela d'utiliser des expressions EL ! Voilà comment nous allons procéder :
1 2 3 4 | <h:inputText id="email" value="#{inscrireBean.utilisateur.email}" ... requiredMessage="#{msg['inscription.email']}" /> <h:inputSecret id="motdepasse" value="#{inscrireBean.utilisateur.motDePasse}" ... requiredMessage="#{msg['inscription.motdepasse']}" /> <h:inputSecret id="confirmation" value="#{inscrireBean.utilisateur.motDePasse}" ... requiredMessage="#{msg['inscription.confirmation']}" /> <h:inputText id="nom" value="#{inscrireBean.utilisateur.nom}" ... requiredMessage="#{msg['inscription.nom']}" /> |
Remplacement des messages en dur dans les attributs requiredMessage
La forme de l'expression EL utilisée est simple : nous ciblons le bundle via son nom msg, puis nous utilisons la notation avec les crochets pour cibler le nom souhaité.
Effectuez ces dernières modifications dans le code de votre Facelet, puis testez à nouveau votre formulaire. Si vous n'avez rien oublié et si vous avez correctement positionné votre fichier bundle, vous devriez observer exactement le même comportement que lors du test effectué avec les messages écrits en dur.
L'intérêt de cette technique, c'est bien évidemment d'écrire un code plus propre dans vos Facelets, mais surtout de regrouper tous vos messages de validation dans un seul et unique fichier. Pratique, notamment pour l'internationalisation ! Par exemple, si vous souhaitez traduire votre application dans une langue différente, vous n'avez alors qu'à changer de bundle : inutile de repasser sur chacune de vos Facelets pour traduire les messages un par un !
Nous allons nous arrêter là pour les tests et améliorations. Passons maintenant au paragraphe suivant, afin de rendre tout ce mécanisme… ajaxisé !
Une inscription ajaxisée
Présentation
Qu'est-ce que c'est, une inscription "ajaxisée" ?
AJAX est l'acronyme d'Asynchronous Javascript and XML, ce qui en français signifie littéralement « Javascript et XML asynchrones ». Derrière cette appellation se cache un ensemble de technologies qui permettent la mise à jour d'un fragment d'une page web sans que le rechargement complet de la page web visitée par l'utilisateur ne soit nécessaire. C'est ce type de technologie qui permet à certains sites de proposer des fonctionnalités avancées et intuitives à leurs utilisateurs. Citons par exemple le Site du Zéro qui propose l'auto-complétion lors de la saisie dans le champ de recherche d'un membre, ou encore le site Stackoverflow avec son système de vote en direct sur les réponses posées et questions apportées.
Ainsi, lorsque je parle d'inscription ajaxisée, je désigne en réalité le fait de pouvoir valider le contenu de chacun des champs de notre formulaire d'inscription sans nécessiter un clic sur le bouton d'envoi, ni nécessiter un rechargement de la page entière.
L'AJAX avec JSF
Si nous travaillions toujours à la main, il nous faudrait mettre les mains dans le cambouis et mettre en place du JavaScript, des traitements spéciaux dans nos servlets pour ne déclencher l'actualisation que d'un morceau de la page visitée par l'utilisateur, etc. Heureusement, avec JSF nous allons pouvoir garder nos mains propres : le framework nous propose un moyen ultra-simple pour court-circuiter le processus classique de traitement d'une requête, et permettre à un composant de s'actualiser de manière indépendante, et non pas dans le flot complet de l'arbre des composants présents dans la vue courante comme c'est le cas traditionnellement.
La solution offerte se matérialise sous la forme… d'un composant ! C'est cette fois une balise de la bibliothèque Core que nous allons utiliser : la bien nommée <f:ajax>
. Sans plus attendre, je vous propose le nouveau code de notre Facelet, et nous en discutons ensuite.
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 | <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:f="http://java.sun.com/jsf/core" xmlns:h="http://java.sun.com/jsf/html"> <h:head> <meta charset="utf-8" /> <title>Inscription</title> <h:outputStylesheet library="default" name="css/form.css" /> <f:loadBundle basename="com.sdzee.bundle.messages" var="msg"/> </h:head> <h:body> <h:form id="formulaire"> <fieldset> <legend>Inscription</legend> <h:outputLabel for="email">Adresse email <span class="requis">*</span></h:outputLabel> <h:inputText id="email" value="#{inscrireBean.utilisateur.email}" required="true" size="20" maxlength="60" requiredMessage="#{msg['inscription.email']}"> <f:ajax event="blur" render="emailMessage" /> </h:inputText> <h:message id="emailMessage" for="email" errorClass="erreur" /> <br /> <h:outputLabel for="motdepasse">Mot de passe <span class="requis">*</span></h:outputLabel> <h:inputSecret id="motdepasse" value="#{inscrireBean.utilisateur.motDePasse}" required="true" size="20" maxlength="20" requiredMessage="#{msg['inscription.motdepasse']}"> <f:ajax event="blur" render="motDePasseMessage" /> </h:inputSecret> <h:message id="motDePasseMessage" for="motdepasse" errorClass="erreur" /> <br /> <h:outputLabel for="confirmation">Confirmation du mot de passe <span class="requis">*</span></h:outputLabel> <h:inputSecret id="confirmation" value="#{inscrireBean.utilisateur.motDePasse}" required="true" size="20" maxlength="20" requiredMessage="#{msg['inscription.confirmation']}"> <f:ajax event="blur" render="confirmationMessage" /> </h:inputSecret> <h:message id="confirmationMessage" for="confirmation" errorClass="erreur" /> <br /> <h:outputLabel for="nom">Nom d'utilisateur <span class="requis">*</span></h:outputLabel> <h:inputText id="nom" value="#{inscrireBean.utilisateur.nom}" required="true" size="20" maxlength="20" requiredMessage="#{msg['inscription.nom']}"> <f:ajax event="blur" render="nomMessage" /> </h:inputText> <h:message id="nomMessage" for="nom" errorClass="erreur" /> <br /> <h:messages globalOnly="true" infoClass="info" /> <h:commandButton value="Inscription" action="#{inscrireBean.inscrire}" styleClass="sansLabel"> <f:ajax execute="@form" render="@form" /> </h:commandButton> <br /> </fieldset> </h:form> </h:body> </html> |
/inscription.xhtml
Les différences apparentes avec la version précédente de la Facelet sont minimes, seules quelques lignes font leur apparition :
- chaque balise
<h:inputText>
et<h:inputSecret>
contient dorénavant un corps, et dans ce corps est placée la fameuse balise<f:ajax>
; - de même, la balise
<h:commandButton>
contient dorénavant elle aussi un corps, dans lequel est logée une balise<f:ajax>
.
Étudions la construction de cette nouvelle balise. Seuls deux de ses attributs nous sont utiles dans les champs de saisie :
- event, qui nous permet de définir l'action déclenchant l'envoi de la requête AJAX au serveur ;
- render, qui nous permet de définir le ou les composants dont le rendu doit être effectué, une fois la requête traitée par le serveur.
Autrement dit, c'est extrêmement simple : il suffit de placer la balise <f:ajax>
dans le corps du champ qui est concerné par l'événement déclencheur, et de préciser dans l'attribut render le(s) composant(s) pour le(s)quel(s) nous souhaitons relancer un rendu graphique.
Dans notre cas, ce qui nous intéresse c'est de faire valider le contenu d'un champ dès que l'utilisateur a terminé la saisie, et d'actualiser le message associé au champ en conséquence :
- l'event que nous utilisons pour considérer que l'utilisateur a terminé la saisie du contenu d'un champ s'intitule blur. Cette propriété JavaScript va permettre de déclencher une requête dès que le champ courant perd le focus, c'est-à-dire dès que l'utilisateur clique dans un autre champ, ou ailleurs sur la page, ou bien lorsqu'il navigue en dehors du champ via la touche tabulation de son clavier par exemple ;
- pour désigner quel composant actualiser, nous devons simplement préciser son id dans l'attribut render.
La balise <f:ajax>
présente sur chaque champ de saisie va soumettre (et donc valider !) seulement le contenu du champ courant, et déclencher un réaffichage du message associé lorsque le champ va perdre le focus. La balise <f:ajax>
placée sur le bouton de validation est un peu différente des autres : elle permet d'effectuer un envoi complet, mais là encore ajaxisé et non pas simplement un envoi classique avec rechargement de la page. Pour ce faire, vous constatez l'emploi d'un mot-clé un peu spécial : @form
. Il existe quatre marqueurs de la sorte :
@this
: désigne le composant englobant la balise<f:ajax>
;@form
: désigne le formulaire entier ;@all
: désigne l'arbre des composants de la page entière ;@none
: ne désigne aucun composant.
En précisant @form
dans l'attribut render, nous nous assurons ainsi que tout le formulaire va être actualisé lors d'un clic sur le bouton d'envoi. Par ailleurs, nous n'utilisons plus l'attribut event comme nous le faisions sur les champs de saisie, car nous savons très bien que c'est un clic sur le bouton qui va déclencher l'action. Nous utilisons cette fois l'attribut execute, qui permet de définir la portée de l'action à effectuer : en l'occurrence, nous souhaitons bien traiter le formulaire complet.
Effectuez ces modifications dans votre Facelet, puis testez à nouveau votre formulaire d'inscription. À chaque fois que vous allez cliquer dans un champ de saisie, puis en dehors de ce champ, vous pourrez observer l'actualisation des éventuels messages d'erreurs associés à chaque champ, et ce presque en temps réel !
En fin de compte, JSF offre un masquage total de ce qui se passe sous la couverture ! Il suffit d'inclure une simple balise dans le corps d'un composant, et le tour est joué. C'est d'une facilité à la fois admirable et déconcertante !
L'importance de la portée d'un objet
Avant de passer à la suite, je tiens à vous faire remarquer quelque chose de très inquiétant : nous avons oublié de nous soucier de la portée de notre backing-bean ! Souvenez-vous, celle que nous avions déclarée via l'annotation @RequestScoped
. Nous ne nous en étions pas inquiétés plus que ça sur le moment, mais maintenant que nous avons ajaxisé notre formulaire, cette notion de portée va prendre de l'importance.
Actuellement, notre objet est dans la portée requête. Cela signifie que notre bean ne vit que le temps d'une requête : il est créé par JSF à l'arrivée d'une requête sur le serveur, puis détruit une fois la réponse renvoyée. Autrement dit, notre formulaire provoque la création et la destruction d'un bean sur le serveur à chaque fois que l'utilisateur change de champ ! C'est du gâchis de performances.
Nous pourrions envisager de mettre notre objet en session plutôt que dans la portée requête. Oui, mais la session est un objet qu'il est coûteux de maintenir pour le serveur, car il faut conserver un unique objet pour chaque visiteur utilisant l'application. Voilà pourquoi en pratique, il ne faut utiliser la session que lorsque c'est impérativement nécessaire, comme pour réaliser un panier d'achats par exemple, où les items commandés doivent absolument être conservés tout au long de la navigation du visiteur. Dans notre simple système d'inscription, garder nos informations serait du pur gâchis de mémoire.
Nous devons donc impérativement réfléchir à ce type de problématique lorsque nous développons une application : car plus il y a d'utilisateurs, plus le gaspillage de ressources va être susceptible de poser des problèmes que vous ne pouvez pas identifier par de simples tests mono-utilisateur. En d'autres termes, votre application peut très bien donner l'impression de fonctionner au poil et d'être parfaitement codée, jusqu'au jour où un nombre critique d'utilisateurs simultanés va faire surgir des problèmes imprévus.
Pour revenir à notre cas, il existe une portée qui se place entre la requête et la session, et qui semble donc parfaite pour ce que nous faisons ici : le scope de conversation. Il permet de conserver un objet tant qu'une même vue est utilisée par un même utilisateur. Cela signifie que tant qu'un utilisateur effectue des requêtes depuis une seule et même page, alors l'objet est conservé sur le serveur et réutilisé. Dès que l'utilisateur effectue une requête vers une autre vue, alors l'objet est finalement détruit. Dans notre cas, c'est parfait : notre formulaire provoque l'envoi de plusieurs requêtes vers le serveur pour la validation de chacun des champs, et toutes constituent un échange continu entre le serveur et une unique vue. Nous allons donc placer notre objet dans cette portée, pour limiter le gâchis de ressources sur le serveur.
Pour déclarer un backing-bean dans cette portée, il faut l'annoter avec @ViewScoped
, et non plus avec @RequestScoped
. Voilà tout ce qu'il nous faut changer pour optimiser notre application !
À titre d'exercice, et si vous avez déjà parcouru l'annexe sur le débuggage de projet avec Eclipse, je vous encourage à utiliser le mode debug pour vérifier de vos propres yeux la conservation d'une instance du backing-bean d'une requête à l'autre avec le scope de conversation, et sa destruction avec le scope de requête.
Une inscription contrôlée
Déporter la validation de la vue vers l'entité
Nous nous sommes jusqu'à présent contentés d'effectuer une simple vérification sur les champs du formulaire, directement dans la vue grâce aux attributs required et requiredMessage. Nous allons déplacer ces conditions de la vue vers notre modèle, à savoir notre entité, grâce à de simples annotations ! Le serveur GlassFish fournit par défaut un moyen de validation, identifié sous le nom de JSR 303 : il contient différentes contraintes de validation sous forme d'annotations, par exemple @NotNull
qui permet d'indiquer qu'une propriété ne peut être laissée vide.
Première étape, nous allons donc supprimer de notre Facelet les quelques required et requiredMessage présents sur les champs de notre formulaire, et que nous utilisions justement pour indiquer que nos champs ne pouvaient pas être laissés vides. Il nous suffit ensuite d'éditer notre entité Utilisateur et d'y ajouter les annotations sur les attributs correspondants pour remettre en place ces contraintes. Voici le code de notre entité reprise et complétée (j'omets ici les méthodes getters/setters pour ne pas encombrer le code inutilement) :
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 | package com.sdzee.entities; import java.sql.Timestamp; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.validation.constraints.NotNull; @Entity public class Utilisateur { @Id @GeneratedValue( strategy = GenerationType.IDENTITY ) private Long id; @NotNull private String email; @Column( name = "mot_de_passe" ) @NotNull private String motDePasse; @NotNull private String nom; @Column( name = "date_inscription" ) private Timestamp dateInscription; .... } |
com.sdzee.entities.Utilisateur
Les seuls ajouts effectués sont les trois annotations @NotNull
, issues du package javax.validation.constraints
.
Par contre, comprenez bien que ceci ne fonctionnera que si vous avez suivi mon conseil en début de chapitre, et ajouté javax.faces.INTERPRET_EMPTY_STRING_SUBMITTED_VALUES_AS_NULL
en paramètre de contexte. Sans cette configuration, les champs du formulaire laissés vides seraient traités comme des chaînes vides par votre serveur, et par conséquent celles-ci ne seraient pas détectées comme nulles par ces annotations fraîchement mises en place dans l'entité.
Le seul cas où vous pourriez vous passer de cette configuration se présenterait si vous utilisiez Hibernate en guise de framework ORM, et que vous utilisiez l'annotation spécifique à Hibernate intitulée @NotEmpty
. Cependant comme je vous l'ai déjà expliqué, si vous commencez à mettre du code spécifique à Hibernate dans votre application, vous ne pourrez plus revenir en arrière, c'est-à-dire changer d'implémentation de JPA, sans modifier votre code…
Une fois ces légères modifications effectuées, ouvrez à nouveau la page d'inscription dans votre navigateur, et tentez une nouvelle fois de vous inscrire en laissant des champs vides. Vous constaterez alors que les contraintes fonctionnent bien, mais que les messages de validation sont une nouvelle fois trop génériques (voir la figure suivante).
Pas d'inquiétude, nous allons pouvoir les personnaliser directement depuis notre entité en complétant nos annotations :
1 2 3 4 5 6 7 8 9 10 11 | ... @NotNull( message = "Veuillez saisir une adresse email" ) private String email; @Column( name = "mot_de_passe" ) @NotNull( message = "Veuillez saisir un mot de passe" ) private String motDePasse; @NotNull( message = "Veuillez saisir un nom d'utilisateur" ) private String nom; ... |
com.sdzee.entities.Utilisateur
s
Il suffit comme vous pouvez l'observer dans cet exemple de préciser entre parenthèses un attribut message à l'annotation @NotNull
.
Nous voilà de retour à une situation similaire à celle que nous observions lorsque nous effectuions la validation directement depuis notre vue (voir la figure suivante).
Quelle est la méthode de validation à préférer : depuis la vue, ou depuis l'entité ?
Cela dépend principalement des contraintes du projet. Par exemple, si l'application doit pouvoir fonctionner sur un serveur léger comme Tomcat sans support des EJB ni de JPA, alors il ne faudra pas utiliser la validation depuis l'entité mais lui préférer la validation depuis la vue JSF.
Affiner les contrôles effectués
Allons un petit peu plus loin, et essayons de nous rapprocher davantage du fonctionnement de notre formulaire lorsque nous utilisions une page JSP et notre objet métier fait maison.
Nous souhaitons affiner la validation des différents champs de notre formulaire. Dans notre ancien objet métier, nous procédions aux vérifications suivantes :
- que l'adresse respecte bien le format standard d'une adresse email ;
- que le mot de passe soit long de 3 caractères ou plus ;
- que le nom soit long de 3 caractères ou plus.
Le hasard fait encore une fois bien les choses :l'annotation @Pattern
fournie par la JSR 303 est parfaite pour vérifier le format de l'adresse grâce à l'utilisation d'expressions régulières, et l'annotation @Size
est parfaite pour vérifier la taille des champs ! Voici le code complété :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | ... @NotNull( message = "Veuillez saisir une adresse email" ) @Pattern( regexp = "([^.@]+)(\\.[^.@]+)*@([^.@]+\\.)+([^.@]+)", message = "Merci de saisir une adresse mail valide" ) private String email; @Column( name = "mot_de_passe" ) @NotNull( message = "Veuillez saisir un mot de passe" ) @Size( min = 3, message = "Le mot de passe doit contenir au moins 3 caractères" ) private String motDePasse; @NotNull( message = "Veuillez saisir un nom d'utilisateur" ) @Size( min = 3, message = "Le nom d'utilisateur doit contenir au moins 3 caractères" ) private String nom; ... |
com.sdzee.entities.Utilisateur
Les documentations respectives des deux annotations expliquent que l'attribut regexp permet de définir une expression régulière qui sera appliquée au champ ciblé par l'annotation @Pattern
, et que l'attribut min permet de définir la taille minimale autorisée pour le champ ciblé par l'annotation @Size
.
Effectuez ces ajouts, puis tentez à nouveau de vous inscrire en saisissant un nom ou un mot de passe trop court, et une adresse email dont le format est incorrect.
Rendez-vous bien compte de la simplicité avec laquelle nous avons mis en place ces quelques vérifications : de simples annotations placées sur les champs de l'entité suffisent ! Alors qu'auparavant, nous avions dû écrire pas loin d'une centaine de lignes de code dans notre objet métier, rien que pour mettre en place ces vérifications et gérer les exceptions proprement…
Par ailleurs, nous n'avons pas mis en place ce type de contrôle dans notre application, mais nous pourrions tout à fait vérifier que le mot de passe fourni par l'utilisateur présente un niveau de sécurité suffisamment élevé. Pour ce faire, l'annotation @Pattern
se révèle une nouvelle fois très utile. Voici par exemple le code à mettre en place afin de s'assurer que le mot de passe entré par l'utilisateur contient au moins 8 caractères, dont au moins un chiffre, une lettre minuscule et une lettre majuscule :
1 2 3 | @NotNull( message = "Veuillez saisir un mot de passe" ) @Pattern(regexp = ".*(?=.{8,})(?=.*\\d)(?=.*[a-z])(?=.*[A-Z]).*", message = "Le mot de passe saisi n'est pas assez sécurisé") private String motDePasse; |
Vérification du niveau de sécurité du mot de passe saisi
Simple et rapide, n'est-ce pas ? La puissance des expressions régulières associée à la simplicité des annotations, c'est tout simplement magique ! Alors qu'il nous aurait fallu écrire une barbante méthode de traitement supplémentaire et l'exception associée dans notre ancien objet métier, nous pouvons désormais nous contenter d'une courte annotation sur le champ à valider, directement dans notre entité !
Ajouter des contrôles "métier"
Jusqu'à présent, nous avons réussi à appliquer des contrôles sur le format des données de manière étonnamment simple. Cela dit, nous avons lâchement évité deux contrôles qui étaient pourtant en place dans notre ancien objet métier, et qui sont nécessaires pour réaliser une inscription valide :
- le mot de passe et sa confirmation doivent être égaux ;
- l'adresse email ne doit pas exister dans la base de données.
Ces contraintes se distinguent des simples vérifications de format que nous avons mises en place jusqu'à présent, car elles ont trait à l'aspect métier de notre application. Nous devons donc impérativement trouver un moyen propre de mettre en place ces contrôles dans notre nouvelle architecture.
Comme toujours, JSF propose un outil dédié à notre besoin. Le framework fournit une interface nommée javax.faces.validator.Validator
, qui permet de créer une classe contenant une méthode de validation, qui pourra alors être liée à un composant très simplement depuis la vue, via un attribut placé dans une balise.
Nous allons pour commencer mettre en place la vérification de l'existence de l'adresse email dans la base de données. Pour ce faire, nous devons créer un nouvel objet implémentant cette interface Validator, que nous allons nommer ExistenceEmailValidator et placer dans un nouveau package com.sdzee.validators
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | package com.sdzee.validators; import javax.faces.application.FacesMessage; import javax.faces.component.UIComponent; import javax.faces.context.FacesContext; import javax.faces.validator.Validator; import javax.faces.validator.ValidatorException; public class ExistenceEmailValidator implements Validator { @Override public void validate( FacesContext context, UIComponent component, Object value ) throws ValidatorException { ... } } |
com.sdzee.validators.ExistenceEmailValidator
La seule méthode qu'il est nécessaire de surcharger est la méthode validate() : c'est elle qui va contenir le code métier chargé d'effectuer le contrôle de l'existence de l'adresse dans la base. Nous savons déjà comment réaliser cette tâche, nous l'avions déjà fait dans notre ancien objet métier. Il nous suffit donc d'adapter le code que nous avions alors écrit. Voici un exemple de solution :
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 | package com.sdzee.validators; import javax.ejb.EJB; import javax.faces.application.FacesMessage; import javax.faces.component.UIComponent; import javax.faces.context.FacesContext; import javax.faces.validator.Validator; import javax.faces.validator.ValidatorException; import com.sdzee.dao.DAOException; import com.sdzee.dao.UtilisateurDao; public class ExistenceEmailValidator implements Validator { private static final String EMAIL_EXISTE_DEJA = "Cette adresse email est déjà utilisée"; @EJB private UtilisateurDao utilisateurDao; @Override public void validate( FacesContext context, UIComponent component, Object value ) throws ValidatorException { /* Récupération de la valeur à traiter depuis le paramètre value */ String email = (String) value; try { if ( utilisateurDao.trouver( email ) != null ) { /* * Si une adresse est retournée, alors on envoie une exception * propre à JSF, qu'on initialise avec un FacesMessage de * gravité "Erreur" et contenant le message d'explication. Le * framework va alors gérer lui-même cette exception et s'en * servir pour afficher le message d'erreur à l'utilisateur. */ throw new ValidatorException( new FacesMessage( FacesMessage.SEVERITY_ERROR, EMAIL_EXISTE_DEJA, null ) ); } } catch ( DAOException e ) { /* * En cas d'erreur imprévue émanant de la BDD, on prépare un message * d'erreur contenant l'exception retournée, pour l'afficher à * l'utilisateur ensuite. */ FacesMessage message = new FacesMessage( FacesMessage.SEVERITY_ERROR, e.getMessage(), null ); FacesContext facesContext = FacesContext.getCurrentInstance(); facesContext.addMessage( component.getClientId( facesContext ), message ); } } } |
com.sdzee.validators.ExistenceEmailValidator
Vous devriez comprendre sans problème avec les commentaires présents dans le code. Vous découvrez ici de nouveaux objets du framework JSF, comme UIComponent ou ValidatorException, et en retrouvez d'autres que vous connaissez déjà comme FacesContext et FacesMessage. N'hésitez pas à parcourir leur documentation, via Eclipse ou dans la Javadoc en ligne directement depuis votre navigateur, si vous souhaitez en apprendre davantage sur les constructeurs et méthodes ici utilisés.
Vous retrouvez en outre l'injection de notre EJB Stateless, le DAO Utilisateur, dans notre objet via l'annotation JPA @EJB
.
Maintenant que notre objet est prêt, il nous faut le déclarer auprès de JSF afin qu'il le rende accessible à notre vue. Oui, parce que pour le moment c'est bien gentil d'avoir codé un objet implémentant l'interface Validator, mais encore faut-il que notre vue puisse s'en servir pour l'appliquer au champ de saisie de l'adresse email ! Comme d'habitude, les annotations sont là pour nous sauver, et JSF propose l'annotation @FacesValidator
. Celle-ci permet de déclarer auprès du framework un objet comme étant un Validator, et permet ainsi de rendre cet objet accessible depuis une balise dans nos Facelets.
Malheureusement, nous n'allons pas pouvoir utiliser cette annotation… Pourquoi ? Eh bien il s'agit là d'un comportement étrange de JSF : les objets annotés avec @FacesValidator
ne sont pas pris en charge par le conteneur d'injection.
Qu'est-ce que cela veut dire, concrètement ?
Formulé autrement, cela signifie qu'il n'est pas possible d'injecter un EJB avec l'annotation @EJB
dans un Validator JSF annoté avec @FacesValidator
. C'est un problème, car nous faisons usage de notre DAO Utilisateur dans notre validateur, et puisque nous travaillons avec JPA nous avons besoin de pouvoir y injecter cet EJB.
D'après les informations qui circulent sur la toile, il semblerait que la communauté des développeurs chargés du maintien de JSF et de JPA travaille sur ce point, et qu'une correction soit prévue pour la prochaine version à venir de JSF (JSF 2.2, pas encore sorti lors de la rédaction de ce cours).
Il nous faut donc contourner cette limitation nous-mêmes. Il existe plusieurs moyens : nous pouvons conserver l'annotation @FacesValidator
et récupérer manuellement notre EJB depuis notre validateur, ou encore laisser tomber l'annotation et nous débrouiller autrement. Nous allons opter pour la seconde méthode, et en ce qui concerne la première je vous renvoie vers cet excellent article pour plus de détails.
Nous n'allons donc pas utiliser cette annotation. À la place, nous allons déclarer notre objet comme un simple backing-bean ! Ainsi, notre vue pourra y accéder, c'est le principe même du backing-bean, et nous pourrons y injecter notre EJB sans problème. Nous devons donc ajouter les annotations suivantes à notre objet :
1 2 3 4 5 6 | ... @ManagedBean @RequestScoped public class ExistenceEmailValidator implements Validator { ... |
Annotations de notre validateur
Au sujet de la portée requête ici utilisée, puisque notre validateur ne sera utilisé que pour valider un champ, nous pouvons utiliser la portée par défaut sans souci.
Maintenant que notre objet est déclaré et donc accessible depuis la vue, nous devons ajouter une balise à notre Facelet pour qu'elle fasse appel à ce validateur pour le champ de saisie de l'adresse email. Pour ce faire, nous allons utiliser la balise <f:validator>
. En regardant sa documentation, nous découvrons qu'elle possède un attribut validatorId permettant de préciser quel objet utiliser en tant que validateur. Oui mais voilà, cet attribut ne fonctionne que si notre objet est annoté avec @FacesValidator
, et pour des raisons techniques évoquées un peu plus tôt nous avons décidé de ne pas utiliser cette annotation…
Heureusement, la balise propose un autre attribut intitulé binding, qui permet de préciser un backing-bean à utiliser en tant que validateur, à la condition que cet objet implémente l'interface Validator. Bingo ! C'est exactement notre cas, et il ne nous reste donc plus qu'à ajouter cette balise dans notre Facelet. Le code responsable du champ de saisie de l'adresse email va donc devenir :
1 2 3 4 | <h:inputText id="email" value="#{inscrireBean.utilisateur.email}" size="20" maxlength="60"> <f:ajax event="blur" render="emailMessage" /> <f:validator binding="#{existenceEmailValidator}" /> </h:inputText> |
Ajout du validateur sur le champ de saisie de l'adresse email
Sans surprise, nous utilisons une expression EL pour cibler notre backing-bean, comme nous l'avons déjà fait pour inscriptionBean dans les autres balises. Sa dénomination est par défaut, je vous le rappelle, le nom de la classe de l'objet débutant par une minuscule, à savoir existenceEmailValidator dans notre cas.
Une fois cette modification effectuée, rendez-vous à nouveau sur la page d'inscription depuis votre navigateur, et essayez de vous inscrire avec une adresse email qui existe déjà dans votre base de données, comme indiqué sur la figure suivante.
Vous constatez alors que la validation est effectuée comme prévu : en plus des contrôles sur le format de l'adresse email, votre application vérifie maintenant que l'adresse saisie n'existe pas déjà dans la base de données, et affiche un message d'erreur le cas échéant ! Par ailleurs, vous remarquerez que puisque nous avons donné le niveau FacesMessage.SEVERITY_ERROR
à notre message d'erreur depuis la méthode de validation, le message est bien considéré comme tel par JSF et est coloré en rouge (la classe CSS erreur lui est appliquée).
Il nous reste encore un contrôle à effectuer avant d'en avoir terminé avec notre formulaire : vérifier que le mot de passe et la confirmation saisis sont égaux. De la même manière que précédemment, nous allons créer un validateur dédié à cette tâche. Seulement cette fois-ci, nous n'allons pas avoir besoin d'injecter un EJB dans notre objet. En effet, pour vérifier que les deux champs sont égaux, nous n'avons absolument pas besoin de faire appel à notre DAO Utilisateur. Nous allons donc pouvoir utiliser l'annotation @FacesValidator
pour lier notre validateur à la vue :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | package com.sdzee.validators; import javax.faces.component.UIComponent; import javax.faces.component.UIInput; import javax.faces.context.FacesContext; import javax.faces.validator.FacesValidator; import javax.faces.validator.Validator; import javax.faces.validator.ValidatorException; @FacesValidator( value = "confirmationMotDePasseValidator" ) public class ConfirmationMotDePasseValidator implements Validator { @Override public void validate( FacesContext context, UIComponent component, Object value ) throws ValidatorException { ... } } |
com.sdzee.validators.ConfirmationMotDePasseValidator
Celle-ci attend en paramètre le nom qui sera utilisé comme identifiant depuis la vue pour désigner ce validateur : nous lui donnons ici le même nom que la classe, avec la première lettre en minuscule pour respecter la règle à suivre pour les backing-bean et avoir des dénominations homogènes dans notre Facelet.
Voilà tout pour la forme. Le plus important maintenant, c'est de trouver un moyen de comparer les contenus de deux champs différents. Car c'est bien cela que nous cherchons à faire : comparer le contenu du champ de saisie du mot de passe avec celui de la confirmation. Pour ce faire, nous allons commencer par regarder comment procéder depuis notre Facelet, avant de revenir sur notre objet et de coder la méthode de validation.
Commençons tout simplement par lier notre validateur, même s'il n'est pas encore terminé, au composant en charge de la confirmation :
1 2 3 4 | <h:inputSecret id="confirmation" value="#{inscrireBean.utilisateur.motDePasse}" size="20" maxlength="20"> <f:ajax event="blur" render="confirmationMessage" /> <f:validator validatorId="confirmationMotDePasseValidator" /> </h:inputSecret> |
Puisque cette fois nous avons pu nous servir de l'annotation @FacesValidator
, nous pouvons donc utiliser l'attribut validatorId de la balise <f:validator>
. Celle-ci, à la différence de l'attribut binding, n'attend pas une expression EL mais directement le nom du validateur. Il s'agit de celui que nous avons défini en tant que paramètre de l'annotation dans notre validateur, en l'occurrence confirmationMotDePasseValidator. En mettant en place cette balise, nous nous assurons ainsi que notre validateur est associé au composant en charge du champ de confirmation.
La prochaine étape va consister à déclarer le composant en charge du champ de mot de passe en tant qu'attribut du composant en charge de la confirmation. L'intérêt de réaliser cette association est de rendre disponible le contenu du champ de mot de passe depuis le composant en charge du champ de confirmation. Ne vous embrouillez pas trop pour le moment, regardez simplement comment procéder :
1 2 3 4 5 | <h:inputSecret id="confirmation" value="#{inscrireBean.utilisateur.motDePasse}" size="20" maxlength="20"> <f:ajax event="blur" render="confirmationMessage" /> <f:attribute name="composantMotDePasse" value="#{composantMotDePasse}" /> <f:validator validatorId="confirmationMotDePasseValidator" /> </h:inputSecret> |
Nous utilisons la balise <f:attribute>
pour mettre en place l'association. Sa propriété name permet de définir le nom de l'objet qui va être créé en tant qu'attribut au composant courant, et sa propriété value permet de définir son contenu. Nous avons donc nommé l'objet composantMotDePasse, et avons lié la propriété value directement avec sa valeur en utilisant l'expression EL #{composantMotDePasse}
.
Cette déclaration permet donc de créer un objet représentant le champ mot de passe en tant qu'attribut du composant de confirmation, mais il faut encore faire en sorte que la valeur du champ mot de passe soit bien affectée à la valeur de cet objet. Pour ce faire, nous allons modifier la déclaration du composant en charge du mot de passe de cette manière :
1 2 3 | <h:inputSecret id="motdepasse" value="#{inscrireBean.utilisateur.motDePasse}" binding="#{composantMotDePasse}" size="20" maxlength="20"> <f:ajax event="blur" render="motDePasseMessage" /> </h:inputSecret> |
En ajoutant un attribut binding et en le liant à la valeur de l'objet via l'expression EL #{composantMotDePasse}
, nous nous assurons alors que la valeur saisie dans le champ de mot de passe sera affectée à l'objet déclaré en tant qu'attribut du composant de confirmation !
La dernière étape consiste maintenant à accorder nos violons. Actuellement, nos validations AJAX déclenchent le rendu de leur champ respectif uniquement. Maintenant que nous vérifions l'égalité entre les deux champs, pour rendre l'expérience utilisateur plus intuitive et logique, il faut faire en sorte que la validation AJAX de l'un entraîne la validation de l'autre, et vice-versa. Pour ce faire, nous devons modifier les balises <f:ajax>
que nous avions mises en place, et le code final devient alors :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | ... <h:inputSecret id="motdepasse" value="#{inscrireBean.utilisateur.motDePasse}" binding="#{composantMotDePasse}" size="20" maxlength="20"> <f:ajax event="blur" execute="motdepasse confirmation" render="motDePasseMessage confirmationMessage" /> </h:inputSecret> ... <h:inputSecret id="confirmation" value="#{inscrireBean.utilisateur.motDePasse}" size="20" maxlength="20"> <f:validator validatorId="confirmationMotDePasseValidator" /> <f:attribute name="composantMotDePasse" value="#{composantMotDePasse}" /> <f:ajax event="blur" execute="motdepasse confirmation" render="motDePasseMessage confirmationMessage" /> </h:inputSecret> ... |
Vérification de l'égalité des mots de passe
Seules les balises <f:ajax>
ont changé. Deux choses importantes à remarquer :
- l'ajout effectué dans les attributs render. Avec cette configuration, l'affichage du message associé au mot de passe ET de celui associé à la confirmation sera actualisé quoi qu'il arrive. Pour information, mais vous pouvez trouver cela vous-mêmes dans la documentation de la balise
<f:ajax>
, il suffit de séparer les composants à actualiser par un espace. - l'utilisation de l'attribut execute. Je vous ai déjà expliqué qu'il permet de définir la portée de l'action réalisée. C'est exactement ce dont nous avons besoin ici : nous voulons que lorsque l'événement blur est déclenché, à la fois le composant mot de passe et le composant confirmation soient traités. Voilà pourquoi nous y précisons les identifiants de nos deux champs de saisie.
Notre Facelet est enfin terminée, il ne nous reste maintenant plus qu'à coder la méthode de validation, dans notre Validator associé au champ de confirmation. Le petit challenge qui nous attend, c'est de trouver un moyen pour récupérer le contenu du champ mot de passe depuis le composant traité par la méthode, c'est-à-dire le composant de confirmation.
En regardant la documentation de l'objet UIComponent
, nous y trouvons la méthode getAttributes()
qui permet de récupérer une liste des attributs du composant. C'est justement ce que nous cherchons à faire ! Eh oui, rappelez-vous : nous avons déclaré le composant mot de passe comme attribut du composant confirmation dans notre Facelet, par le biais de la balise <f:attribute>
. Nous apprenons que les éléments de cette liste sont accessibles par leur nom, il va donc nous suffire de cibler l'élément nommé composantMotDePasse et le tour est joué ! Voici le code final de notre validateur :
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 | package com.sdzee.validators; import javax.faces.application.FacesMessage; import javax.faces.component.UIComponent; import javax.faces.component.UIInput; import javax.faces.context.FacesContext; import javax.faces.validator.FacesValidator; import javax.faces.validator.Validator; import javax.faces.validator.ValidatorException; @FacesValidator( value = "confirmationMotDePasseValidator" ) public class ConfirmationMotDePasseValidator implements Validator { private static final String CHAMP_MOT_DE_PASSE = "composantMotDePasse"; private static final String MOTS_DE_PASSE_DIFFERENTS = "Le mot de passe et la confirmation doivent être identiques."; @Override public void validate( FacesContext context, UIComponent component, Object value ) throws ValidatorException { /* * Récupération de l'attribut mot de passe parmi la liste des attributs * du composant confirmation */ UIInput composantMotDePasse = (UIInput) component.getAttributes().get( CHAMP_MOT_DE_PASSE ); /* * Récupération de la valeur du champ, c'est-à-dire le mot de passe * saisi */ String motDePasse = (String) composantMotDePasse.getValue(); /* Récupération de la valeur du champ confirmation */ String confirmation = (String) value; if ( confirmation != null && !confirmation.equals( motDePasse ) ) { /* * Envoi d'une exception contenant une erreur de validation JSF * initialisée avec le message destiné à l'utilisateur, si les mots * de passe sont différents */ throw new ValidatorException( new FacesMessage( FacesMessage.SEVERITY_ERROR, MOTS_DE_PASSE_DIFFERENTS, null ) ); } } } |
com.sdzee.validators.ConfirmationMotDePasseValidator
La seule petite difficulté ici est de penser à convertir l'objet récupéré depuis la liste des attributs du composant confirmation, à la ligne 23, en tant que UIInput
, afin de pouvoir récupérer ensuite simplement sa valeur avec la méthode getValue()
. Quant au reste de la méthode, c'est le même principe que dans le validateur de l'adresse email que nous avons codé un peu plus tôt.
Une fois ces modifications effectuées dans le code de votre projet, rendez-vous une ultime fois sur la page d'inscription depuis votre navigateur. Tentez alors de rentrer des mots de passe différents, égaux, de rentrer la confirmation avant le mot de passe, de rentrer des mots de passe égaux puis d'en modifier un des deux, etc. Vous observerez alors, si vous n'avez rien oublié, une actualisation en direct des messages d'erreur associés aux champs mot de passe et confirmation !
Nous en avons enfin terminé avec notre formulaire d'inscription ! Tout cela a dû vous paraître bien long, mais c'est avant tout parce que nous avons pris le temps de décortiquer toutes les nouveautés qui interviennent. Si vous prenez un peu de recul, et que vous regardez le code final de votre application, vous remarquerez alors qu'il est bien moins volumineux et bien plus organisé que lorsque nous travaillions sur MVC à la main. Mission réussie pour le tandem JSF + JPA : la gestion des formulaires est maintenant une partie de plaisir, et seules les vérifications métier nécessitent encore l'écriture de méthodes de validation !
- Grâce à notre travail effectué avec JPA, la couche de données est totalement indépendante du reste de l'application.
- Pour gérer correctement les champs de formulaire laissés vides par l'utilisateur, il est recommandé de configurer le paramètre
javax.faces.INTERPRET_EMPTY_STRING_SUBMITTED_VALUES_AS_NULL
dans le web.xml du projet. - Pour gérer un formulaire simple, il suffit d'une Facelet, d'un backing-bean et d'une entité.
- Le backing-bean contient une référence à l'entité, et une ou plusieurs méthodes d'actions appelées lors de la soumission du formulaire associé.
- La portée du backing-bean ne doit pas être définie à la légère, il est important d'utiliser la plus petite portée possible pour limiter le gâchis de ressources sur le serveur.
- La méthode
FacesContext.addMessage()
permet de définir un message géré par JSF, et de lui attribuer un niveau de gravité. - La Facelet contient des balises qui représentent les composants JSF, et qui sont liées aux propriétés de l'entité par l'intermédiaire du backing-bean, à travers des expressions EL.
- La validation du format des données saisies est entièrement prise en charge par JSF, et il est possible de l'effectuer :
- depuis la vue, via des attributs et balises dédiées.
- depuis le modèle, via des annotations sur les propriétés des entités.
- Les contrôles métier se font par le biais d'un objet
Validator
, dont la création est simple et guidée par le framework. - L'ajaxisation de la validation des champs d'un formulaire avec JSF est incroyablement simple.
- Avec le tandem JPA et JSF, nous pouvons construire un formulaire efficace sans SQL et sans JavaScript.