Archives Wild Code School
Java

JSF et Facelets

Quête 39 sur 43

Salut Wilder! Cette quête s'adresse à toi si tu veux passer à la vitesse supérieure en JEE! JSF est un framework MVC orienté composants et évènementiel, pour faire du Web. Facelets une techno de templating. Tous deux très puissants mais pas facile à apprivoisier. Tu es prêt ?

Objectifs

  • Savoir faire un setup de projet JSF
  • Maîtriser EL
  • Savoir créer des pages JSF avec ou sans Facelets
  • Dompter le cycle de vie d'une app JSF
  • Savoir créer des composants
  • Savoir utiliser des libs tierces

Setup

Un projet JSF est un projet JEE Web standard sur lequel on a branché et activé quelques ressources supplémentaires: Tout d'abord dans ton répertoire webapp (si tu utilises Maven), sous l'habituel /WEB-INF/web.xml, tu dois déclarer la FacesServlet. C'est une bonne vieille servlet comme tu as vu dans les quêtes précédentes, mais tu ne vas pas y toucher. C'est elle qui va intercepter toutes les requêtes à destination d'une page JSF (avec ses doGet/doPost standards) et te donner la main d'une manière différente. Tu dois aussi dire quelle extension de page tu souhaites utilsier. Normalement c'est .jsf. Mais tu pourrais mettre .wcs si tu es Wilder dans l'âme :) /WEB-INF/web.xml :

xml
<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>*.jsf</url-pattern>  
</servlet-mapping>

JSF stocke tout l'arbre de composants que constatue la page entre deux requêtes. tu as le choix entre client et server.

xml
<context-param>  
 <description>State saving method: 'client' or 'server' (=default). See JSF Specification 2.5.2</description>  
 <param-name>javax.faces.STATE_SAVING_METHOD</param-name>  
 <param-value>client</param-value>  
</context-param>

Ainsi tu l'as compris, JSF est/était stateful (mais il se soigne). Tu retrouves ces paramètres de JSF 1.0 à 2.3.

Avant JSF 2.0 tu devais configurer les contrôleurs derrière les pages en XML, et il y avait un fichier pour ça: /WEB-INF/faces-config.xml, dans le même répertoire que web.xml. On y mettait aussi des règles de navigation, des converters, des validators, des bundles d'internationalisation, des phaselisteners... et puis les annotations sont passées par là, donc ce fichier tendait à disparaître...

...sauf que pour activer JSF 2.3, il le faut quand-même (sic)... donc on va le déclarer dans le /WEB-INF/web.xml :

xml
<context-param>  
 <param-name>javax.faces.CONFIG_FILES</param-name>  
 <param-value>/WEB-INF/faces-config.xml</param-value>  
</context-param>

puis le créer: /WEB-INF/faces-config.xml :

xml
<?xml version='1.0' encoding='UTF-8'?>  
<faces-config version="2.3" xmlns="http://xmlns.jcp.org/xml/ns/javaee"  
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
  xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee  
 http://xmlns.jcp.org/xml/ns/javaee/web-facesconfig_2_3.xsd">  
 <application>
  <locale-config>
   <default-locale>en</default-locale>  
  </locale-config>
 </application>  
</faces-config>

Ne me crois pas si tu veux, mais là tu n'as que JSF 2.2. Malgré la version déclarée juste au dessus. Pour activer la v2.3, tu dois créer une classe bidon dans n'importe quel package de ton appli avec:

java
import javax.faces.annotation.FacesConfig;  

@FacesConfig(version = FacesConfig.Version.JSF_2_3)  
public class JsfConfig {/*fou non?*/}

De même à une époque, pour bénéficier de CDI, il fallait instancier un fichier vide beans.xml (lui aussi dans WEB-INF, ou META-INF dans une app non-web). Ce n'est plus nécessaire...sauf qu'il le faut toujours pour bénéficier des derniers ajouts de CDI dans JSF 2.3. Pourquoi faire simple quand on peut faire compliqué? Hein, je te le demande? /WEB-INF/beans.xml :

