A propos des frameworks web et de JSF en particulier...
Une application web se caractérise par une interface graphique exposée à l'utilisateur basée sur des pages HTML enrichies avec l'utilisation de JavaScript :
- Le langage HTML5 permet la description de la page web affichée avec la combinaison de paragraphes de texte, de données audiovisuelles (images vectorielles ou bitmap, sons, animations vectorielles, vidéos) et de composants d'interaction (éléments de formulaire). La page est décrite sous la forme d'un arbre de composants à travers le modèle DOM (Document Object Model).
- Le langage CSS (Cascading Style Sheets) permet de décrire des feuilles de style qui sont appliquées pour modifier l'apparence de composants graphiques de la page selon différents contextes. On peut ainsi distinguer le contenu de la page (HTML5) de la manière dont celui-ci est rendu (feuille CSS).
- JavaScript est un langage de programmation permettant d'exécuter des actions lorsque des événements surviennent sur la page (fin de chargement d'un élément, clic sur un composant graphique, délai expiré...). Il permet de dynamiser une page en permettant notamment de modifier l'arbre DOM de la page ainsi que les styles appliqués. Il est possible également de communiquer à l'aide de JavaScript avec un serveur web pour mettre à jour le contenu de la page (approche AJAX).
Les langages HTML5/CSS/JavaScript sont suffisants pour réaliser des applications complètes autonomes ou interfacées avec des services web fournissant des données avec l'approche AJAX. Ainsi des frameworks favorisent l'écriture d'applications centrées principalement sur le client web (navigateur) :
- AngularJS est un framework permettant une programmation déclarative des interfaces (vue) avec injection automatique de contenu à des endroits indiquées par des balises insérées dans le code HTML5 : le contenu à injecter peut être récupéré sur des services web.
- Google Web Toolkit (GWT) permet de développer une application web entièrement en langage Java et la convertit en utilisant les langages HTML et JavaScript.
Le principal intérêt d'une approche centrée sur le client est qu'elle promeut l'écriture de services web qui peuvent être réutilisés dans d'autres contextes : par exemple utilisées par d'autres types d'interfaces graphiques (clients lourds n'utilisant pas une interface web tel qu'application native Android) ou par des scripts.
Un autre type d'approche consiste à laisser au serveur web la primauté de la gestion de l'application ; les tâches réalisées par le client au sein de l'interface web peuvent être vues comme une simple délégation de travail. Il existe des frameworks web orienté serveur pour la plupart des langages les plus populaires :
- Pour C++ : il existe peu de framworks web (en raison sans doute de la gestion manuelle de la mémoire et de la fiable dynamicité du langage qui rend l'écriture d'applications web plus fastidieuse), on peut citer par exemple Wt.
- Pour C# : ASP.NET est le framework web proposé par Microsoft
- Pour PHP : Symfony, CakePHP inspiré de RubyOnRails, Zend...
- Pour Python : Django, TurboGears, Zope...
- Pour Ruby : Ruby On Rails, Sinatra...
- Pour Java : nous nous intéresserons ici à Java Server Faces mais il existe de nombreux autres frameworks web (souvent plus anciens) proposant également une approche Modèle-Vue-Contrôleur tels que Struts, Spring MVC ou Tapestry
La plupart des frameworks web modernes tendent à promouvoir la séparation entre la description de la vue à afficher, le code métier et le code mettant en relation la partie métier avec la vue : il s'agit ainsi de mettre en oeuvre le paradigme Modèle-Vue-Contrôleur (MVC). Les frameworks web plus anciens étaient moins rigoureux quant à la séparation de ces différentes couches : il est ainsi possible en utilisant Java Server Pages (JSP) de mélanger dans une page à la fois du code HTML et du code Java exécuté côté serveur.
Le paradigme MVC peut être mis en oeuvre de différentes manières ; on distingue deux approches divergentes à ce sujet :
- L'approche push où le contrôleur (avec les actions qu'il réalise) occupe une place centrale. L'action peut consister à consulter, mettre à jour ou modifier des données. Cette approche est cohérente avec l'architecture REST où la ressource et l'action qu'on réalise dessus sont mis en avant. On réalise l'action demandée et on pousse le résultat vers la couche vue (qui peut consiste à injecter le résultat dans une page HTML). Cette approche est donc très liée au protocole HTTP avec son mode de fonctionnement requêtre-réponse et est plutôt appréciée par les développeurs ayant déjà une expérience de la programmation web. Sur les plate-formes Java, Spring MVC propose une telle approche (avec également Play et Struts). Django et Symfony sont également des framework populaires utilisant cette approche pour leur langage respectif (Python et PHP).
- L'approche pull où l'on met plutôt au centre la couche vue. C'est la vue qui va réclamer (et donc tirer) les informations depuis un contrôleur (lui-même relié à un modèle) ; la vue peut également appeler des actions sur le contrôleur suite à des événements rencontrés sur la page (clic sur un lien, appui sur un bouton...). Cette approche est très utilisée par les frameworks utilisés pour développer des applications graphiques natives. Cela permet donc d'adapter facilement une application native vers une version web (et vice-versa) mais avec une certaine perte de contact avec le protocole HTTP. Java Server Faces se classe dans cette catégorie de framework web avec approche pull (tout comme Tapestry ou Lift).
La plupart des frameworks web embrassent différents aspects de la réalisation d'applications web ; ils mettent généralement en place les mécanismes suivants :
- L'utilisation d'un ORM afin de manipuler le modèle sous la forme d'objets persistants sans avoir à interagir avec des instructions de bas-niveau avec le système de stockage (généralement une base de données relationnelle ou NoSQL)
- L'emploi d'un système de template. Un template peut ainsi décrire le squelette d'une page HTML avec des emplacements paramétrés en fonction de données du modèle.
- L'utilisation de composants graphiques (appelés widgets). Il s'agit de composants complexes réutilisables composés de plusieurs primitives HTML. On peut ainsi par exemple disposer de composants pour sélectionner une heure, sélectionner une date à partir d'un calendrier...
- L'utilisation d'un système de cache afin de conserver par exemple des portions de vue restant immuables pendant un certain temps.
- La mise en oeuvre d'un système de validation afin de vérifier la validité données pouvant être communiquées par l'utilisateur (à la fois côté client et serveur).
- Un système permettant l'utilisation de requête AJAX afin de mettre à jour dynamiquement la page sans son rechargement complet.
- Un système d'injection de dépendances.
- Un système de gestion de comptes utilisateurs et droits d'accès.
JSF repose principalement sur l'usage des API standard de Java EE à savoir JPA pour la persistance des objets (dont on peut utiliser l'implantation Hibernate par exemple). L'injection de dépendances est géré avec les Enterprise Java Beans et CDI ; le système de validation de beans peut être également utilisé pour valider les entrées utilisateur. JSF propose le système de templates Facelets.
Pour en savoir plus à propos de JSF :
FacesServlet
- Un seul composant se charge de la partie contrôleur de l'application : il s'agit de FacesServlet.
- Cette servlet réceptionne toutes les requêtes et les redirige vers les méthodes de traitement adaptées : FacesServlet abstrait donc le protocole HTTP pour le développeur.
- Il est toujours possible d'utiliser d'autres servlets sur le serveur web en plus de la servlet gérant JSF : il suffit de les ajouter dans le fichier web.xml
Voici un exemple de fichier web.xml pour mettre en place FacesServlet. Toutes les URIs de la forme jsf/*.xhtml pointeront vers FacesServlet :
<?xml version="1.0" encoding="UTF-8"?> <web-app> <context-param> <param-name>javax.faces.PROJECT_STAGE</param-name> <param-value>Development</param-value> </context-param> <servlet> <servlet-name>Faces Servlet</servlet-name> <servlet-class>javax.faces.webapp.FacesServlet</servlet-class> <load-on-startup>1</load-on-startup> <!-- we force the loading of the servlet at startup --> </servlet> <servlet-mapping> <servlet-name>Faces Servlet</servlet-name> <url-pattern>/jsf/*.xhtml</url-pattern> </servlet-mapping> </web-app>
Modèle
Le modèle de l'application est géré à l'aide d'un ou plusieurs beans. De tels beans peuvent eux-mêmes référencer des beans entités correspondant à des données persistantes.
Une classe beans utilisée par JSF est annotée avec 0ManagedBean. On peut également spécifier sa portée (cf portées des beans), la portée la plus pertinente étant généralement @ConversationScoped qui est à mi-chemin entre @RequestScoped (portée limitée à la requête courante) et @SessionScoped (portée limitée à la session courante). Le concept de conversation représente un morceau de session correspondant à une succession de requêtes correspondant à un dialogue avec le serveur. JSF maintient donc l'instance du bean durant toute cette conversation. JSF introduit donc une notion de bean avec conservation d'état par dessus le protocole HTTP qui est par nature sans état.
Un exemple typique de modèle pourrait être le contenu entré par l'utilisateur sur un formulaire web découpé en plusieurs pages. Cela pourrait être le cas par exemple pour un processus de finalisation d'achat sur un site e-commerce avec des pages pour valider le panier, choisir son mode de livraison, entrer son adresse de facturation, son adresse de livraison, choisir son mode de paiement et entrer ses coordonnées bancaires. Toutes ces informations peuvent être regroupées dans une bean Order.
Vue avec facelets
Historiquement, JSF proposait l'utilisation de pages JSP (Java Server Pages) comme mode de création de vue. Cette technologie a été abandonnée au profit des facelets qui permettent une meilleure séparation entre code de contrôle et présentation des données. Les facelets ont êté largement inspirées par les concepts introduits par Apache Tapestry.
La composition des templates
Facelets permet de définir des templates pour la vue qui peuvent être utilisés individuellement ou être composés ensemble, voire paramétrés. Nous pouvons ainsi factoriser des portions de vue similaires que l'on peut réutiliser dans des contextes différents.
Par exemple, la plupart des sites web comportent généralement un en-tête de page contenant le titre et le menu du site ainsi qu'un pied de page. Nous pouvons séparer ces différentes sections au sein de différents templates.
Considérons pour exemple le site web d'un zoo. Nous créons d'abord l'en-tête de page avec le menu dans un fichier templates/header.xhtml ; nous pouvons utiliser du code HTML normal en entourant par une balise <ui:insert name=".."> les emplacements à remplacer par du contenu spécifique :
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:ui="http://xmlns.jcp.org/jsf/facelets"> <body> <ui:composition> <div class="logo" style="font-size:34pt">JavaZoo</div> <div class="menu"> Zoo residents : <a href="/aslan.xhtml">Aslan the lion</a> | <a href="/dumbo.xhtml">Dumbo the elephant</a> | <a href="/kingkong.xhtml">King-Kong the gorilla</a> </div> <hr /> <h1> <ui:insert name="title">Default title (to be replaced)</ui:insert> </h1> </ui:composition> </body> </html>
On écrit maintenant le template templates/footer.xhtml pour le pied de page :
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:ui="http://xmlns.jcp.org/jsf/facelets"> <hr /> <div class="footer_links"> <a href="/contact.xhtml">Contact us</a> | <a href="/guestbook.xhtml">Sign the guestbook</a> </div> </ui:composition>
Nous crééons maintenant un template représentant une page stéréotypique concernant un animal résident du zoo dans templates/resident.xhtml :
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:f="http://java.sun.com/jsf/core" xmlns:h="http://java.sun.com/jsf/html"> <h:head> <title>#{animalName}</title> </h:head> <h:body> <ui:decorate template="/templates/header.xhtml"> <ui:define name="title">#{animalName} the #{animalSpecies}</ui:define> </ui:decorate> <p>Welcome on the webpage of #{animalName} !</p> <!-- Display a debug component on the page --> <ui:debug /> <ui:insert name="picture">Sorry but no picture of #{animalName} is available</ui:insert> <ui:insert name="presentation"> <p>Hello, my name is #{animalName} and I'm a #{animalSpecies}. I'm born in the year #{animalBirthYear}.</p> </ui:insert> <ui:include src="/templates/footer.xhtml" /> </h:body> </html>
On peut maintenant créer des pages spécifiques pour chaque animal du zoo, par exemple ici pour Aslan :
<ui:composition template="templates/resident.xhtml" xmlns:ui="http://xmlns.jcp.org/jsf/facelets" xmlns:h="http://java.sun.com/jsf/html"> <ui:param name="animalName" value="Aslan" /> <ui:param name="animalSpecies" value="Lion" /> <ui:param name="animalBirthYear" value="1980" /> <ui:define name="picture"> <h:graphicImage value="lion.jpg" library="images" /> </ui:define> </ui:composition>
En conclusion :
-
Lorsque l'on inclut un template dans un autre uniquement la zone entourée par des balises <ui:composition> est prise en compte. Cela permet de modifier facilement un template avec un éditeur HTML classique sans se préoccuper du code ajouté en dehors des balises composition.
- Si ce comportement n'est pas souhaité, on peut utiliser <ui:decorate template="/template.xhtml"> pour injecter le template plutôt que ui:composition
-
La composition peut être paramétrée :
-
Des paramètres peuvent être indiqués avec <ui:param name="..." value="..." /> dans <ui:include>
- Ces paramètres peuvent être accédés au niveau du template inclus en utilisant une expression EL : #{nameOfTheParameter}
- Il est possible également d'utiliser des beans comme valeur de paramétre
-
La balise <ui:define name="...">...</ui> permet de définir un contenu pour les zones substituables d'un template
- La balise <ui:insert name="...">...</ui> sert à indiquer l'emplacement de ces zones substituables dans le template utilisé (avec éventuellement un contenu par défaut)
-
Des paramètres peuvent être indiqués avec <ui:param name="..." value="..." /> dans <ui:include>
-
Il est possible de réaliser une inclusion simple d'un fichier en utilisant la balise <ui:include src="fileToInclude.xhtml />
- include ne supporte pas insert
Astuce :
- Si l'on ne souhaite ne rendre accessibles publiquement que certaines pages (les pages finales) et pas des fichiers template intermédiaires, on peut placer ceux-ci dans le répertoire WEB-INF dont le contenu n'est pas récupérable depuis le serveur web
La gestion des ressources et l'i18n
Ressources textes
- Les chaînes de caractères peuvent être externalisées dans un fichier mystrings.properties (mystrings à remplacer par le nom souhaité) présent dans la version compilée du site dans le répertoire WEB-INF/classes (on peut le placer à la racine de src lors du développement) :
# strings used by our website animalCompleteName = {0} the {1} zooResidents = Zoo residents lion = lion elephant = elephant gorilla = gorilla contactUs = Contact us signTheGuestbook = Sign the guestbook
-
Quelques informations sur le format du fichier .properties :
- Il contient des couples de clé/valeur (un couple par ligne, séparation de la clé et de la valeur par =)
- Les lignes de commentaires commencent par un caractère #
- Une valeur peut s'étaler sur plusieurs lignes : à la fin de chaque ligne non-terminale, on préfixe le retour à la ligne par un caractère \
- L'encodage latin1 (iso-8859-1) est utilisé ; cela impose de coder spécifiquement les caractères Unicode (on peut s'aider du programme native2ascii). Par exemple αλφάβητο (alphabet en grec) doit s'écrire \u03b1\u03bb\u03c6\u03ac\u03b2\u03b7\u03c4\u03bf
- Il est bien sûr possible (et conseillé) d'utiliser plusieurs fichiers .properties pour les différentes catégories de chaînes à gérer (on aurait pu ici créer un fichier spécialement pour la désignation des animaux par exemple)
- On déclare ensuite le fichier de chaînes en tant que resource-bundle dans le fichier faces-config.xml :
<faces-config ..> <application> <resource-bundle> <base-name>mystrings</base-name> <var>strings</var> </resource-bundle> </faces-config>
-
Il est ensuite possible d'accéder aux chaînes en utilisant le nom de variable spécifié avec var (ici strings) dans les templates :
- Directement par une expression EL avec #{varName.stringName}
- Ou en utilisant la balise <h:outputFormat> avec des sous-balises <f:param> pour les chaînes paramétrées
- Voici un exemple pour l'en-tête de page précédemment traité :
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:ui="http://xmlns.jcp.org/jsf/facelets"> <body> <ui:composition> <div class="logo" style="font-size:34pt">JavaZoo</div> <div class="menu"> #{strings.zoo_residents} : <a href="/aslan.xhtml"> <h:outputFormat value="#{strings.animalCompleteName}"> <f:param value="Aslan"/> <f:param value="#{strings.lion}"/> </h:outputFormat> </a> </div> <hr /> <h1> <ui:insert name="title">Default title (to be replaced)</ui:insert> </h1> </ui:composition> </body> </html>
-
L'internationalisation (i18n) est facilitée avec les resource-bundles :
-
Pour chaque langue supportée, on peut créer un fichier X_lg.properties adapté à la langue lg
- lg est le code ISO 639-1 à 2 lettres de la langue (e.g. : en pour l'anglais, fr pour le français, es pour l'espagnol...)
- Le code langue peut être suivi par un code pays à 2 lettres en MAJUSCULES (ce qui permettrait par exemple de proposer des chaînes en anglais britannique avec X_en_UK.properties et en anglais américain avec X_en_US.properties)
- On peut demander l'internationalisation des chaînes dans un template en entourant le code par la balise suivante (celle-ci utilise la locale préférée du navigateur qui est émise dans l'en-tête HTTP Accept-Language) :
-
Pour chaque langue supportée, on peut créer un fichier X_lg.properties adapté à la langue lg
<f:view locale="#{facesContext.externalContext.requestLocale}"> ... </f:view>
Internationalisation de pages
- Il n'existe pas de mécanisme intégré pour une internationalisation des templates
-
Il est possible d'utiliser un listener preRenderView comme suggéré ici
- Nous écrivons un managed bean avec une méthode chargée de déterminer le template à charger selon la ressource
- Le template principal de la page utilise un preRenderView sur la méthode écrite dans le managed bean
Voici le code du managed bean :
@ManagedBean(name="internationalizedPageLoader") @RequestScoped public class InternationalizedPageLoader { public String getResource(String path, Locale l) throws IOException { ExternalContext context = FacesContext.getCurrentInstance().getExternalContext(); for (String suffix: new String[]{l.getLanguage() + "_" + l.getCountry(), l.getLanguage()}) { String newPath = path.replace(".xhtml", String.format(".%s.xhtml", suffix)); URL url = context.getResource(newPath); if (url != null) return newPath; } return null; } public void loadPage() throws IOException { FacesContext context = FacesContext.getCurrentInstance(); ExternalContext external = context.getExternalContext(); Iterator<Locale> it = context.getExternalContext().getRequestLocales(); String path = context.getViewRoot().getViewId(); while (it.hasNext()) { String newPath = getResource(path, it.next()); if (newPath != null) { external.redirect(external.getRequestContextPath() + newPath); return; } } String newPath = getResource(path, Locale.ENGLISH); if (newPath != null) external.redirect(external.getRequestContextPath() + newPath); else external.setResponseStatus(HttpServletResponse.SC_NOT_FOUND); } }
Et le code du template principal :
<f:metadata xmlns:f="http://xmlns.jcp.org/jsf/core"> <f:event type="preRenderView" listener="#{pageLoader.loadPage}"/> </f:metadata>
Il ne reste plus qu'à écrire des templates spécifiques pour les différentes langues (template.en.xhtml, template.fr.xhtml).
ResourceHandler
- Un ResourceHandler permet d'obtenir une ressource à partir de la donnée de son identifiant (qui est peut être composé d'une library et d'un name) : cette ressource est un flot d'octets qui sera renvoyée au client. Le chemin d'une ressource (URI) est d'abord fourni dans la page HTML rendue puis le navigateur réalise une requête ultérieure pour la récupérer.
- On peut créer son propre ResourceHandler qui peut même générer du contenu dynamique (on pourrait par exemple produire dynamiquement de nouvelles images captcha, récupérer des images d'une base de données...) en implantant une classe dérivée de ResourceHandlerWrapper et en la déclarant ainsi dans le fichier faces-config.xml :
<application> <resource-handler>fr.upem.MyResourceHandler</resource-handler> </application>
L'utilisation d'un ResourceHandler une gestion automatisée et centralisée des ressources multimédia telles que les images, les scripts JavaScript ainsi que les feuilles de style. Nous pouvons à cet effet utiliser les balises suivantes :
-
Des ressources de différentes natures peuvent être incluses avec les balises suivantes :
- <h:outputStylesheet library="mytheme" name="css/style.css" />
- <h:outputScript library="mytheme" name="js/script.js" />
- <h:graphicImage library="mytheme" name="img/logo.png" />
-
La bibliothèque de tags h étant incomplète, il peut être utile d'utiliser des balises HTML en imprimant soi-même l'URL de la ressource ; voici un exemple avec une balise video avec l'utilisation d'une expression EL :
- <video src="#{resource['mytheme:video/presentation.mkv']}"}
- L'usage de ces balises implique d'utiliser la hierarchie de répertoires suivantes pour placer les ressources si l'on utilise le ResourceHandler par défaut :
WebContent/ resources/ mytheme/ css/ style.css js/ script.js img/ logo.png
- Il est possible de versionner les ressources (en intercalant par exemple des sous-répertoires 1_0, 1_1... sous mytheme) ; si une ressource existe sous plusieurs versions, la version la plus élevée est choisie par défaut.
- Il est possible d'utiliser plusieurs libraries pour les ressources (ici nous n'en avons qu'une seule : mytheme)
Formulaire et beans
Les facelets peuvent faire appel à des managed beans afin de s'interfacer avec le code métier de l'application. On peut ainsi communiquer bidirectionnellement entre la vue (la page HTML) et le managed bean hébergé sur le serveur :
- en injectant le contenu d'attributs du bean dans la page (avec une expression #{nomDuBean.nomAttribut})
- et également en récupérant le contenu de formulaires HTML pour mettre à jour des attributs de bean
Quelques conseils pour la création de managed beans :
-
Annoter le bean avec l'annotation @ManagedBean
- une propriété name peut être ajoutée pour donner un nom au bean différent du nom de la classe
- la propriété eager=true indique que le bean n'est pas chargé de manière paresseuse
- Utiliser un constructeur sans argument (ou laisser le constructeur par défaut)
- Employer des champs privés et des getters/setters correspondants
- JSF instantie automatiquement les managed beans de façon paresseuse (sauf en mode eager)
-
Le contexte de vie d'une instance de managed bean dépend de sa portée (un peu à la manière d'un bean CDI) :
- @NoneScoped : le bean vit le temps de l'évaluation d'une expression (c'est la portée la plus faible)
- @ViewScoped : le bean est instantié pour une vue (page HTML ou portion de page)
- @RequestScoped : le bean vit le temps de la requête
- @SessionScoped : le bean vit durant la session (groupe de requêtes rapprochées dans le temps du même utilisateur)
- @CustomScoped("#{mymap}") : le bean a la même durée de vie que la map référencée comme expression EL en paramètre
- @ApplicationScoped : le bean est global à l'application (attention aux problématiques d'accès concurrent)
-
Les managed beans JSF font double-emploi généralement avec les beans CDI qui offrent des fonctionnalités plus avancées
- Les beans CDI permettent par exemple d'injecter un bean de plus faible portée dans un bean de plus forte portée (en utilisant des proxys)
- On préferera donc utiliser plutôt des beans CDI (voire des EJB) sauf lorsque ce n'est pas possible : par exemple si l'on utilise un serveur n'implantant pas toute l'API Java EE mais uniquement la partie web (comme Tomcat)
À titre d'exemple, implantons un livre d'or qui gère une liste de messages (chaînes de caractères et auteur) envoyés par les visiteurs d'un site.
Voici le template (vue) utilisé :
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:f="http://java.sun.com/jsf/core" xmlns:h="http://java.sun.com/jsf/html"> <h:head> <title>Guestbook</title> </h:head> <h:body> <ui:decorate template="/WEB-INF/templates/header.xhtml"> <ui:define name="title">Post a message on the guest book</ui:define> </ui:decorate> <h:form> <h:messages styleClass="messageCssStyle" globalOnly="true" showDetail="true" /> <h:panelGrid columns="3"> <h:outputLabel for="name" value="Name" /> <h:inputText id="name" value="#{guestbookBean.message.name}" /> <h:message for="name" /> <h:outputLabel for="email" value="Email" /> <h:inputText id="email" value="#{guestbookBean.message.email}" /> <h:message for="email" /> <h:outputLabel for="body" value="Body" /> <h:inputTextarea id="body" value="#{guestbookBean.message.body}" cols="80" rows="10" /> <h:message for="body" /> </h:panelGrid> <h:commandButton action="#{guestbookBean.submit}" value="Submit" /> </h:form> <ui:include src="/WEB-INF/templates/footer.xhtml" /> </h:body> </html>
Ainsi que le bean CDI gérant le livre d'or :
@RequestScoped @Named("guestbookBean") public class GuestbookBean { @ConversationScoped public static class Message implements Serializable { private static final long serialVersionUID = 1L; private String name; private String email; private String body; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getBody() { return body; } public void setBody(String body) { this.body = body; } } @ApplicationScoped public static class Guestbook { private List<Message> messages = new ArrayList<>(); public List<Message> getMessageList() { return messages; } } @Inject private Message message; @Inject private Guestbook guestbook; public Message getMessage() { return message; } public Guestbook getGuestbook() { return guestbook; } public String submit() { guestbook.getMessageList().add(message); FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO, "Message added", "Your message was successfully added in the guestbook, the guestbook contains now " + getGuestbook().getMessageList().size() + " messages")); return null; } }
Une architecture plus modulaire utiliserait des beans EJB et CDI :
- Un EJB entité pourrait représenter un message de livre d'or
- Un EJB session Stateless pourrait être chargé de gérer les données du livre d'or en proposant des méthodes pour insérer une entrée ou pour consulter les entrées existantes. Cet EJB est un Data Object Manager (DAO) reposant sur l'utilisation de l'API JPA.
- Un bean CDI pourrait être utilisé pour gérer au niveau de la vue l'ajout ou la récupération de messages
À titre d'exercice, on pourra essayer d'implanter un livre d'or persistant stockant les messages dans une base de données plutôt que la mémoire (en utilisant l'API JPA) en implantant les beans précédemment décrits.
Validation des données
La validation des données communiquées dans un formulaire peut être réalisée à deux niveaux différents :
- au niveau de la vue en utilisant les mécanismes de validation proposés par JSF
- au niveau des beans entités manipulés
Dans la mesure du possible, on essaiera de réaliser la validation au plus bas niveau possible, i.e. des beans entités car le mécanisme mis en oeuvre sera réutilisable potentiellement avec des technologies autres que JSF s'occupant de la couche vue.
Validation des données de la vue par JSF
Les templates de déclaration de vue peuvent intégrer des balises spécifiant des contraintes de valeur pour les champs (comme sous-balises des champs). L'attribut for est commun à toutes ses balises et permet d'indiquer l'identifiant du composant concerné par la contrainte de validation.
Voici quelques balises de contraintes de validation :
Nom de la balise | Propriétés spécifiques |
<f:validate{Double,Long}Range /> | minimum="minValue", maximum="maxValue" |
<f:validateLength /> | minimum="minLength", maximum="maxLength" |
<f:validateBean /> | |
<f:validator /> | binding="validator" |
<f:validateRegex /> | pattern="regularExpression" |
- La contrainte validateRegex permet d'indiquer une expression régulière pour vérifier le contenu d'un champ (cela utilise la classe Pattern pour compiler l'expression régulière)
- La contrainte validateBean permet de déléguer la validation au bean entité utilisé plutôt que d'utiliser un mécanisme propre à JSF (ce que nous verrons dans la section suivante).
- La contrainte validator permet d'indiquer dans l'attribut bindingle nom d'un bean de validation JSF : un tel bean est implanté en créeant une nouvelle classe héritant de javax.faces.validator.Validator
Exemple d'utilisation de validateRegex
On peut utiliser validateRegex pour vérifier une adresse email :
<h:inputText id="email" value="#{message.email}" required="true" requiredMessage="You must enter an email address" validatorMessage="The entered email address is not acceptable (invalid or too long)"> <f:validateRegex pattern="^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,63}$" /> <f:validateLength maximum="128" /> </h:inputText>
Nous avons également ajouté un test sur la longueur de l'adresse afin qu'elle ne dépasse pas 128 caractères.
Plutôt que d'utiliser une chaîne de caractères en dur, il est recommandé d'utiliser une chaîne provenant d'un ResourceBundle pour pouvoir internationaliser les messages.
Exemple d'utilisation de validator
Il est possible d'implanter un validateur JSF personnalisé : cela peut être utile par exemple afin de tester si deux champs ont les mêmes valeurs. On peut ainsi demander à l'utilisateur de fournir deux fois le contenu d'un champs afin de vérifier qu'il n'y a pas d'erreur de saisie (comme une adresse email ou un mot de passe).
Voici un extrait de template avec deux champs pour l'adresse email :
Email address: <h:inputText id="email" ...> .... </h:inputText> Email address confirmation: <h:inputText id="email2" required="true" requiredMessage="You must confirm the email address" validatorMessage="Confirmed email address not the same than the initial address"> <f:validator binding="duplicateFieldValidator" /> <f:attribute name="referenceField" value="#{email}" /> </h:inputText>
Nous devons maintenant implanter un bean chargé de réaliser la validation :
@FacesValidator( value = "duplicateFieldValidator" ) public class DuplicateFieldValidator implements Validator { @Override public void validate( FacesContext context, UIComponent component, Object value ) throws ValidatorException { // we retrieve the content of the confirmation field UIInput referenceComponent = (UIInput) component.getAttributes().get("referenceField"); String referenceInput = referenceComponent.getSubmittedValue(); // and we test the equality between the reference field and the confirmation field inputs if (! referenceInput.equals(value.toString()) throw new ValidatorException(new FacesMessage("Inconsistency between the content of the two fields")); } }
Validation par un bean
- La contrainte validateBean permet de déléguer la validation au bean entité utilisé.
- Pour plus d'informations concernant la validation par un bean entité, on pourra se reporter au cours sur JPA
Truc : si l'on souhaite que les chaînes de caractères vides de formulaire soient assimilées à des chaînes null, on peut ajouter cette option dans le fichier web.xml :
<context-param> <param-name> javax.faces.INTERPRET_EMPTY_STRING_SUBMITTED_VALUES_AS_NULL </param-name> <param-value>true</param-value> </context-param>
Conversion
Les valeurs des champs étant envoyées sous la forme de chaînes de caractères, quelquefois une conversion du champ peut être nécessaire dans le type déclaré (int, long, float, double, boolean...). On peut ajouter au niveau du champ de formulaire un attribut converterMessage qui est utilisé pour afficher un message d'erreur si la conversion n'est pas possible. Pour les types courants, le client pourra également réaliser la vérification de son côté (par exemple pour n'autoriser que l'entrée de chiffres pour un champ représentant un entier).
Retour de messages
Si la validation de champs a échoué il est nécessaire d'en informer l'utilisateur. Il existe deux façons de réaliser la validation pour informer l'utilisateur : au niveau du client ou du serveur.
Validation au niveau du client
La validation côté-client n'est pas proposée en standard par JSF. Certains implantations proposent cependant en complément ce mécanisme. Il s'agit d'exécuter un script JavaScript chargé de vérifier le contenu de champs de formulaire (à chaque caractère entré ou en changement de champ). L'utilisateur est informé immédiatement d'un éventuel problème de saisie : cela est plus rapide et limite les communications avec le serveur. La validation côté client doit toujours être utilisée en complément de la validation côté-serveur et jamais en substitution : le navigateur peut ne pas exécuter le code JavaScript pour différentes raisons (vieux navigateur, JavaScript désactivé, soumission de formulaire par un script...). On notera que la validation côté-client ne fonctionne que pour les contraintes prédéfinies (étendue de valeur, respect d'une expression régulière...) ; les contraintes plus complexes exécutant du code Java arbitraire ou consultant des données du serveur ne peuvent être mises en oeuvre ; le développeur peut toutefois indiquer lui-même du code JavaScript réalisant la validation.
Par exemple l'implantation RichFaces de JBoss propose une balise <rich:validator /> à incorporer comme sous-balise : pour plus d'informations on pourra se référer à cette page. Un attribut event peut être utilisé pour cette balise pour indiquer si l'on souhaite déclencher la validation au changement de champ (change) ou à chaque caractère entré (keyup).
Validation au niveau du serveur
Par défaut la validation a toujours lieu côté serveur. Cela nécessite l'envoi complet du formulaire. Il faut intégrer sur la page un affichage de messages d'erreurs globaux pour la validation ainsi que pour chaque champ individuel. Ainsi l'utilisateur peut être informé des erreurs de saisie.
Il est également possible de réaliser des validations champ par champ côté serveur en utilisant un appel AJAX : le seul contenu du champ est envoyé au serveur et celui-ci répond en indiquant les erreurs rencontrées ou non. Ce mécanisme est mis en oeuvre automatiquement. Voici un exemple :
<h:inputText id="email" value="#{message.email}" required="true" requiredMessage="{messages.emailAddressRequired}" validatorMessage="{messages.emailAddressInvalid}"> <f:validateRegex pattern="^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,63}$" /> <f:validateLength maximum="128" /> <f:ajax event="blur" render="sessionidMessage" /> </h:inputText> <h:message id="emailMessage" for="email" />
Il ne faut pas oublier de spécifier une balise message afin d'indiquer l'emplacement où doit être affiché le message d'erreur. Si le navigateur ne réalise pas d'appel AJAX, la validation sera de toute façon réalisée globalement lors de la soumission du formulaire.
Navigation
Utilisation de liens hypertextes
L'impression d'un lien hypertexte est possible avec la balise h:outputLink, l'URL du lien étant indiquée avec l'attribut value. Il est possible de rajouter des paramètres au lien qui sont ajoutés et encodés correctement dans la section query de l'URL. Voici un exemple pour afficher un lien recherchant le contenu de l'attribut keywords de mybean sur Google :
<h:outputLink id="googleLink" value="http://www.google.com/"> <f:param name="q" value="#{mybean.keywords}" /> <h:outputText value="Search keyword" /> </h:outputLink>
h:outputLink est particulièrement adapté pour des liens sur des sites externes. Pour des liens internes, déterminer la valeur du lien à indiquer peut-être problématique car on ne connaît pas nécessairement le contexte d'installation de l'application web (avec notamment le mapping entre nom de template et URL). On utilisera donc plutôt dans ces conditions la balise h:link qui prend un attribut outcome correspondant au nom du template à afficher.
<h:link id="contactLink" outcome="contact" value="Contact us" />
L'exemple précédent affiche un lien affichant le texte "Contact us" (qu'on pourra remplacer par une expression EL pour i18n) nous permettant de nous rendre sur le template contact.
Notons que les liens hypertextes impliquent obligatoirement l'envoi d'une requête HTTP avec une méthode GET : ils sont donc adaptés pour la navigation de page en page mais pas pour envoyer des données (on utilisera plutôt à cet effet des formulaires).
Il est possible de communiquer des arguments à un template de notre site depuis un lien hypertexte réalisant une requête GET. En reprenant l'exemple précédent d'un lien de contact, nous pouvons indiquer le service que l'on souhaite contacter (avec son adresse email, quoique cela soit une mauvaise idée pour limiter le spam) :
<h:link id="contactLink" outcome="contact" value="Contact the webmaster"> <f:param name="email" value="postmaster@example.com /> </h:link> <h:link id="contactLink" outcome="contact" value="Contact the CEO""> <f:param name="email" value="ceo@example.com /> </h:link>
On peut ensuite indiquer dans une section <f:metadata> les paramètres que l'on souhaite récupérer (ici l'adresse email de contact) :
<f:metadata> <f:viewParam name="email" value="#{contactBean.emailAddress}" /> </f:metadata>
Cela a pour conséquence d'appeler la méthode setEmailAddress() de contactBean en lui passant en argument le paramètre email AVANT le rendu de la vue. Ainsi dans la vue, nous pourrons utiliser l'expression EL #{contactBean.emailAddress} pour récupérer cette adresse, en la plaçant par exemple dans un champ caché de formulaire de contact :
<h:form> <h:inputHidden id="email" value="#{contactBean.email}" /> <h:inputTextarea id="content" value="#{contactBean.content}" /> <h:commandButton value="Submit" action="#{contactBean.send}" /> </h:form>
Envoi de formulaires
L'envoi d'un formulaire doit normalement se réaliser par le clic sur un bouton. On utilise la balise h:commandButton à cet effet comme pour l'exemple suivant :
<h:form> <h:inputText id="input" value="#{myBean.input}" /> <h:commandButton value="Submit" action="#{indexBean.submit}" /> </h:form>
Il est possible de créer un bouton affichant une image avec l'attribut image de h:commandButton. L'attribut action du bouton spécifie par une expression EL la méthode à appeler lors de la soumission du formulaire. Cette méthode n'est appelée que si la validation de tous les champs du formulaire réussit : dans le cas contraire, la vue courante est réaffichée avec d'éventuels messages d'erreur d'accompagnement.
La méthode appelée par l'attribut action ne doit pas prendre d'argument et retourner un String indiquant le nom du prochain template à afficher. La chaîne peut également être null : dans ce cas, le template courant est rechargé.
L'envoi d'un formulaire peut également être réalisé avec un h:commandLink mais ceci est déconseillé car cela implique l'utilisation de code JavaScript.
La soumission d'un formulaire en utilisant une requête AJAX est possible : cela permet de conserver la vue courante en mettant à jour uniquement certaines sections de celle-ci.
Utilisation des requêtes/réponses
- Normalement, on ne devrait pas avoir besoin d'accèder à la requête ou réponse HTTP directement : JSF est un framework MVC de plus haut niveau qui permet de s'abstraire du protocole HTTP
- Si toutefois on souhaite obtenir la requête ou la réponse, cela demeure possible (notamment si l'on souhaite envoyer un code de statut HTTP spécifique ou manipuler des en-têtes) en passant par l'ExternalContext :
ExternalContext context = FacesContext.getCurrentInstance().getExternalContext(); HttpServletRequest request = (HttpServletRequest)context.getRequest(); HttpServletResponse response = (HttpServletResponse)context.getResponse();