Cet article est inspiré de ma lecture du livre de référence Java Server Faces 2.0 (de CompuPress) à la découverte du fonctionnement des JSF. Chapitre par chapitre, je vais faire un petit résumé.
Introduction
JSF est issu de la JSR 127 qui date de 2001 ; rédigée par les grands pontes habituels (Oracle, IBM, …). La version 1 est sortie en 2004 dans JEE 1.4.
Historique :
JSP → Jakarta Struts Web application (1er MVC) → Spring / Spring MVC (Inversion of Control ; POJO) → JSF
JSF fournit un ensemble de composants de base : des tags HTML et des tags « Core » JSF.
- HTML : Rendu HTML final. Par exemple :
<h:form> ; <h:inputText> ; <h:commandButton>
- Core : Composants de validation, de conversion, d’accès aux ressources.
JSF permet le templating, Ajax, créer des composants composites (association de composants), Bean Validation (JSR 303), l’accès aux ressources (managed beans). Pour migrer des JSP vers les JSF facilement, voir la procédure décrite sur jsfcompref.com.
Architecture
web.xml
: Ce fichier est facultatif via les derniers applications servers tels que Glassfish (JEE 6) et les annotations. Il permet d’associer le contrôleur « Faces » aux pages correspondantes selon un pattern paramétré. Par exemple :/faces/*
. Si ce fichier n’est pas présent, les pattern par défaut sont :/faces/* ; *.jsf ; *.faces
.faces-config.xml
: Optionnel également à l’aide des annotations fournies par JEE 6, ce fichier est situé dans le dossier WEB-INF. Il contient les règles de routage des jsf.WEB-INF/lib
: librairies tierces. Utile notamment si le conteneur d’application ne supporte pas nativement les JSF.
====== Data Layer xxxDataAccess/x/x/x/data => DAO xxxDataAccess/x/x/x/data/impl => DAO xxxDataAccess/x/x/x/model => Entities xxxDataAccess/x/x/x/service => Services xxxDataAccess/x/x/x/service/impl => Services ====== Data(tests) Layer xxxDataAccessTest/x/x/x/service/test => Unit Tests xxxDataAccesTest/lib => Test Libs ====== Beans Layer xxxWeb/x/x/x/web/controller => ManagedBeans. Uses Services xxxWeb/x/x/x/web/messages => i18n, ... xxxWeb/x/x/x/web/model => ManagedBeans, Entity beans xxxWeb/x/x/x/web/phase => Listeners ====== Web services Layer xxxWebServices/x/x/x/web/service => (rest/*) Uses Services ====== View (website) Layer xxxFacelets/x/x/x/webapp => JSF xxxFacelets/x/x/x/resources => JSF xxxFacelets/x/x/x/templates => JSF xxxFacelets/x/x/x/contracts => JSF xxxFacelets/x/x/x/*.html => JSF
Toute page JSF est au format XHTML et doit contenir une en-tête (namespace) qui permet d’utiliser les tags correspondants. :
<!-- A partir de JSF 2.2 le domaine java.sun.com devient xmlns.jcp.org --> <html xmlns="http://www.w3.org/1999/xhtml xmlns:h=http://java.sun.com/jsf/html xmlns:ui=http://java.sun.com/jsf/facelets" xmlns:f=http://java.sun.com/jsf/core" xmlns:cc=http://java.sun.com/jsf/composite" xmlns:p=http://java.sun.com/jsf/passthrough" xmlns:jsf=http://java.sun.com/jsf">
JSF Request processing lifecycle (Cycle de vie des pages JSF)
- Le client envoie une demande au server (submit)
- Côté Serveur :
- Créer / Restaurer la vue
- Appliquer les valeurs reçues
- Validation
- Mise à jour du modèle
- Mise à jour de la vue
- Rendu de la vue renvoyée au client
- Côté client : Réception et visualisation de la page générée.
Le modèle JSF est basé sur le pattern MVC
- Le modèle contient les POJO et les managed beans (EJB)
- La vue est constituée de XHTML JSF
- Le contrôleur « Faces Servlet » répond aux URL dont le pattern est défini dans le fichier
web.xml
. Le modèle de navigation est paramétré dans fichierfaces-config.xml
Le modèle de navigation se paramètre dans le fichier faces-config.xml
. Il contient les informations de routage des pages JSF vers les pages de réponse :
from-view-id
: Page à l’origine de la demande.from-outcome
: résultat de la validation du traitement de la requête (retour de la méthode). Le fichier faces-config peut paramétrer « success » ou » « failure » dans ce paramètre. Il est lié au paramètre suivantto-view-id
.to-view-id
: Page à renvoyer au client.
Première application : un formulaire d’enregistrement
Structure d’une petite application basique d’enregistrement d’un nouvel utilisateur, appelée jsfreg :
Le fichier jsfreg/WEB-INF/web.xml
Contient la définition des applications Faces. Fichier optionnel dans Glassfish, grâce notamment aux annotations. Contenu :
<web-appp version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"> <display-name>JSFReg</display-name> <description>Application exemple pour la saisie de formulaire</description> <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> </servlet> <servlet-mapping> <servlet-name>Faces Servlet</servlet-name> <url-pattern>/faces/*</url-pattern> </servlet-mapping> <welcome-file-list> <welcome-file>faces/register.xhtml</welcome-file> </welcome-file-list> </web-app>
Faces Servlet
PROJET_STAGE
welcome-file-list
Charge le controleur JSF. Le servlet-mapping correspondant définit sa portée.
Définit le niveau de log. Plusieurs valeurs sont possibles : "Development", "Production", "SystemTest", "UnitTest"
.
Page d’accueil par défaut quand le contexte root est demandé.
Le fichier faces-config
Il n’est plus nécessaire depuis JSF 2.0 grâce aux annotations.
Le fichier jsfreg/register.xhtml
Correspond au formulaire d’enregistrement. Structure :
<!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:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core"> <h:head>....</h:head> <h:body> <h:form> ... </h:form> </h:body> </html>
Dans les fichiers JSF, utiliser dans la mesure du possible les tags JSF plutôt que les tags HTML. Par exemple : <h:body> ; <h:head> ; ...
- Un champ de saisie
<h:inputText label="First Name" id="fname" value="#{userBean.firstName}" required="true"/> <h:message for="fname"/>
Les messages d’erreurs lors de la validation seront insérés à l’endroit du tag message
. La valeur par défaut du champs est la valeur du bean dont on a la référence via l’inversion de controle (IoC). Enfin la saisie du champ est marquée comme obligatoire.
<h:selectOneRadio label="Sex" id="sex" value="#{userBean.sex}" required="true"/> <f:selectItem itemLabel="Male" itemValue="male"/> <f:selectItem itemLabel="Female" itemValue="female"/> </h:selectOneRadio> <h:message for="sex"/>
<h:inputText label="Date Of Birth" id="dob" value="#{userBean.dob}" required="true"> <f:convertDateTime pattern="MM-dd-yy" /> </h:inputText> (mm-dd-yy) <h:message for="dob"/>
<h:inputText label="Email" id="email" value="#{userBean.email}" required="true" validator="#{userBean.validateEmail}"> <h:message for="email">
<h:selectOneMenu label="Service Level" value="#{userBean.serviceLevel}"> <h:selectItem itemLabel="Medium" itemValue="medium"/> <h:selectItem itemLabel="High" itemValue="high"/> </h:selectOneMenu>
<h:commandButton value="Register" action="confirm" />
Appelle la page du même nom, après validation des données de la page courante.
<h:messages/>
Le fichier jsfreg/confirm.xhtml
Reprend les données saisies dans le formulaire et propose de les modifier ou de les confirmer.
- Appel d’un attribut du bean :
<h:outputText value="#{userBean.firstName}">
<h:commandButton value="Confirm" action="#{userBean.addConfirmedUser}" />
Appelle la méthode du bean, qui va renvoyer à son tour l’action correspondant au résultat de la méthode pour renvoyer vers la page correspondante.
Le fichier jsfreg/done.xhtml
Affiche les données saisies dans le formulaire avec un message de succès.
Le fichier com/jsfreg/model/UserBean.java
- Annotation
@ManagedBean
- Annotation
@SessionScoped
- Méthodes POJO
- Méthode de validation de l’email :
Bean managé (Spring Bean, EJB, POJO, …) contenant les attributs de saisie du formulaire, les méthodes de validation des champs de saisie, les méthode d’action.
Similaire à l’instruction jsp <jsp:useBean>
avec les choix possibles ‘request’, ‘view’, ‘session’, ‘application’, ‘none’.
getFirstName / setFirstName ; getSex / setSex /
…
public void validateEmail(FacesContext ctx, UIComponent comp, Object value) throws FacesException { String addr = (String)value; if (-1 == addr.indexOf('@')) { FacesMessage msg = new FacesMessage("Adresse mail non valide"); throw new FacesException(msg); } }
public void addConfirmedUser() { boolean added = doAddUser(); FacesMessage msg = null; String outcome = null; if (added) { msg = new FacesMessage("L'utilisateur a bien été enregistré"); outcome= "done"; } else { msg = new FacesMessage("Echec lors de la création de l'utilisateur"); outcome = "register"; } FacesContext.getCurrentInstance().addMessage(null, msg); return outcome; }
outcome
Correspond à la cible : le nom de la vue à appeler. Les méthodes d’action et de validation sont appelées par la vue (register.xhtml ; confirm.xhtml).
Construction et déploiement de l’application web
Les dépendances nécessaires sont : jsf-api 2.0 ; jsf-impl 2.0.0-SNAPSHOT
. Ces dépendances sont intégrées des les serveurs d’applications tels que Glassfish. Pour Tomcat, il suffit de copier ces JAR soit dans l’application, dossier WEB-INF/lib
, soit dans le conteneur d’applications, dans le dossier lib
global.
Construire le war (ant, maven, …)
Le fait de construire un war « exploded » permet d’accéder aux pages xhtml depuis le serveur et de les modifier à la volée.
Accéder à son application
http://localhost:8080/jsfreg
Le cycle de vie des JSF
Le traitement d’une requête se fait en plusieurs étapes
- Création / Restauration de la vue :
- On crée ou récupère l’arborescence objet de la vue client. Une requête post-back (envoi POST du client) génère la restauration de la vue préalablement générée par le serveur lors de la requête précédente, et une requête non post-back (accès direct à une URL) provoque la construction d’une nouvelle vue (arborescence objet des composants JSF).
- Appliquer les valeurs de la requête.
- Requêtes de type « page » : chargement du facelet.
- Requêtes de type « ressource » : le flux d’octets est envoyé à la ressource utilisateur ciblée.
- Cette phase consiste à lire le contenu de la requête et à attribuer les valeurs aux composants IHM correspondants.
- Il existe deux types de composants. Ceux qui gèrent les valeurs implémentent
ValueHolder
(EditableValueHolder
pour les composants éditables) ; ceux qui gèrent les actions implémententActionSource
.
- Validation et conversion des données :
- Gestion des messages d’erreurs.
- Conversion et validation des valeurs.
- En cas d’erreur, on saute à la phase « Construction de la réponse cliente »
- Mise à jour des beans
- JSF permet le rattachement (le bind) des données de la vue aux beans : le « state management ».
- Appel des setters des beans managés.
- Mise à jour du modèle métier
- Appel des méthodes d’action définies dans les composants d’action (boutons).
- Construction de la réponse cliente
- Sauvegarde de la nouvelle arborescence objet en vue de la prochaine requête cliente.
- Renvoi du résultat généré au client (html, xml, …)
- Si la page ciblée n’existe pas (outcome inexistant) la même vue est renvoyée.
L’attribut immediate
- Permet d’appeler une page du serveur sans post-back (donc une nouvelle vue est générée côté serveur sans conservation de la session)
- Tags ne générant pas de réponse post-back :
<h:link outcome="register"> ; <h:button outcome="register">
- Tags générant des réponses post-back :
<h:commandLink outcome="register"> ; <h:commandButton outcome="register">
- Pour empêcher le post-back, l’attribut
immediate="true"
permet cela. Les étapes de validation du formulaire sont alors ignorées (il n’a pas a être validé pour être annulé). - L’utilisation de
immediate
sur des champs de saisie permet de procéder à la validation de ceux-ci (et à la mise à jour de l’IHM) sans avoir à soumettre le formulaire en entier. L’étape de validation est exécutée à la phase « Appliquer les valeurs de la requête ».
Les Phase Listener
Ils permettent d’intercepter toute phase exécutée lors du cycle de vie de la requête. Par exemple pour personnaliser des messages d’erreurs, vérifier l’accès à la base de données avant validation du formulaire, etc. Il suffit d’écrire une classe qui implémente PhaseListener et de l’enregistrer dans le fichier faces-config.xml
.
Vue : Le templating avec Facelets
Les templates sont des fichiers XML contenant des instructions diverses. Ils permettent notamment la création de composants réutilisables.
Différences et points communs entre les JSP et les Facelets
- Possibilité d’utiliser les 2 technologies dans une même application. Le ViewHandler de Facelets délègue les requêtes qu’il ne sait pas traiter au ViewHandler JSP.
- JSP ne gère pas le templating (uniquement l’inclusion de pages).
- JSF 2.0 inclut nativement les Facelets, il est inutile de les importer dans le projet, sauf si celui-ci utilise les vielles instructions dans com.sun.facelets.
- Les Facelets manipulent des XML. Les JSP compilent des .class
- Les tags dans les Facelets servent à construire l’aborescence de la vue ; pas la vue directement
Deux types de templates
- Le template « client » : Son nom correspond au ViewID. Il utilise plusieurs templates. Il contient les données brutes à intégrer dans la vue « décorée ». Un template client peut être le template d’un autre template client. Exemple de template client :
<ui:composition template="laf-template.xhtml"> <!--Toute section en dehors des balise n'est pas renvoyée au client.--> <ui:define name="body"> Partie HTML contenant la section devant être intégrée dans le template ... <!-- jsfc permet de définir quel est le composant à afficher si on affiche la page en brut dans un navigateur web. --> <input type="text" jsfc="h:inputText" required="true" id="fname" value="#{UserBean.firstName}" /> ... </ui:define> </ui:composition>
<ui:insert>
des divers <ui:define>
définis dans le template client.
<ui:insert name="body">Texte descriptif visible dans le fichier brut si ouvert dans le navigateur directement</ui:insert>
Exemple de template « mixte » :
<html ...> <f:view> <body> ~~HTML~~ <ui:composition template="_header.xhtml"> <ui:define name="header" /> </ui:composition> <ui:insert name="body" /> ~~HTML~~ </body> </f:view> </html>
Glossaire des tags templates
<ui:composition template="optionnal">
<ui:decorate template="required">
<ui:define>
<ui:insert>
<ui:include>
Utilisé par les templates clients. Le contenu sera intégré dans l’arborescence de l’IHM (insertion de template).
Même fonction que <ui:composition>
, mais la mise en forme est conservée.
Dans un template client et dans un <ui:composition>
. Définit le contenu qui doit être remplacé dans le template là ou est utilisé <ui:insert>
.
Dans le template. Définit l’emplacement d’insertion du contenu paramétré avec le même nom dans le template client.
A utiliser avec des <ui:param>
pour permettre l’inclusion paramétrée de pages. Présent dans tout type de template.
Autres tags Facelets utilisé non liés au fonctionnement des templates
<ui:component id="optional" binding="optional">
<ui:fragment id="optional" binding="optional">
<ui:remove>
<ui:debug hotkey="optional">
Définit un composant réutilisable.
Idem, mais un fragment contient plusieurs ui:component.
Permet de mettre en commentaire le reste des composants situés dans le fichier.
Ouvre une popup affichant l’arobrescence de l’IHM. Par défaut sur Ctrl-Shift-D. Le web.xml doit être paramétré pour autoriser cette action. Ajouter le context-param javax.faces.FACELETS_DEVELOPMENT.
Utilisation des beans managés depuis les JSF
- Elle se fait par IoC (Inversion of Control = injection). Pas de
new
sur ces objets donc. Cette fonction est tirée de Spring. - Les beans managés sont des POJO (attributs, getters, setters)
- La définition de la portée (scope) se fait par les annocations ou dans le fichier faces-context.xml (via
<managed-beans>
). - Pour accéder aux getters depuis une vue JSF :
#{userBean.firstName}
ou encore#{userBean.interests[0]}
pour les listes ou#{userBean.interests['sport']}
pour les maps. - Pour accéder au setters, la syntaxe est identique. Le set se fait dans les composants de saisie (
inputText
) et lors de la soumission du formulaire.
Description des annotations des beans managés
@ManagedProperty
(sur les attributs) : permet de définir une valeur par défaut à l’attribut (appelle alors le setter qui doit exister). L’exemple suivant instancie uneAdress
(Bean de scope null cf. plus bas) sur le beanUserBean
:
@ManagedProperty(value="#{adressBean}") private Adress homeAddress;
@ManagedPropoerty(value="#{param.userid}") private String userid; /* URL type : http://.../myPage.jsp?userid=toto */
@ManagedBean
(sur la classe) : définit la classe comme un managed bean, il prend par défaut le nom de la classe première lettre en minuscule.@PostConstruct, @PreDestroy, @Resource, @EJB, @EJBs, @WebServiceRef, @WebServiceRefs, @PersistenceContext, @PersistenceContexts, @PersistenceUnit, @PersistenceUnits
Définition des scopes
Se placent après @ManagedBean
en en-tête de classe.
@NoneScoped
@RequestScoped
@ViewScoped
@SessionScoped
@ApplicationScoped
@CustomScoped
Beans non instanciés servant à être inclus dans d’autres beans. Leur portée (scope) hérite alors du bean appelant. Exemple de bean @NoneScoped
: Address
(attribut complexe pouvant intégré dans d’autres beans)
Portée à privilégier car génère le moins de fuites mémoires. Le bean est relâché à la fin de la requête HTTP, ce qui encourage à faire des vues légères.
Le bean reste accessible tant que l’utilisateur reste sur la même vue.
Le bean est accessible durant la session de l’utilisateur. Type d’utilisation : panier de courses.
Accessible tant que l’application est en ligne, et tout client confondu (statique).
Le développeur fournit une classe de type Map dans laquelle seront stockées les instances. A sa charge ensuite de gérer la mémoire.
Spécification du langage EL
EL
: Expression Language. Utilisé par la spécification JSF.
Value Expression
- Appeler une valeur dans un bean managé :
#{nomDuBean.attribut}
- Les beans sont évalués lors du Runtime. Le contexte parcours les différentes portées (scopes) lors de son exécution, et découvre les beans correspondants.
- Exemple de rendu conditionnel d’un composant :
rendered="#{nomDuBean.administrateur}"
(méthode isAdministrateur qui renvoie true ou false) ou encore :rendered="#{! empty nomDuBean.listeQuelconque}"
- Certains beans sont accessibles dans toute l’application par défaut :
application (ServletContext) ; applicationScope(Map) ; component(UIComponent) ; cookie(Map) ; header (Map) ; headerValues (Map) ; initParam (Map) ; param (Map) ; paramValues (Map) ; request (ServletRequest) ; requestScope (Map) ; resource (Resource) ; session (HttpSession) ; sessionScope (Map) ; view (UIViewRoot) ; viewScope (Map)
- Les Value Expression Flash permettent de stocker en mémoire « flash » (à court terme) dans le but d’être réutilisé par exemple dans la même page suite à l’envoi d’un formulaire en cours de saisie. Gestion de ces variables :
#{flash.nomVariable}
Value Operators
- Permettent de manipuler plusieurs EL dans une EL
- Syntaxe :
"#{userBean.attribut == 'Value'}" ; #{(userBean.tempFahreneit - 32) * 5 / 9}
Method Expression
- Invocation de méthodes publiques non statiques dans les managed beans
- Exemple :
action="#{userBean.addUser}" ; #{model.getValue(param.arg1, param.arg2)}
- Pas d’argument possible avant EL 2.1 (à partir de JEE 6)
- Attention à la sécurité : ne pas passer les arguments non vérifiés aux méthodes des beans. Utiliser
View Parameters
(cf chapitres suivants)
Utilisation des beans managés depuis les beans sessions
Les beans session ont une portée plus grande que les beans managés dont la durée de vie correspond au temps d’exécution de la requête HTTP. Ces beans appelle les beans sessions et ces derniers doivent pouvoir accéder au contenu des beans managés lorsqu’ils exécutent leur contenu.
Récupérer et modifier des valeurs dans un bean managé
ELContext elcontext = context.getELContext(); Application app = context.getApplication(); /* Récupérer l'UID d'un user */ String uid = (String)app.evaluateValueExpressionGet(context, "#userBean.userid}", String.class); ExpressionFactory ef = app.getExpressionFactory(); /* Manipulation de l'ID du User */ ValueExpression veUserID = ef.createValueExpression(elcontext, "#{userBean.userid}", String.class); /* Récupération du User complet */ ValueExpression veUser = ef.createValueExpression(elcontext, "#{userBean}", UserBean.class); /* Récupérer l'UID d'un user */ uid = (String)veUserID.getValue(elcontext); /* Mettre à jour la valeur */ veUserID.setValue(elcontext, "newUserID"); /* Récuperer le User */ UserBean u = (UserBean)veUser.getValue(elcontext);
Invoquer des méthodes dans un bean managé
Application app = FacesContext.getCurrentInstance().getApplication(); ExpressionFactory ef = app.getExpressionFactory(); MethodExpression me = ef.createMEthodExpression(elcontext, "#{userBean.addUser}", Void.class, null); try { me.invoke(context, null); } catch (ELException e) { /* Tout erreur d'invocation est rejetée ici. Voir e.getCause() */ }
Ecriture des Backing Beans
- Permet d’associer rapidement le code source aux beans, et facilite le développement via les IDE. Un backing bean joue le rôle de man-in-the-middle pour faire le lien entre la vue cliente et ce qui sera traité par la couche business.
- Il convient de créer un backing bean par page JSF ayant le scope Request. Cette classe porte le même nom que la page JSF. Par exemple pour une page login.JSF le backing bean s’appellera Login.java. Il est également de bon ton de regrouper les backing beans dans un même sous package
.backing.*
.
Le modèle de navigation
La navigation est traitée par le NavigationHandler
. Il reçoit une demande via une action ou un outcome, selon le composant à l’origine de la demande. L’outcome est généré après traitement de l’actionEvent associé. Il peut être null ou ne correspondre à aucune page ; dans ce cas on reste sur la même page ; sinon la nouvelle page est générée.
On distingue deux types de navigation
- Navigation implicite
Lien d’une page à une autre par appel direct vers celle-ci dans l’attribut « action » ou outcome du composant de la facelet. Le fichier ciblé doit avoir le même nom que l’action souhaitée et la même extension que le fichier d’origine. Possible à partir de JSF 2.0. Pratique pour prototype, mais pas pour maintenir l’application (renommage de page, etc).
Syntaxe d’une page1 pointant sur une page2 :
<!-- le fichier page1.html pointant vers page2.xhtml de façon implicite --> <h:commandButton value="Goto Page 2" action="page2">
Description du comportement de l’application dans le fichier faces-context.xml
. Elle peut-être dynamique, c’est à dire dépendre du contexte actuel pour diriger vers telle ou telle page, ou statique. Un diagramme de flux est déductible de ce fichier (reverse-engineering), ou peut aider à construire ce fichier (via une analyse).
Syntaxe du fichier de navigation (faces-config, ou fichier séparé) :
<navigation-rule> <!-- nom de la page source ; wildcard acceptés (*) --> <from-view-id>page1.xhtml</from-view-id> <navigation-case> <!-- Facultatif --> <description>Blah blah blah </description> <!-- Facultatif, on peut cibler l'action qui a généré l'outcome --> <from-action>#{myBean.isMyMethod}</from-action> <!-- Facultatif, évaluer le controleur. Pas de else, écrire un autre navigation-case si nécessaire --> <if>#{myBean.isValueOk}</if> <!-- Décrit l'action / outcome à paramétrer --> <from-outcome>allerAPage2</from-outcome> <!-- Page à cibler une fois les critères ci-dessous validés --> <to-view-id>page2.xhtml</to-view-id> <!-- Facultatif, génère une redirection. Voir explications ci-après --> <redirect /> </navigation-case> </navigation-rule>
Les redirections
L’instruction xml <redirect/>
permet de rediriger vers une page. Une redirection ferme la requête en cours, et en crée une nouvelle, avec une nouvelle vue. Les informations du formulaire sont perdues, et le navigateur pointe sur une toute nouvelle URL (autrement seule la vue est mise à jour mais le navigateur ne change pas d’URL).
Pour rappel, les composants <h:button/>
et <h:link/>
génèrent des requêtes GET. Ils créent donc des redirections par défaut. Les composants <h:commandLink/>
et <h:commandButton/>
génère des requêtes POST, et ne créent pas de redirection.
Pour forcer la redirection en navigation implicite (sans paramétrage du faces-context
)
<h:commandButton value="HTTP POST + Redirection" action="page2.xhtml?faces-redirect=true" />
Paramétrage de la vue
Il est possible de transférer certains paramètres idempotents via une requête GET. Ils sont dits idempotents car ils ne servent qu’à charger une vue, et pas à mettre à jour le modèle. On ne doit pas utiliser d’autre type de paramètre via des requêtes GET. Cela n’est pas du tout sécurisé (indexation dans les moteurs de recherche, favoris, etc). Pour ce faire, il faut utiliser les paramètres de la vue. Les argument sont passés par URL.
- De manière implicite
action="page2?faces-context=true&includeViewParameters=true"
Récupération des paramètres d’URL dans la page2
<html> <f:metadata> <!-- page1 contient un composant avec l'id "idAttribut1" dans le formulaire --> <f:viewparam name="idAttribut1" value="#{myBean.attribut1}" /> </f:metadata> <head>
<navigation-rule> ... <navigation-case> ... <redirect> <view-param> <name>idAttribut1</name> <value>#{myBean.attribut1}</value> </view-param> </redirect> </navigation-case> </navigation-rule>
Gestion des exceptions
Le fichier web.xml
permet de traiter les exceptions inattendues et de rediriger vers les pages correspondant au type d’exception relevé. Une page par défaut est utilisée pour les cas non paramétrés, affichant la trace complète de l’exception.
- Syntaxe
<error-page> <exception-type>javax.faces.application.ViewExpiredException</exception-type> <location>/faces/sessionExpired.xhtml</location> </error-page> <error-page> <exception-type>my.own.ExceptionType</exception-type> <location>/faces/MyOwnException.html</location> </error-page>
Les composants
L’interface visuelle se construit à l’aide de composants (hérités de UIComponent
) et de helpers, tels que renderers, tag handlers, validateurs, convertisseurs, etc.
Un framework basé composant offre une palette d’outils réutilisables. C’est le cas par exemple de JSF UI Compoents
, de Oracle ADF
, etc. Aller sur jsfcentral.com pour voir un ensemble de frameworks existants, des tutoriels, et diverses librairies pouvant être utiles.
- Composants d’action : Ils implémentent
ActionSource2
(UICommand
) - Composants d’affichage de données : Ils implémentent
ValueHolder
, ouEditableValueHodler
(UIInput, UIOutput
) - Composants conservant leur état d’une requête à l’autre : Ils implémentent
PartialStateHolder
(UIComponent
et helpers) - Composants qui fournissent un espace nommé aux composants fils : Ils implémentent
NamingContainer
(UIForm, UIData, ...
) - Composants qui sont liés au comportement du client (Ajax) : Ils implémentent
ClientBehaviourHolder
(composants HTML)
Accéder aux composants pour les manipuler directement
Utiliser un backing bean :
@ManagedBean(name="backing_page1") @RequestScoped public class Page1 { private HtmlInputText input1; private HtmlOutputText output1; /* Getters et Setters de ces deux attributs */ public String actionUpdate() { output1.setText("Updated"); return "success"; } }
Contenu de la page correspondante, page1.xhtml :
<h:form> <h:inputText binding="#{backing_page1.input1}" /> <h:commandButton value="update Me" action="#{backing_page1.actionUpdate}" /> <h:outputText binding="#{backing_page1.output1}" /> </h:form>
Quelques conseils
- La bonne façon de faire est d’utiliser un bean modèle si les données du modèle doivent être mises à jour directement (value binding)
- Si seule la vue doit être mise à jour, préférer l’utilisation de backing beans (component binding)
- La gestion d’événements de la vue est plus propre à gérer dans les backing beans que dans les classe modèles.
- Ne pas instancier les composants dont on n’a pas besoin dans le backing bean (attention à la génération automatique de code dans les IDE). Cela alourdit pour rien.
Conversion et validation des données
L’étape de conversion sert à valider le type de donnée, à ne pas confondre avec l’étape de validation qui sert elle à valider le contenu de la donnée (via des contraintes).
Processus général
- L’utilisateur valide le formulaire et envoie les données au serveur qui les récupère (
EditableValueHolder.getSubmittedValue
) - Recherche de la classe convertisseur associée. Cherche dans le renderer du composant, puis dans le composant, dans le binding. Si aucun convertisseur n’est trouvé, la valeur est considérée comme traduite.
- Conversion de la valeur (
Converter.getAsObject(String)
). En cas d’erreur, fin de la procédure. Validation non traitée. - Validation de la valeur convertie. Test du required, appel des méthodes de validation trouvées. Fin de la procédure en cas d’erreur de validation.
- Si la valeur est validée, un
ValueChangeEvent
est publié ; sinon on appelle leFacesContext.renderResponse()
qui va être chargé de renvoyer les erreurs trouvées.
Conversion
Les composants sont rattachés à 0 ou 1 converter et sont de type input ou output.
Conversion implicite dans les JSF via les attributs standards tels que :
required="true"
<f:convertNumber type="percentage"/>
<f:validateLongRange miniumum="0" maximum="10" />
- De même avec
validateDoubleRange, validateLength, validateBean /* cf. Bean Validation */, validateRegularExpression
, … - Les types primitifs sont implicitement convertis (
Integer
, etc.) - Association vers un converter personnalisé :
<f:converter conterterId="myConverter"/>
- Un convertisseur a deux méthodes (issues de Converter) :
getAsObject(String)
; etgetAsString(Object)
pour faire la conversion dans les deux sens.
Quelques convertisseurs
<f:convertDateTime>
: contient les éléments permettant d’analyser les dates en fonction des locales, timezone, d’un pattern.<f:convertNumber>
: contient les éléments permettant de gérer le format monétaire, le groupement des chiffres, un pattern.<f:converter>
: Appel d’un converter personnalisé, paramétré dans faces-config.xml.
Il est possible d’associer un convertisseur par programmation
FacesContext ctx = FacesContext.getCurrentInstance(); Converter intConv = null; UIComponent c = null; UIViewRoot root = ctx.getViewRoot(); // Recherche du composant dans l'arborescence : c = (ValueHolder) root.findComponent("form" + NamingContainer.SEPARATOR_CHAR + "intComponent"); intConv = ctx.getApplication().createConverter("javax.faces.Integer"); c.setConverter(intConv);
Il est possible d’écrire un convertisseur personnalisé
@FacesConverter(value="myConverter") public class MonConvertisseur implements Converter { @Override public Object getAsObject(FacesContext ctx, UIComponent ui, String value) { ... } @Override public String getAsString(FacesContext ctx, UIComponent ui, Object value) { ... } public boolean loadUserPref(FacesContext ctx) { Application app = ctx.getApplication(); ValueExpression ve = app.getExpressionFactory() .createValueExpression(ctx.getELContext(), "#{user.pref.isPref}", Boolean.class); return (Boolean)ve.getValue(ctx.getELContext()); } }
Validation
Les composants sont rattaché à 0 ou n validateurs et sont de type Input.
- Association vers un validateur personnalisé :
<f:validator validatorId="myValidator"/>
- Un validateur contient une méthode
validate
qui, en cas d’échec doit rejeter une exception de typeValidatorException
, avec le message correspondant. - Les composants « input » peuvent définir le valdidateur directement en tant qu’attribut du composant :
<h:inputText validator="#{user.validateAge}" value="#{user.age}"/>
. Le bean user contient alors la méthodepublic void validateAge(FacesContext, UIComponent, Object)
.
Il est possible d’associer des validateurs par programmation
FacesContext ctx = FacesContext.getCurrentInstance(); Validator progressVal = null; MethodBinding ptrToValMethod = null; EditableValueHolder comp = null; UIViewRoot root = ctx.getViewRoot(); // Recherche du composant dans l'arborescence : comp = (EditableValueHolder)root.findComponent("form" + NamingContainer.SEPARATOR_CHAR + "userComponent"); // Vérifier les doublons Validator[] vs = comp.getValidators(); boolean found = false; for (Validator v : vs) { found |= v[i] instanceof ProgressValidator; } if (found) { return; } progressVal = ctx.getApplication().createValidator("progressVal"); comp.addValidator(progressVal); // Si le composant a déjà une méthode rattachée à un validateur on s'arrête if (comp.getValidator() != null) { return; } Class[] params = new Class {FacesContext.class, UIComponent.class, Object.class}; ptrToValMethod = ctx.getApplication().createMethodBinding("#{user.validateAge}", params); comp.setValidator(ptrToValMethod);
Il est possible d’écrire ses validateurs personnalisés (voir exemple dans le livre)
@FacesValidator(value="myValidator") public class MyValidator implements Validator { public void validate(FacesContext ctx, UIComponent ui, Obejct value) { try { ... } catch (Throwable t) { ... } if (...) { FacesMessage msg = new FacesMessage("Erreur !"); throw new ValidatorException(msg); } } }