xml
<?xml version="1.0" encoding="UTF-8"?>  
<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee"  
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
  xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_2_0.xsd"  
  bean-discovery-mode="all" version="2.0">  
</beans>

/WEB-INF/web.xml:

xml
<context-param>  
 <param-name>javax.faces.ENABLE_CDI_RESOLVER_CHAIN</param-name>  
 <param-value>true</param-value>  
</context-param>

VOILA! On va pouvoir faire du JSF. Ah, non, j'allais oublier: il faut déclarer les api dans ton pom.xml:

xml
<dependency>  
 <groupId>javax.faces</groupId>  
 <artifactId>javax.faces-api</artifactId>  
 <version>2.3</version>  
 <scope>provided</scope>  
</dependency>  

Normalement tu as déjà déclaré javax.el-api, javax.inject et cdi-api dans les quêtes précédentes, mais si tu as ripé, je te le rappelle.

Tu as envie de mourir? Moi aussi, mais ça va aller maintenant :)

↗
JSF parameters

Paramètres standards JSF. Il existe plusieurs implémentations de JSF (Mojarra, MyFaces) et chacune a ses paramètres propres en plus.

stackoverflow.com
↗
Mojarra, getting started
javaserverfaces.github.io
↗
MyFaces, getting started
myfaces.apache.org
↗
JSF 2.3 activation
github.com

Il sent les années 90 ton site

C'est pas pour dire mais tes JSP dans le projet github-tracker, elles sentent drôlement le J2EE 1.4. On en est à JEE8 là. Faisons une petite migration. Tu vas commencer par créer un répertoire /faces/ dans ton /webapp afin de bien différencier JSP et JSF. Et à l'aide du menu contextuel de IntelliJ sur le répertoire /faces tu vas y créer un fichier JSF githubers.xhtml. Ouvre-le.

  • Le début te fait beaucoup penser à une JSP mais où les définitions de taglibs seraient en fait des déclarations de namespaces xml:
xml
<?xml version="1.0" encoding="UTF-8"?>  
<!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://xmlns.jcp.org/jsf/html"  
  xmlns:ui="http://xmlns.jcp.org/jsf/facelets"  
  xmlns:f="http://xmlns.jcp.org/jsf/core">

Oui tu l'as compris, tu vas bénéficier là aussi de certaines fonctionnalités dans les pages grâce aux préfixes h, f, et ui.

Ensuite tu peux définir ta vue JSF à l'aide de

xml
<f:view contentType="text/html" encoding="UTF-8" locale="fr">

et enfin mettre les sections <head> et <body> que tu connais bien...enfin pas exactement. Il s'agit de <h:head> et <h:body>. Ce sont des composants JSF, ils ont une existence dans l'arbre que construit JSF pour gérer la page, et tu pourrais même les retrouver programmatiquement. Tandis que <head> et <body> ne sont pour JSF que du verbatim, du texte qu'il n'a pas à essayer de comprendre.

Bref,

xml
<?xml version="1.0" encoding="UTF-8"?>  
<!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://xmlns.jcp.org/jsf/html" xmlns:ui="http://xmlns.jcp.org/jsf/facelets" xmlns:f="http://xmlns.jcp.org/jsf/core">  
<f:view contentType="text/html" encoding="UTF-8" locale="fr">
  <h:head>  
    <title>Githubers</title>  
  </h:head>  

  <h:body>  
    <!-- TODO -->  
  </h:body>  
</f:view>  
</html>

Dans la page githubers.jsp, tu vas aller chercher le code qui itérait sur les githubers et le copier ici. Ne te préoccupe pas s'il y a des classes bootstrap dedans.

  • Tu vas remplacer les expressions EL ${} en #{}

  • Tu vas remplacer ton <img class="..." src="#{githuber.avatarUrl}" alt="#{githuber.name}"></img> en <h:graphicImage styleClass="..." value="#{githuber.avatarUrl}" alt="#{githuber.name}" />

  • Tu vas remplacer la boucle <c:forEach items="${githubers}" var="githuber"> que tu es censée avoir faite en <ui:repeat value="${githubersController.allGithubers}" var="githuber">

Tu l'as compris, en JSF, EL est préfixé # au lieu de $, et toute balise html comme <img> a son pendant comme <h:graphicImage/>. Tous les composants JSF ont un attribut id, value, styleClass et rendered, et au "rendering" (c'est une phase du cycle de vie JSF), ces composants sont traduits en joli html. Là encore on peut se passer de ces composants et écrire du html pur, mais alors c'est du "verbatim", aucune action possible dessus via JSF (ajax...).

Et githubersController, kézako ? Le C de MVC pardi! ainsi tu vas créer ce contrôleur GithubersController dans un package ad-hoc et l'annoter @Named (ou @Named("githubersController") si la classe porte un autre nom). Et là, il n'y a plus à hésiter, tu vas y créer une méthode getAllGithubers() qui va déléguer le travail à ta classe GithubersService que tu auras pris soin d'injecter. 4 lignes de code :)

Tour de magie: en écrivant #{githubersController.allGithubers}, EL va chercher un bean @Named qui s'appelle githubersController, CDI va l"instancier, et EL va appeler dessus la méthode get<ce qu'il y a après le point>. Mais tu pourrais aussi appeler toute méthode directement comme #{githubersController.getAllGithubers()} avec les parenthèses. Même si elles ont des paramètres!

**Tu peux tester que l'appli marche en appelant **http://localhost:8080/GithubTracker/faces/githubers.jsf (ou .wcs si tu es un Wilder dans l'âme)

En l'absence de toute spécification, le scope de GithubersController est @Dependent? Si tu n'es pas sûr du peux tester en y ajoutant une méthode annotée @Postconstruct qui logge quelque chose, et taper plusieurs fois F5 sur ton navigateur... Après, mon conseil, c'est de toujours explicitement donner un scope @*Scoped pour ne pas avoir de surprise.

↗
JSF EL @Oracle
docs.oracle.com
↗
JSF EL @ Baeldung
baeldung.com

Facelets

Sauf que c'est moche. Et tu te vois mal habiller chaque page avec toute la grappe de ressources de Bootstrap. En fouillant un peu tu vois qu'il existe <ui:insert> qui ressemble à <jsp:include> mais tu n'es pas sûr de comment l'utiliser. En effet, Facelets renverse complètement la manière de penser: on définit un template avec des placeholders <ui:insert> et les pages à destination des visiteurs "implémentent" ces placeholders grâce à <ui:define>. tu peux faire le parallèle avec une classe abstraite et son implémentation!

  • Créée une nouvelle page appelée template.xhtml

  • Dans le <h:head> colles-y les mêmes meta et link que dans header.jsp (en prenant soin de bien </...> terminer les tags car nous écrivons du xml à présent)

  • et rajoutes-y <title><ui:insert name="pageTitle">Github Tracker</ui:insert></title> (<ui:insert> définit un placeholder et son contenu c'est un peu comme une implémentation par défaut)

  • Dans <h:body> colle ta navbar...

  • ...remplace ${pageContext.request.contextPath} (JSP) par #{facesContext.externalContext.requestContextPath} (JSF)

  • ...et si tu avais utilisé des tags jsp comme c:set ou c:if, tu vas devoir simplifier ou adapter

  • ...puis rajoute une section

xml
<div class="container">  
  <ui:insert name="contents">  
  <h:outputText value="CONTENTS PLACEHOLDER"/>  
  </ui:insert>  
</div>

  • ...et finis par les mêmes scripts que dans footer.jsp

Ensuite dans githubers.xhtml, tu vas uniquement garder la substantifique moelle:

  • pas de <h:head>

  • <h:body> qui contient seulement:

xml
 <ui:composition template="template.xhtml">  
  <ui:define name="pageTitle">  
    <h:outputText value="All our Githubers"/>
  </ui:define>  

 <ui:define name="contents">  
 <ui:repeat value="#{githubersController.allGithubers}" var="githuber">  
 <!-- rest of your code -->
</ui:repeat>  
 </ui:define>  
 </ui:composition>  
</h:body>  
</html>

Tour de magie: le template est utilisé et les zones définies par <ui:insert> sont pour ainsi-dire "implémentées" dans ta page avec <ui:define>

Au passage nous avons utilisé un objet magique, le FacesContext, qui sait toujours TOUT sur la requête, la réponse, et bien d'autres choses, requête après requête.

↗
Introduction to Facelets
docs.oracle.com
↗
What is FacesContext
stackoverflow.com

Formulaires

Nous allons à présent introduire un formulaire visant à suivre un nouveau githuber. De la même manière que ci-dessus, tu vas créer une page track.xhtml, lui faire implémenter template.xhtml, et reproduire la même fonctionnalité que dans track.jsp de la manière suivante: En JSF un formulaire se définit avec <h:form>, sans action, method, ou enctype. Et à l'interieur, on peut y mettre entre autres des <h:outputLabel>, des <h:input*>; et des <h:commandButton> / <h:commandLink> pour soumettre. Pour chacun, l'attribut value attend un texte (output) ou une expression #{EL}(input/output) qui correspond à ce qu'on affiche/attend. Et les <h:command> attendent:

  • soit un attribut action se branchant sur une méthode public String myAction() ou public Enum<?> myAction(), la valeur de retour étant une règle de navigation ou un nom de page,

  • soit un attribut actionListener se branchant sur une méthode public void myListener(ActionEvent)

En ce qui te concerne, pour soutenir cette page, tu crééeras un bean @Named @RequestScoped TrackController qui contiendra une propriété login (+getter/setter) branchée à ton <h:inputText> et une méthode public String track() branchée à ton <h:commandButton>.

Outre la validation côté client que tu peux contrôler avec les attributs size, maxlength et required de ton <h:InputText>, tu ne souhaites pas que l'utilisateur puisse envoyer n'importe quoi avec Postman par exemple, donc tu vas mettre un bloc de validateurs à l'intérieur de ton inputText, comme ça:

xml
<h:inputText id="login" value="#{trackController.login}" styleClass="form-control" size="39" maxlength="39" required="true">  
    <f:validateRequired/>  
    <f:validateLength maximum="39"/>
    <f:validateRegex pattern="[a-zA-Z0-9][a-zA-Z0-9-]*"/>  <!-- regexp: only alphanumeric and dash from the second char -->
</h:inputText>

Note: nous verrons les RegExp plus tard.

Tu pourrais tout aussi bien implémenter tes propres validateurs avec @FacesValidator. Idem si tu as besoin de convertir tes données (eg un numéro de carte bancaire 4 digits par 4) grâce à <f:convert*> et @FacesConverter.

TrackController devra évidemment injecter et utiliser GithubersService. Grâce aux validateurs, tu n'as aucunement besoin de contrôler que le login est au format correct dans ta méthode track(). Mais si ce login n'existe pas sur Github, tu devras quand-même gérer l'erreur avec du code du genre:

java
FacesMessage fm = new FacesMessage("Inexsitent Github user: " + login);  
FacesContext.getCurrentInstance().addMessage(null, fm);

Ces messages custom s'affichent dans ta page seulement si tu as ajouté le tag <h:messages/> quelque part dans ta page. Fais-le.

La valeur de retour d'une action est soit une règle de navigation, soit un nom de page; ici nous retournerons:

  • "githubers" en cas de succès pour directement retourner sur la liste des githubers;

  • null en cas d'erreur pour rester sur la même page.

A présent, teste que /faces/track.jsf fonctionne comme track.jsp!

↗
How to add placeholders in JSF Input
stackoverflow.com
↗
Many JSF2 examples
mkyong.com
↗
JSF form example on StackOverflow
stackoverflow.com
↗
Validation in JSF
incepttechnologies.blogspot.fr
↗
Username limitations for various services
gist.github.com
↗
PostMan
getpostman.com

Ajax

Jusqu'à présent tu ne t'es pas préoccupé du bouton [untrack] dans la liste des githubers. En JSP c'était simple, il suffisait de passer l'id du githuber comme valeur associée au bouton, ou bien d'avoir un pattern d'URLs spécifique comme .../untrack/123. En JSF c'est tout aussi simple: il suffit de créer une methode public void untrack(long githuberId) dans GithubersController qui délègue à la bonne méthode de GithubersService; et dans la page, en supposant que la variable d'itération du <ui:repeat> est githuber, alors il te suffit de créer ton bouton comme cela:

xml
<h:form>  
    <h:commandButton styleClass="btn btn-outline-danger btn-sm" value="Untrack" actionListener="#{githubersController.untrack(githuber.id)}">  
    </h:commandButton>  
</h:form>

Evidemment, pour que toute action marche, il faut que sa balise soit à l'intérieur d'un <h:form>.

Ah, tu préfères travailler à partir de l'objet lui-même? La méthode n'a qu'à être public void untrack(Githuber gh) et que le bouton:

xml
<h:form>  
    <h:commandButton styleClass="btn btn-outline-danger btn-sm" value="Untrack" actionListener="#{githubersController.untrack(githuber)}">  
    </h:commandButton>  
</h:form>

Tour de magie: Comme JSF garde en mémoire toute la strucutre de composants de la page et les valeurs associées, il est capable de retrouver le githuber correspondant à chaque élément de ta liste de githubers et la passer directement à une méthode! Et tu peux faire la même chose avec une combo box par exemple !

...Oui, bon, sauf que quand on clique sur le bouton c'est toute la page qui se recharge. Ce serait bien qu'on ne raffraîchisse que la liste des githubers.

Rien de plus simple: il suffit:

  • d'encadrer le composant qui liste les githubers par un container simple avec un un id explicitement défini. Par exemple:
xml
<h:panelGroup id="githubersList">  
    <ui:repeat value="..." var="githuber">
    ...
    </ui:repeat>
</h:panelGroup>    

  • de reformater ton bouton comme ça:
xml
<h:form>  
    <h:commandButton styleClass="btn btn-outline-danger btn-sm" value="Untrack">  
        <f:ajax listener="#{githubersController.untrack(githuber.id)}" render="githubersList"/>  
    </h:commandButton>  
</h:form>

Ainsi <f:ajax> active l'ajax sur ton bouton, et si tu mets un attribut render, tu peux demander le raffraîchissement du (ou des) composants qui portent cet (ces) id(s).

Au passage tu te rappeleras que l'attribut qui déclenche l'action n'est ni action ni actionListener, mais listener cette fois... et tu jetteras un oeil sur l'attribut execute qui permet de ne soumettre qu'une partie du formulaire dans lequel est inscrit le <f:ajax>.

Ca a l'air simple comme ça mais il y a plein de pièges, tu verras dans la partie 2.

↗
A comprehensive guide to JSF Ajax
beyondjava.net
↗
f:ajax javadoc
docs.oracle.com

Challenge

Titre du challenge

  1. Savoir faire un setup de projet JSF
  2. Migrer de EL JSP à EL JSF
  3. Migrer tes JSP en pages JSF
  4. Migrer tes pages JSF en facelets
  5. Faire un formualire en JSF
  6. Utiliser Ajax en JSF

Critères de validation

  • Tu as réussi le setup du projet
  • Tu as reproduit en JSF la liste des githubers
  • Tu as reproduit en JSF la page pour tracker un githuber
  • Ces pages utilisent des facelets
  • Tu as utilisé f:ajax pour le bouton untrack
  • Ton code est dispo dans une branche de ton projet github-tracker sur GitHub

stackedit_data: eyJoaXN0b3J5IjpbNTU5ODE0MTQ1LDEzNTM3MDE5NDksMTE2Nj c1MDM1MSwxMzE5ODg5NjA3LDExNjY3NTAzNTEsMTIyOTQwMjM0 NSwxODM0MTc0MzU4LC0xMDQzNTk0NzY5LC0xOTc3Nzk5NzI5LC 0xNzU1OTQxNjY1LDIwMzM0MjMzNTcsLTgyNDA4OTEwMCwtNDM2 MDE0NzMyLDE0MDY1OTMwNDksMTAxMjEyMDcyOCwtMjA4ODAzMT M5NCwtOTc2NDE2NDM0LC05NjgwNjk3NjQsLTE0NzcxMDAwMDUs LTY5NjgzNTM2MV19