TP 1 - Prise de contact et survol de Jakarta EE

Retour TPs

  1. Introduction
  2. Logiciels utilisés
  3. Création d'un projet de type Web Application
  4. Git
  5. Création de classes entités à partir d'une base de données existante
  6. Création d'un bean CDI CustomerManager pour la gestion des clients
    1. Ajout de méthodes liées à JPA dans CustomerManager
  7. Présentation de la partie front-end web
  8. Configuration de JSF
  9. Création d'un bean géré par CDI (backing bean de la page customerList.xhtml)
  10. Modification du fichier web.xml
  11. Ajout d'une page JSF pour afficher la liste des clients
    1. Ajout d'une DataTable JSF dans la page
  12. Exécution du projet
    1. Améliorer l'affichage pour discount
  13. Déploiement d'une application
  14. Utilisation d'une DataTable provenant de la librairie de composants JSF PrimeFaces
    1. Ajout de la librairie PrimeFaces dans le projet
    2. Modification de la page JSF
  15. Affichage des détails d'un client lorsqu'on clique sur une ligne
    1. Ajout d'un lien dans la table pour déclencher l'affichage des détails d'un client
    2. Ajout d'un backing bean pour la page des détails
    3. Ajout d'une page JSF pour afficher les détails d'un client
    4. Affichage des Discount dans la page des détails d'un client
    5. Ajout de boutons pour la mise à jour et retour à la liste
    6. Choix d'un Discount - convertisseur
    7. Soumission du formulaire, cycle de vie JSF
  16. Quelques petites améliorations
  17. Problèmes devant encore être réglés

Introduction

Vous allez écrire une application qui permet de gérer des clients (customers en anglais) qui sont enregistrés dans une base de données.

Ce TP permet une prise de contact directe avec les technologies Maven, Git, CDI, JPA, et JSF.

Il est conseillé de terminer ce TP avant le début des cours. Vous aurez ainsi plus de facilités à comprendre le cours car vous aurez des illustrations concrètes des notions exposées dans le cours. Prenez le temps de bien lire les explications.

Il ne servirait à rien de terminer le TP en recopiant le code donné dans le TP, sans essayer de comprendre les explications. Ne vous inquiétez pas si vous ne maitrisez pas tout car tous les points abordés dans ce TP seront approfondis dans les différents cours et TPs à venir.

IMPORTANT : s'il vous est demandé de finir un TP et de le mettre dans un repository GitHub, le projet doit contenir tous les commits demandés dans le TP, et tous les commits après chaque étape importante du TP. Attention, à bien cliquer sur le projet avant de faire un commit pour que tous les fichiers modifiés soient pris en compte. Un projet qui ne contient pas ces commits ne sera pas pris en compte.

Parties optionnelles : ce TP et les suivants comportent des parties optionnelles pour approfondir certains points du cours. L'examen ne pourra pas porter sur ces parties optionnelles qui, cependant, pourront vous être utiles si vous souhaitez développer un projet Jakarta EE.

Logiciels utilisés

Si vous ne l'avez pas déjà fait il est temps d'installer les logiciels.

Rappel : les captures d'écrans de ce TP peuvent ne pas correspondre exactement à ce que vous verrez car les options proposées peuvent varier selon les versions des logiciels utilisés.

Création d'un projet NetBeans de type Web Application

Dans ce TP vous allez développer une application de type "Web", qui correspond au profile Web de Jakarta EE. Un profile est un sous-ensemble de la plateforme Jakarta EE complète.

Lancez NetBeans.

Vous allez créer un projet Maven. Maven est le logiciel qui va vous permettre de créer le projet, de le construire (construire en particulier le fichier jar pour l'exécution du projet), de gérer les dépendances du projet (par exemple, le projet dépendra de l'API Jakarta EE). Les fichiers jar des dépendances utilisées par le projet seront récupérés automatiquement par Maven sur l'entrepôt (repository) central de Maven et enregistrés dans l'entrepôt local de Maven qui est sur votre ordinateur. Sous NetBeans vous pourrez voir la liste de ces fichiers dans l'onglet "Services", entrée "Maven Repositories".

Si toutes les dépendances utilisées par votre projet ne sont pas déjà dans l'entrepôt local, vous devez être connecté à Internet pour créer le projet.

Pour créer un projet "Web" utilisez le menu File > New Project de NetBeans puis choisissez la catégorie "Java with Maven", et un projet de type "Web Application" :

.Nouveau projet

Définition du projet

Cliquez sur Next.

Une nouvelle fenêtre pour la création du projet est affichée. Choisissez l'emplacement du projet et son nom. Il vaut mieux ne pas mettre de caractères particuliers dans le chemin ou dans le nom, pas d'accents (é, à,...) par exemple. On ne sait jamais... ;-).

Modifiez le nom du projet : par exemple tpCustomerXxxxx ; Xxxx pour votre nom ; arrangez-vous pour que ce nom vous identifie parmi tous les autres étudiants de votre classe ; relisez ce guide. Ce nom sera le nom affiché par NetBeans pour le projet.

Si vous voulez regrouper tous les projets de ce cours dans un répertoire spécial, désignez-le en cliquant sur le bouton "Browse". Ce répertoire sera placé au-dessus des répertoires qui contiendront les projets.

Modifiez aussi "Group Id" (prenez exemple sur l'image ci-dessous, en adaptant à votre cas). Group Id devrait identifier l'organisme qui a créé le projet, si on l'enregistrait sur l'entrepôt central de Maven. Ça ne sera pas votre cas et vous avez donc la liberté de choisir ce que vous voulez mais il déterminera le début des noms des paquetages des classes de votre projet et je vous conseille donc d'utiliser un nom significatif. Optionnel : voir https://maven.apache.org/guides/mini/guide-naming-conventions.html.

NetBeans attribue automatiquement la valeur de "Artefact Id" à partir du nom du projet.

Le nom du jar généré par la construction du projet (le build) sera formé avec la valeur de "Artifact Id" suivi du numéro de version. Ça sera un fichier war pour une application Jakarta EE Web.

Par défaut, la base des noms des paquetages des classes générées est définie par les valeurs de "Group Id" et du nom du projet (sans majuscules). Vous pouvez modifier le paquetage de base, par exemple en enlevant votre nom à la fin.

Par convention de Maven, le nom de la version est suffixée par SNAPSHOT si la version est encore en développement.

Une fois le projet créé, si vous le souhaitez, vous pourrez modifier indépendamment les valeurs de GroupId, ArtifactId, Version, Name par un clic droit sur le projet et Properties. Vous pourrez aussi changer le répertoire qui contient le répertoire du projet par un clic droit sur le projet et Move.

Nom du projet

Cliquez sur Next.

Dans la fenêtre suivante, indiquez le nom du serveur (Payara 6 que vous avez installé) et la version Jakarta EE 10 Web (ne tenez pas compte de cette ancienne capture d'écran) :

Choix du serveur

Cliquez ensuite sur le bouton "Finish". NetBeans génère alors un projet pour votre application. Le temps mis dépend des artefacts (des jars) que Maven devra charger de l'entrepôt central.

Le message "BUILD SUCCESS" doit apparaître dans la fenêtre "Output - Project Creation" en bas de la fenêtre de NetBeans.

Examen du projet généré

Le code généré par NetBeans produit une application REST très simple, avec aussi une page Web qui affiche un message du type "Hello World!" (ce message dépend de la version de NetBeans). Vous pourriez supprimer du code généré mais il n'est pas gênant pour la suite et je vous conseille de le garder.

Manipulation des fenêtres et zones d'affichage de NetBeans

Dans la partie gauche de la fenêre de NetBeans, 3 onglets Projects, Files et Services contiennent les informations sur une vue du projet, les fichiers du projets (y compris ceux qui ont été utilisés par NetBeans) et sur les services offerts aux applications: bases de données, serveurs d'application, etc.

Ces onglets peuvent être en haut ou à gauche de la zone gauche. S'ils sont à gauche, il suffit de déplacer la souris sur un des onglets pour faire afficher son contenu dans la zone de NetBeans en haut à gauche. Pour conserver cette zone ouverte, il suffit de cliquer sur les 2 carrés superposés en haut à droite de la zone; pour ne plus la voir (et donc avoir plus de place pour, par exemple, le code d'un fichier), il suffit de cliquer sur le trait horizontal en haut à droite de la zone. Un double clic affiche la zone sur tout l'espace dédié à NetBeans ; un nouveau double clic remet la zone à sa taille initiale. Ce type d'action fonctionne pour toutes les zones affichées par NetBeans. Vous pouvez fermer une des fenêtres, par exemple la fenêtre des projets, en cliquant sur la croix en haut à droite de la fenêtre ; pour rouvrir ensuite la fenêtre, utilisez le menu "Window" en haut de NetBeans.

L'onglet Services vous sera utile pour gérer le SGBD MySQL et le serveur d'application Payara :

Après la création du projet

Voyons rapidement le contenu de l'onglet Projects (après avoir déployé les entrées) :

Onglet Projects

Le projet créé contient des fichiers de configuration :

L'onglet Files (Menu Window > Files s'il n'est pas affiché) présente la structure des fichiers du projet. Vous ne verrez pas tous les fichiers de la copie d'écran ci-dessous, car ils seront écrits plus loin dans ce TP, mais vous verrez la plupart des répertoires :

Onglet Files

Si vous allez voir en dehors de NetBeans les fichiers qui constituent le projet, vous verrez que cet onglet Files montre exactement la structure des fichiers sur le disque dur.

Cette structure de fichiers est une des conventions définies par Maven :

A la racine du projet, NetBeans a ajouté nb-configuration.xml qu'il utilise pour son propre fonctionnement ; vous pouvez ignorer ce fichier.

L'onglet Project montre la structure "logique" du projet, pas la structure physique montrée par l'onglet Files. Par exemple, dans l'onglet Projects vous trouverez une entrée pour les dépendances du projet (les jar qu'il utilise)

Vous travaillerez le plus souvent avec l'onglet Projects.

Fichier pom.xml - Parenthèse sur Maven

Maven facilite la création et la gestion des projets Java. Nous n'aurons pas le temps de l'étudier en détails dans ce cours mais il vous sera sans doute nécessaire d'en connaître au moins les bases pour développer plus tard vos propres projets.

Le fichier pom.xml (Project Object Model) contient des informations sur le projet. Il est écrit par le développeur (ou généré par l'IDE) et il est utilisé par Maven pour gérer le projet, par exemple pour construire le projet (build), en particulier pour compiler les classes Java. Les dépendances qui n'existent pas déjà en local seront automatiquement téléchargées depuis l'entrepôt central de Maven. La plupart des librairies "classiques" utilisées par des projets Java sont enregistrées dans cet entrepôt central. Si une librairie n'est pas fournie par le serveur, elle est intégrée au fichier d'archive de l'application. Les librairies sont identifiées par les valeurs de groupId et de artifactId.

pom.xml contient plusieurs sections :

Remarque importante : si vous faites un copier/coller dans votre code, reformattez le code (menu Source > Format ou Alt-Maj-F). Pour avoir un code plus facile à lire, faites-le pour tout copier/coller, dans un fichier Java, XML, HTML ou autre. Si vous avez besoin d'aide et que vous envoyez du code pour vous faire aider, la moindre des choses est d'envoyer un code bien formatté.

Changer la version du compilateur

Cette version peut-être définie de plusieurs façons. Soit par une propriété, soit dans la configuration du plugin Maven de compilation.

Donnez la valeur "17" pour le source et le target. Le compilateur utilisera toutes les possibilités de Java 17 et produira du bytecode Java 17.

Propriétés du projet

Dans l'onglet Projects, clic droit sur le projet, et Properties pour voir les propriétés du projet.

Les propriétés qui vous seront le plus souvent utiles :

Pour vérifier si tout va bien, faites un clean and build du projet (clic droit sur le projet et Clean and Build). Une fenêtre "Output - Build" s'affiche. Vous pouvez avoir un message d'erreur, par exemple sur un des plugins utilisés par Maven si vous n'avez pas changé les versions des plugins.

N'oubliez pas de faire ces modifications pour les projets des autres TPs.

Si vous faites un clic droit sur le projet et choisissez Run, Payara est lancé (s'il ne l'était pas déjà) et une page doit s'ouvrir sur votre navigateur préféré avec l'affichage "Hello World!". Le lancement de Payara peut prendre un peu de temps. Un onglet pour Payara doit être ouvert en bas dans "Output". Cet onglet contient les logs de Payara et il sera très utile pour comprendre les éventuels bugs et problèmes lors du développement.

Une page s'ouvre dans le navigateur. Son URL (<context-path> est le nom affiché par clic droit sur le projet et Properties > Run dans le champ "Context Path") :
http://localhost:8080/<context path>/. C'est le contenu de la page Web index.html de votre projet qui est affiché (valeur par défaut si un nom de page n'est pas donné dana l'URL).

Si ce message ne s'affiche pas c'est que vous avez fait une erreur. N'allez pas plus loin dans le TP avant d'avoir réparé cette erreur.

Un peu de REST en action avant de passer à la suite.

Git

Git est inclus dans NetBeans.

Cette page décrit comment utiliser Git pour gérer les versions de votre code, et GitHub pour sauvegarder ces versions à distance. Vous devez appliquer ce qui y est dit avant d'aller plus loin.

Notez bien : Si vous voulez profiter des bonus accordés pour votre travail dans les TPs, vous devez effectuer tous les commits, à chacune des étapes importantes des TPs. Un projet GitHub sans ces commits ne pourra pas vous rapporter ces bonus.

Création de classes entités à partir d'une base de données existante

Vous allez utiliser la base de données relationnelle "customer" que vous avez définie pendant l'installation des logiciels.

Comme vous le verrez dans le cours sur JPA, vous ne manipulerez pas des données directement en SQL. Les données seront manipulées sous la forme d'objets particuliers qu'on appelle des entités, instances d'une classe entité. Dans les cas les plus simples, une classe entité correspond à une table, porte le même nom (aux majuscules/minuscules près), et ses attributs correspondent aux colonnes de la table. Une instances d'une classe entité correspond souvent à une ligne dans cette table.

La base de données sera gérée par le SGBD MySQL. MySQL doit être déjà démarré. Démarrez-le si ça n'est pas déjà fait (avec un service Windows, voir installation de MySQL).

Il existe un wizard de NetBeans pour générer des classes entités à partir de tables de la base de données "customer". Vous allez utiliser ce wizard pour générer les classes entités qui correspondent à la table CUSTOMER et aux 2 tables DISCOUNT et MICRO_MARKET qui y sont associées

Pour le lancer, clic droit sur le projet puis New > Other > Persistence > Entity Classes from Database. Cochez "Local Data Source" et désignez la connexion à la base "customer" que vous avez créée pendant l'installation des logiciels dans la section "Intégration de MySQL à NetBeans" de la page "MySQL". Les 3 tables customer, discount et micro_market doivent apparaitre dans la zone "Available Tables". Clic sur "customer" et ensuite sur "Add". Les 3 tables doivent apparaitre dans la zone "Selected Tables" car la case "Include Related Tables" est cochée. Clic sur Next.

Changez le nom du package. Donnez le nom du sous-paquetage "entity" du paquetage de base du projet : ajoutez ".entity" au nom proposé (par exemple, pour moi c'est fr.grin.tpcustomer.entity). Clic sur Next.

Clic sur Finish.

Vérifiez que les 3 classes entités ont bien été créées dans le package "entity" de votre projet.

[ Remarque : si vous n'arrivez pas à faire fonctionner ce wizard, ajoutez directement ces 3 classes entités dans votre projet et passez à la suite. ]

Etudiez le code des classes entités :

Modifications dans le code de l'entité Customer pour améliorer la lisibilité du code Java :

Regardez comment les attributs des classes correspondent aux colonnes des tables. Pour voir la structure de la base de données, cliquez sur l'onglet "Services" en haut à gauche (on a travaillé jusqu'à maintenant avec l'onglet "Projects") sous l'entrée de la connexion pour customer de Databases. Vous pouvez voir les données enregistrées dans les tables par un clic droit sur une table ; vous choisissez "View Data...".

Sous les noms des colonnes retrouvez aussi les colonnes clés étrangères dans l'entrée "Foreign Keys". Elles correspondent aux champs "associations" entre les entités.

Contenu des tables

Fichier persistence.xml

Faites afficher le contenu du fichier persistence.xml. Ce fichier est dans le projet, entrée Other Sources > src/main/resources > META-INF/persistence.xml. Son emplacement suit les conventions de Maven ; vous pouvez voir son emplacement dans l'onglet "Files" ; si cet onglet n'apparait pas, utilisez le menu Window de NetBeans pour le faire apparaitre.

Si vous faites un double clic sur l'entrée du fichier, vous pouvez le voir de 2 façons : Design (vue "logique" du fichier) ou Source (vue XML). Cliquez sur Source. Le contenu de persistence.xml devrait être (ça dépend de la version de NetBeans que vous utilisez) :

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="3.0" xmlns="https://jakarta.ee/xml/ns/persistence"   
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
             xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd">
  <!-- Define Persistence Unit -->
  <persistence-unit name="my_persistence_unit">

  </persistence-unit>
</persistence>

Modifiez le nom de l'unité de persistance, par exemple customerPU, à la place de my_persistence_unit.

Pour la suite NetBeans devrait vous aider à de nombreuses reprises en complétant ce que vous écrivez. Profitez-en, ce qui vous évitera des fautes de frappe.

Il faut ajouter l'information sur la source de données. Payara doit être démarré (clic droit sur Payara et Start sinon). Le nom de la source de données, qui correspond à la base de données customer et qui est déclarée dans le serveur Payara, se trouve dans l'onglet Services, entrée Servers pour Payara : ouvrez l'entrée Resources > JDBC > JDBC Resources. Clic droit sur la ressource "jdbc/customer" et Properties. On voit que cette ressource utilise le pool de connexions JDBC nommé mySQLPool. Les informations sur ce pool de connexions se trouvent dans les propriétés sous l'entrée "Connection Pool". On voit que la base de données est bien customer que l'on a utilisé pour générer les entités.

Il faut aussi ajouter l'information sur le fournisseur de persistance (provider) qui est EclipseLink (information fournie dans la documentation de EclipseLink), implémentation de JPA. Jakarta EE est un ensemble de spécifications qui doivent être implémentées dans des produits. Hibernate et Eclipse Link sont 2 implémentations de JPA.

Enfin, ajoutez la propriété non standard eclipselink.logging.level de EclipseLink. Le niveau FINE permet de voir les requêtes SQL générées par JPA, ce qui peut être bien utile en cas de mauvaises performances.

Le fichier persistence.xml devient :

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="3.0" xmlns="https://jakarta.ee/xml/ns/persistence"   
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
             xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd"">
  <!-- Define Persistence Unit -->
  <persistence-unit name="customerPU">
      <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
      <jta-data-source>jdbc/customer</jta-data-source>
      <properties>
          <property name="eclipselink.logging.level" value="FINE"/>
      </properties>
  </persistence-unit>
</persistence>

Sans entrer dans les détails :

  1. customerPU servira par la suite à indiquer dans quelle source de données on veut travailler ce qui pourrait être utile si on travaillait avec plusieurs sources de données. Ce nom désignera la source de données enregistrée sur le serveur d'application sous le nom JNDI "jdbc/customer".
  2. Cette unité de persistance va gérer les accès BD pour toutes les classes entités du projet. On pourrait aussi énumérer en dessous les classes entités qu'elle gère, ce qui serait indispensable si une application travaillait avec plusieurs sources de données.
  3. Des propriétés permettent de configurer l'unité de persistance, par exemple pour faire générer des tables relationnelles si elles n'existent pas déjà (on le fera dans les TPs sur JPA), pour générer des fichiers de log, ou pour voir les requêtes SQL générée par JPA (comme on l'a fait ici avec la valeur FINE).

Remarque importante : L'association entre le nom "jdbc/customer" et la base de données "customer" ne peut pas se voir uniquement en lisant le fichier persistence.xml. Si on veut la base de données, il faut aller dans l'entrée Servers > Payara > Resources > JDBC, clic bouton droit et Properties puis regarder les propriétés de mySQLPool dans "Connection Pools" sous JDBC.

Git

Faite un "Clean and Build" du projet.

S'il n'y a pas d'erreurs, commit du projet dans NetBeans :

  1. Clic sur le projet dans l'onglet Projects
  2. Menu Team > Commit...
  3. Une fenêtre pop-up s'ouvre. Ecrivez le message "Ajout des classes entités". Clic sur Commit.

Push sur GitHub :

  1. Menu Team > Remote > Push...
  2. La première option devrait être sélectionnée avec le bon nom de dépôt GitHub (origin:https://<votre nom GitHub>@github.com/....).
  3. Clic sur Next 2 fois sur les 2 fenêtres.
  4. Clic sur Finish.
  5. Vous pouvez vérifier que les entités sont bien dans le projet sur GitHub.

Pour la suite de ce TP et pour tous les TPs suivants, la section "Git" ne vous sera pas systèmatiquement rappelée mais, à chaque étape importante de l'écriture de l'application, utilisez Git pour garder une version :

  1. Commit local du projet.
  2. Push sur GitHub.

Création d'un Bean CDI CustomerManager pour la gestion des clients

On nomme bean CDI une instance d'une classe, que l'on peut injecter avec CDI dans une autre classe ou dans une page JSF. "Bean CDI" peut aussi désigner la classe de l'instance, suivant le contexte.

Dans une application Jakarta EE il est bon de séparer les tâches. On va centraliser la gestion des Customers dans un bean CDI. De manière classique on crée une "façade" pour les opérations élémentaires sur les clients, de type "CRUD" : création (Create), recherche (Research), modification (Update), suppression (Delete).

Quand il s'agit de gérer les données d'une base de données, on fait appel à des beans CDI avec des méthodes annotées par @Transactional, car elles ont des facilités pour travailler avec une base de données relationnelle, en délégant au serveur d'application le traitement des transactions.

Ajoutez une nouvelle classe au projet : clic droit sur le projet et New > Java Class. Donnez le nom de la classe CustomerManager et le paquetage (sous-paquetage "service" du paquetage de base du projet).

Ecrivez ce squelette de code pour la classe. N'oubliez pas d'ajouter un commentaire javadoc au début de toutes vos classes :

package xx.xxxxx.xxxxx.service; // A MODIFIER suivant le paquetage de base... 

import jakarta.enterprise.context.RequestScoped;

/**
 * Façade pour gérer les Customers.
 * @author xxxx
 */
@RequestScoped
public class CustomerManager {

}

L'annotation @RequestScoped indique qu'une instance de la classe sera un bean CDI (elle pourra être injectée dans une autre classe). Si elle est créée pendant le traitement d'une requête HTTP, elle sera supprimée quand la réponse à la requête sera retournée au client HTTP. La portée @SessionScoped aurait aussi pu être choisie pour indiquer que le bean CDI durera jusqu'à la fin de la session HTTP.

Complétion et importation avec NetBeans

Vous pouvez écrire la ligne pour importer l'annotation ou bien vous pouvez écrire d'abord l'annotation. NetBeans vous prévient alors d'un problème en affichant un rond rouge à gauche de la ligne. Si passez la souris sur ce point (sans cliquer), le message "cannot find symbol class RequestScoped" s'affiche. Si vous cliquez sur le rond rouge, NetBeans vous propose des solutions pour résoudre le problème. Cliquez sur "Add import for jakarta.enterprise.context.RequestScoped".

Une autre façon de faire : tapez "@Requ" et Ctrl-Barre espace. NetBeans vous propose alors de compléter. Faites un double clic sur RequestScoped ou descendez dessus avec les flèches, et [Enter].

Vous pouvez aussi écrire @RequestScoped et taper Ctrl-Shift-I (ou touche commande-Shift-I pour les Mac) pour que NetBeans vous propose des importations (vous pouvez aussi faire un clic droit et fix imports). S'il n'y a pas plusieurs propostions, les importations sont directement écrites.

Attention à ne pas aller trop vite ! Si la fenêtre "Fix All Imports" s'affiche, c'est qu'il peut y avoir plusieurs possibilités pour certains imports, et le choix proposé en premier peut ne pas être le bon.

Ajout de méthodes liées à JPA dans CustomerManager

Ajoutez 2 méthodes au bean CDI, comme dans le code ci-dessous :

L'annotation @Transactional de la méthode update est obligatoire car la méthode va modifier des données dans la base de données et toute modification doit être obligatoirement faite dans une transaction. Dans certaines conditions il peut arriver d'avoir à annoter une méthode qui correspond à un "select" dans la base de données, comme la méthode getAllCustomers(), mais ça ne sera pas nécessaire pour ce TP.

Remarque importante pour toutes vos classes : lorsque vous écrivez votre code, et en particulier si vous faites un copier/coller pour ajouter du code, n'oubliez pas de bien respecter les indentations des lignes préconinées par le langage Java. Avec un IDE c'est très simple car une commande permet d'indenter automatiquement une classe. Avec NetBeans, il suffit de cliquer sur le menu "Source" et de choisir "Format" ; le raccourci clavier est Alt-Shift-F. Lorsque les classes de vos projets seront examinées, n'obligez pas le correcteur à le faire pour vous, ça va le rendre de mauvaise humeur ! ;-)

@RequestScoped  
public class CustomerManager {  

    public List<Customer> getAllCustomers() {  
      return null;  
    }  

    @Transactional
    public Customer update(Customer customer) {
      return null;  
    }          
}

Faites les importations automatiquement comme décrit ci-dessus. Remarquez que les importations ne sont pas écrites tout de suite car il y a plusieurs possibilités pour "List". Cliquez "OK" car java.util.List convient. Sinon, il suffit de faire un autre choix dans la liste déroulante avant de cliquer sur "OK".

Vous devriez avoir un code source comme celui-ci :

package xx.xxxxx.xxxxx.service; // A MODIFIER suivant le paquetage de base...  
        
import xx.xxxxx.xxxxx.entity.Customer;
import java.util.List;
import jakarta.enterprise.context.RequestScoped;
import jakarta.transaction.Transactional;

@RequestScoped
public class CustomerManager {

    public List<Customer> getAllCustomers() {
      return null;
    }

    @Transactional
    public Customer update(Customer customer) {
      return null;
    }
}

Maitenant vous allez modifier le contenu de ces méthodes. Les explications détaillées seront vues dans le cours sur JPA.

Commencez par injecter un entity manager (gestionnaire d'entités) JPA. C'est un objet lié à l'unité de persistance référencée par le fichier persistence.xml. Il va servir à envoyer des requêtes, insérer/supprimer/modifier des données dans la base de données, en utilisant des instances des classes entités que vous avez créées au début de ce TP.

Au début du corps de la classe, ajoutez l'injection de l'EntityManager (c'est l'annotation @PersistenceContext qui provoquera l'injection) et une méthode persist pour ajouter un nouveau customer dans la base de données, avec les valeurs contenues dans l'entité passée en paramètre :

@PersistenceContext(unitName = "customerPU")
private EntityManager em;

@Transactional
public void persist(Customer customer) {
  em.persist(customer);
}

La variable em ne doit pas être initialisée car sa valeur va être "injectée" par le serveur d'application. C'est comme si le code disait "Serveur d'application, donne-moi un objet pour gérer les données de la base customer.".

L'annotation et la classe sont dans le paquetage jakarta.persistence.

"customerPU", correspond au nom donné dans le fichier persistence.xml ; il correspond à l'unité de persistance qui représente la source de données de nom JNDI "jdbc/customer". Cette source de données correspond à la base de données "customer" de MySQL (dans l'installation des logiciels vous avez indiqué cette correspondance lorsque vous avez défini le pool de connexions sur lequel s'appuie la ressource JDBC). Comme c'est la seule unité de persistance définie dans persistence.xml, vous auriez pu écrire plus simplement

@PersistenceContext
private EntityManager em;

Modifiez getAllCustomers et update pour qu'elles jouent leur rôle :

Si vous tapez le code sans faire de copier/coller, vous remarquerez que NetBeans vous fait des propositions à chaque fois que vous tapez un "." (s'il connait l'objet à qui est envoyé le message). Essayez avec la méthode getAllCustomers.

package xx.xxxxx.xxxxx.service; // A MODIFIER suivant le paquetage de base...

import xx.xxxxx.xxxxx.entities.Customer;
import java.util.List;
import jakarta.enterprise.context.RequestScoped;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.transaction.Transactional;

/**
 * Gère la persistance des Customers.
 */
@RequestScoped
public class CustomerManager {

    @PersistenceContext(unitName = "customerPU")
    private EntityManager em;

    public List<Customer> getAllCustomers() {
       Query query = em.createNamedQuery("Customer.findAll");
       return query.getResultList();
    }

    @Transactional
    public Customer update(Customer customer) {
       return em.merge(customer);
    }

    @Transactional
    public void persist(Customer customer) {
       em.persist(customer);
    }
}

Importez la classe jakarta.persistence.Query (utilisée dans getAllCustomers). Ne vous trompez pas de paquetage.

N'oubliez pas d'ajouter un commentaire javadoc sur la classe. Il est bon d'ajouter un commentaire javadoc à toutes les classes d'un projet, ainsi qu'à chaque méthode dont le code n'est pas évident à comprendre. Vous pouvez d'ailleurs le faire pour les 3 entités, si ça n'est déjà fait.

Voilà, on a terminé pour la partie bean CDI transactionnel (processus métier et accès aux bases de données) dans ce projet. On va passer au front-end (interface utilisateur) web.

Git

Clean and Build du projet.

Si tout va bien, commit avec le message "Ajout CustomerManager" et push sur GitHub.

Présentation de la partie front-end web

Dans cette partie on va utiliser JSF, le framework standard MVC de Jakarta EE, qui se base sur l'utilisation de backing beans et de pages JSF écrites avec des composants.

Une page JSF est en gros une page HTML avec des balises HTML et des balises qui représentent des composants JSF similaires aux composants HTML (par exemple une liste déroulante ou une table) ou plus complexes (par exemple un calendrier ou un arbre). Elle peut avoir des emplacements délimités par #{ ... }, qui contiennent des expressions "EL".

EL (Expression Language) est un langage qui permet de lier des propriétés ou méthodes de classes Java à une page JSF. Une expression EL peut désigner une propriété d'un backing bean dont la valeur est affichée dans la page ou dont la valeur est saisie par l'utilisateur dans un formulaire. Une propriété d'un Java Bean est définie par un getter et/ou par un setter ; par exemple la propriété "nom" sera définie par les méthodes String getNom() et/ou void setNom(String nom). Une expression EL peut aussi désigner une méthode du backing bean qui sera exécutée à la suite d'une action de l'utilisateur (par exemple quand l'utilisateur soumet un formulaire).

Une page JSF ne contient pas de code Java. Pour exécuter du code elle utilise un (ou plusieurs) backing bean, instance d'une classe Java. Un backing bean (backing = épauler, aider, en anglais) s'appelle aussi un bean géré (managed bean) car il est géré par le container CDI du serveur d'application. C'est le container CDI qui le crée automatiquement et l'injecte quand l'application en a besoin (injection, comme pour l'EntityManager qu'on a vu plus haut dans le bean CDI). C'est aussi le container qui le supprime quand il le faut (durée de vie liée à la portée du backing bean) comme on le verra dans les cours sur CDI et JSF.

Un container CDI peut aussi gérer d'autres instances Java, indépendantes d'une page JSF. Une classe Java peut demander à CDI d'injecter une instance d'une autre classe Java. Par exemple, des instances de CustomerManager seront injectées dans des backing beans.

L'application aura 2 pages JSF : 

Dans customerList.xhtml un lien placé dans chaque ligne de la liste des clients permettra de lancer une requête HTTP GET pour faire afficher les détails sur le client concerné.

Configuration pour JSF

Pour activer JSF, vous allez ajouter une classe CDI et l'annoter avec @FacesConfig. Il y a d'autres moyens d'activer JSF. Une telle classe de configuration permet d'ajouter d'autres configurations, par exemple pour la sécurité.

Ajoutez donc une nouvelle classe ConfigJSF que vous placez dans le sous-paquetage config du paquetage de base du projet.

L'annotation CDI @ApplicationScoped permet d'indiquer que la classe sera gérée par CDI et qu'une instance de la classe durera tout le temps que l'application s'exécutera.

package xx.xxxxx.xxxxx.config; // A MODIFIER suivant le paquetage de base...

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.faces.annotation.FacesConfig;

/**
 * Configuration JSF
 * @author xxxx
 */
@ApplicationScoped
@FacesConfig
public class ConfigJSF {
}
Pour ajouter la classe vous pouvez faire un clic droit sur "Source Package" et choisir New > "Java Class...".

Création d'un bean géré par CDI (backing bean de la page customerList.xhtml)

Vous allez commencer par écrire le backing bean CustomerBean, qui sera utilisé par la page JSF customerList.xhtml.

Par exemple, pour afficher la liste des customers elle aura besoin d'aller chercher les données dans la base de données. Ce backing bean aura une méthode getCustomers() pour retourner la liste de tous les clients (ce "getter" définira la propriété customers).

Le backing bean sera géré par le container CDI du serveur d'application, qui pourra le créer automatiquement, en particulier quand il sera référencé par une page JSF, par exemple si la page JSF contient l'expression EL #{customerBean.customers} qui référence la propriété customers du bean. Quand la valeur de la propriété customers devra être lue, la méthode getCustomers() sera appelée. customerBean est le nom JSF du backing bean qui est donné par l'annotation @Named de la classe du backing bean.

Pour créer ce backing bean vous allez utiliser un "wizard" de NetBeans.

Sur le projet faites clic droit et New > Other ; dans la catégorie "JavaServer Faces" choisissez "JSF CDI Bean" :

New CDI bean

Ensuite, clic sur Next et renseignez :

customerBean.png

Clic sur Finish.

Le code de la classe CustomerBean devrait être le code ci-dessous. Remarquez l'annotation @Named qui donne le nom CDI qu'il faudra utiliser dans une page JSF pour désigner ce backing bean dans une expression EL : #{customerBean.customers} désignera la propriété customers du backing bean. "@Named" aurait suffi plus simplement puisque "customerBean" est le nom donné par défaut au backing bean (le nom de la classe, avec une minuscule au début).

La portée "view" oblige à implémenter Serializable car le bean pourra être sérialisé en mémoire secondaire si le container l'estime nécessaire. Une plus courte portée (comme la portée requête que vous avez déjà utilisé pour CustomerManager), donc une plus courte durée de vie, ne l'imposerait pas.

Ajoutez un commentaire Javadoc : "Backing bean pour la page JSF customerList."

Pour écrire la méthode getCustomers(), vous allez ajouter du code pour que le bean puisse communiquer avec le bean CDI CustomerManager déjà écrit. Insérez les lignes suivantes au début du corps de la classe :

@Inject  
private CustomerManager customerManager;

L'annotation @Inject permettra d'injecter une instance de la classe CustomerManager quand le backing bean sera créé. Vous n'avez pas à faire de "new" (vous ne DEVEZ PAS faire de "new" !) puisque que c'est le serveur d'application qui va fournir une instance de CustomerManager. Le cours vous donnera plus de détails sur ce mécanisme fondamental d'injection de dépendance.

Vous allez compléter la classe du bean :

package xx.xxxxx.xxxxx.jsf; // A MODIFIER suivant le paquetage de base...
        
import xx.xxxxx.xxxxx.entity.Customer;  
import jakarta.inject.Inject;  
import jakarta.inject.Named;  
import jakarta.faces.view.ViewScoped;  
import java.io.Serializable;  
import java.util.List;  
import xx.xxxxx.xxxxx.service.CustomerManager;

/**
 * Backing bean de la page customerList.xhtml.
 */
@Named(value = "customerBean")  
@ViewScoped  
public class CustomerBean implements Serializable {  
  private List<Customer> customerList;  

  @Inject
  private CustomerManager customerManager;  
        
  public CustomerBean() {  }  
        
  /** 
   * Retourne la liste des clients pour affichage dans une DataTable.
   */  
  public List<Customer> getCustomers() {
    if (customerList == null) {
      customerList = customerManager.getAllCustomers();
    }
    return customerList;
  }  
}

Modification du fichier web.xml

Vous trouverez web.xml dans l'onglet des projets, en ouvrant les entrées Web Pages > WEB-INF.

Le contenu du fichier web.xml généré diffère suivant les versions de NetBeans.

Si ce fichier n'a pas été généré, vous le créez : clic droit sur le projet > New > Other... > Web > Standard Deployment Descriptor (web.xml). Ensuite, vous le modifier pour avoir le code donné ci-dessous.

Il peut indiquer que

Modifier ce fichier pour que

Le fichier web.xml devient

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
   version="6.0">
  <context-param>
      <param-name>jakarta.faces.PROJECT_STAGE</param-name>
      <param-value>Development</param-value>
  </context-param>

  <!-- Faces Servlet -->
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>jakarta.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>*.xhtml</url-pattern>
</servlet-mapping> <session-config> <session-timeout> 30 </session-timeout> </session-config> <welcome-file-list> <welcome-file>customerList.xhtml</welcome-file> </welcome-file-list> </web-app>

Ajout d'une page JSF pour afficher la liste des clients

Pour ajouter une page JSF, sur le projet web faire clic droit et New > Other ; dans la catégorie "JavaServer Faces" choisir "JSF Page" :

Créer page JSF 

Puis Cliquez "Next" et renseignez le nom de la page (pas de .xhtml final ; il sera rajouté par NetBeans) qui est cutomerList.xhtml (attention, la copie d'écran ci-dessous a un C majuscule au lieu du c minuscule qui sera utilisé dans la suite du TP) :

Nom page JSF

Notez que cela ajoute un fichier customerList.xhtml dans le projet, à côté de la page index.html. Double clic pour voir le source. Modifiez la valeur de <title> pour mettre "Liste des clients" (ce qui sera affiché dans l'onglet de la page dans le navigateur).

Remarque : La balise <html> définit 2 espaces de noms. L'espace de noms par défaut (pas d'alias) correspond à HTML. L'alias "h" correspond à un espace de noms de JSF, utilisé pour les composants JSF qui correspondent plus ou moins aux balises HTML (par exemple <h:body> dans le code généré).

Ajout d'une DataTable JSF dans la page

Vous allez faire afficher les informations sur les customer dans un composant de type "datatable" dans la page customerList.xhtml. Le composant datatable est un composant standard fourni avec JSF.

Ajoutez le code d'une table dans le body de la page JSF. Cette table a 2 colonnes pour l'id et le nom des customers.

Voici le code de la table :

  <h:dataTable value="#{customerBean.customers}"
               var="item">
      <h:column>
         <f:facet name="header">
             <h:outputText value="Id"/>
         </f:facet>
         <h:outputText value="#{item.customerId}"/>
      </h:column>

      <h:column>
         <f:facet name="header">
             <h:outputText value="Nom"/>
         </f:facet>
         <h:outputText value="#{item.name}"/>
      </h:column>
  </h:dataTable>

Un point rouge devrait s'afficher à gauche de la ligne pour le header des colonnes car vous n'avez pas déclaré l'alias "f". Ajoutez sa définition dans la balise <html> :

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core">

Normalement vous auriez pu cliquer sur le point rouge et ajoutez l'alias XML, ou tapez Ctrl-Shit-i, comme pour les importations dans une classe, mais cet alias a été modifié récemment pas Jakarta EE et il n'est pas encore pris en compte dans NetBeans.

Ignorez les éventuels avertissements pour <title> ou </html>.

L'attribut value de la table désigne ce qui va être affiché dans la table. La valeur de cet attribut est une expression EL qui utilise la propriété du backing bean customers. Ici cette propriété correspond à la méthode getCustomers() (on veut lire une valeur, donc c'est le getter qui est utilisé) qui va retourner la liste de tous les customers. L'attribut var donne le nom de la "variable de boucle" (comme dans une boucle "for") qui désigne une instance de la liste et donc les valeurs affichées dans une ligne de la table. Par exemple, la valeur #{item.name} désigne le nom de l'employé de la ligne.

Une table contient des colonnes (<h:column>) . Pour chaque ligne de la liste fournie à la table (une liste de Customer), toutes les valeurs des colonnes seront affichées (customerId et name de chaque Customer).

Chaque colonne a un en-tête (<f:facet name="header">) qui contient le titre affiché en haut de la colonne. La valeur de la colonne est affichée avec une balise <h:ouputText> qui contient une expression EL.

Le composant standard <h:ouputText> permet d'afficher une valeur dans la page. A la place de <h:outputText value="Id"/> on aurait pu écrire plus simplement Id. Ce composant standard a des attributs qui peuvent être utiles dans certains cas.

Exercice

Pour voir si vous avez compris, ajoutez une colonne pour la ville dans cette table.

Ajouter aussi la propriété "discount" de l'entité, qui correspond à une association entre les customers (clients) et les discounts (réductions à appliquer sur les montants des factures de certains clients).

Exécution du projet

Commencez par démarrer MySQL si ça n'est pas déjà fait.

Vous pouvez choisir le navigateur Web qui sera utilisé pour l'affichage des pages Web du projet : clic droit sur le projet et Properties. Clic sur Run et choisir le browser.

Commencez par un clean and build du projet.

S'il n'y a pas d'erreurs, clic droit sur le projet et Run. Le lancement est long pour la première exécution car il faut lancer le serveur d'application Payara.

IMPORTANT : Remarquez l'ouverture de l'onglet Payara dans la fenêtre "Output" du bas. Cette fenêtre est importante car elle affiche les logs de Payara et elle est très utile pour la mise au point de l'application. Si rien ne s'affiche dans votre navigateur ou s'il s'affiche un message d'erreur, il vous faut chercher les erreurs dans cet onglet.

Avant chaque nouvelle exécution il est conseillé d'effacer le contenu de l'onglet "Payara" (Clic bouton droit et Clear, ou Ctrl-L dans le contenu de l'onglet) pour que les nouvelles erreurs éventuelles soient plus faciles à repérer.

Ce qu'il se passe quand vous lancez l'exécution de l'application :

  1. Le navigateur que vous avez choisi est lancé, s'il ne l'était pas déjà.
  2. L'URL lié à l'application est fourni par NetBeans à ce navigateur : localhost:8080/tpCustomer/.
    localhost
    , car l'application est démarrée sur un serveur d'application (Payara) hébergée par votre ordinateur ; 8080 est le port sur lequel le serveur d'application écoute ; tpCustomer est le chemin du contexte de l'application qui désigne l'application à Payara (un serveur d'application peut gérer plusieurs applications).
    Dans l'URL aucun chemin ne suit le contexte de l'application et donc le chemin indiqué dans la balise <welcome-file-list> est ajouté pour savoir quelle page afficher (customerList.xhtml pour ce cas).
  3. Une requête HTTP GET est donc lancée, avec l'URL localhost:8080/tpCustomer/customerList.xhtml.
  4. L'URL correspond au pattern "*.xhtml", et donc le servlet Faces (lié à JSF) est utilisé. Il lance le cycle de vie JSF (sera étudié dans le cours). Le cycle de vie de la requête GET utilise les expressions EL de la page JSF (en particulier ceux de la table JSF) pour générer le code de la page HTML retournée en réponse à la requête GET. La liste des customers est donc affichée.

Remarque : Il est possible de modifier le contexte de l'application, mais pas de façon standard (dépend du serveur d'application).

Vous devriez obtenir un affichage du type suivant :

Liste des Customers

Pour le moment cette table n'est pas très bien présentée car il n'y a aucune fioriture de mise en page et l'affichage du code de réduction n'est pas ce qu'on peut attendre d'une application professionnelle ; on va arranger ça dans la suite du TP.

Remarque : Si vous modifiez le code de la page (par exemple en ajoutant "Liste des clients" comme titre de niveau 1 avant votre table) et que vous sauvegardez la page, il suffit de recharger la page dans le navigateur pour voir la modification. C'est parce que, dans les propriétés du projet affichées par clic droit sur le projet et "Properties > Run", la case "Deploy on Save" est cochée. Parfois il faut quand même relancer l'application car les modifications sont telles que la page ne peut pas être réaffichée.

Vous n'avez pas oublié Git ?...

Les données proviennent de la base dont le nom JNDI est "jdbc/customer". Vous pouvez vérifier que ces données sont les bonnes, allez dans l'onglet "Services" de NetBeans qui comprend un gestionnaire de base de données assez simple, mais très pratique. Ouvrez Databases > jdbc:mysql://localhost:3306/customer?zeroDateTimeBehavior=CONVERT_TO_NULL&useTimezone=true&serverTimezone=UTC [root on Default schema] (Remarque : le nom de l'entrée peut être différent mais le début sera le même "jdbc:mysql://localhost:3306/customer?...). Clic droit sur l'entrée et "Connect". Ensuite, ouvrez la base de données "customer" et clic droit sur la table "customer" et choisissez "View Data..." :

Voir les données des tables

Vous pouvez vérifier que ce sont bien les mêmes données qui sont affichées dans la page JSF.

Améliorer l'affichage pour discount

Remarquez l'affichage particulier pour discount. Cette colonne correspondent à une clé étrangère relationnelle de la table CUSTOMER vers la table DISCOUNT. Pour le vérifier, allez voir le code de l'entité Customer ou bien directement la section "Foreign Keys" de la table relationnelle CUSTOMER. Vous allez améliorer cette présentation.

Si vous avez compris le concept de "propriétés", vous pouvez modifier la ligne qui affiche le code. remplacez :

<h:outputText value="#{item.discount}"/>

par :

<h:outputText value="#{item.discount.code} : #{item.discount.rate} %" />

Explications :

Sauvegardez vos modifications et faites un reload de la page JSF pour voir le nouvel affichage.

L'affichage sera meilleur :

Affcihage discountcode

L'affichage ci-dessus vous semble plus agréable que ce que vous voyez ? C'est normal car il provient d'une table PrimeFaces et pas d'une table standard JSF. C'est ce que vous allez obtenir dans la suite du TP.

Optionnel

Le problème est le même pour la propriété "zip" de Customer. Après avoir fini le TP, pour voir si vous avez bien compris, vous pourrez rajouter une colonne pour "zip" dans la table en refaisant sur cette colonne un travail semblable à ce qui a été fait pour discount.

Déploiement d'une application (section optionnelle)

Avant d'ajouter PrimeFaces, voyons ce qui se passe au moment du déploiement de l'application sur le serveur d'application. Le plus souvent ce serveur est sur un autre ordinateur mais, pendant le développement, un serveur local est souvent utilisé, et ça sera votre cas.

Ouvrez l'onglet "Files" dans la fenêtre en haut à gauche de NetBeans. Vous retrouvez la structure standard des applications Maven en développement.

Intéressons-nous au répertoire target. Il contient

C'est le fichier war qui sera déployé sur le serveur d'application.

Le répertoire du nom de l'application peut contenir un sous-répertoire WEB-INF/lib qui contient les librairies utilisées par l'application, qui ne sont pas déjà sur le serveur d'application. Dès que vous allez ajouter PrimeFaces au projet, il y aura le fichier jar pour PrimeFaces dans ce répertoire (et aussi dans le fichier war).

Vous déploierez vos applications directement depuis NetBeans (car vous avez intégré Payara à NetBeans).

Il est aussi possible de les déployer depuis le serveur d'application, par exemple en utilisant la console d'administration de Payara. La console d'administration de Payara sert à administrer Payara, en particulier à gérer les applications (entrée "Applications"). Si Payara est déjà démarré, on peut la faire afficher avec l'onglet Services > Servers, clic droit sur Payara et choix "View Domain Admin Console". On peut aussi, tout simplement, taper l'URL localhost:4848 dans un navigateur. Avec la console d'administration on peut, par exemple, changer l'URL associé à l'application : ouvrir l'entrée "Applications", puis clic sur l'application et changer la valeur de "Context Root".

[ Encore plus optionnel... : Quand on déploie une application depuis NetBeans sur un serveur Payara qui est situé sur le même ordinateur, Payara utilise directement les fichiers du répertoire target. Vous pouvez aller voir le répertoire payara6\glassfish\domains\domain1\applications\__internal du répertoire d'installation de Payara et vous y trouverez un répertoire associé à l'application que vous venez de déployer, mais ce répertoire est vide (puisque Payara va utiliser directement les fichiers du répertoire target). Si vous allez dans l'onglet Services, puis Server > Payara (ce nom dépend du nom que vous avez donné) > Applications, clic droit sur le nom de l'application et Undeploy, vous verrez que le répertoire vide sera supprimé. ]

On pourrait aussi déployer sur un serveur Payara distant et, dans ce cas, il faudrait commencer par faire un upload du fichier war de target sur l'ordinateur qui héberge le serveur. [ Optionnel : Au moment du déploiement, les fichiers du fichier war sont décompactés dans un répertoire du serveur, sous le répertoire payara6\glassfish\domains\domain1\applications (cette fois-ci, ce répertoire n'est pas vide car il contient les fichiers de l'application). ]

Utilisation d'une DataTable provenant de la librairie de composants JSF PrimeFaces

Ajout de la librairie PrimeFaces dans le projet

PrimeFaces propose des composants évolués/complémentaires pour JSF, le site web de référence est : http://www.primefaces.org.

Vous allez l'utiliser pour ajouter simplement de nouvelles fonctionnalités à vos pages JSF et pour améliorer la présentation des pages.

Toutes les versions de PrimeFaces ne sont pas gratuites. Par exemple à la date de l'écriture de ces lignes la dernière version gratuite est la version 13.0.0 ; la version 13.0.1 ne sera pas gratuite. Après 13.0.0, la prochaine version gratuite sera 13.1.0. La version 13.1.1 ne sera pas gratuite. Utilisez donc la dernière version gratuite dans vos projets.

Pour ajouter la librairie PrimeFaces au projet, il suffit d'ajouter une dépendance dans le fichier pom.xml. Indiquez la dernière version gratuite de PrimeFaces ; NetBeans vous aide en affichant les versions disponibles sur le dépôt central Maven si vous tapez Ctrl-espace entre <version> et </version>. Par exemple :

<dependency>
   <groupId>org.primefaces</groupId>
   <artifactId>primefaces</artifactId>
   <version>13.0.0</version>
   <classifier>jakarta</classifier>
</dependency>

Remarque : <classifier> est indispensable pour avoir une version de PrimeFaces adaptée aux dernières versions de Jakarta.

Cette librairie sera téléchargée par Maven depuis l'entrepôt central. Vous devez être connecté à Internet et la vitesse de transfert dépend de l'état de votre réseau.

Faites un build du projet pour voir si tout va bien (clic droit sur le projet > Clean and Build).

[ Remarque pour la suite : Dans les builds suivants, si vous avez une erreur disant que le fichier jar de PrimeFaces ne peut être supprimé, c'est que vous avez sans doute déjà déployé le projet sur le serveur et Windows refuse de supprimer un fichier utilisé par une application. En ce cas, il suffit de faire un undeploy du projet :

  1. Allez dans l'onglet Services de NetBeans (en haut à gauche).
  2. Ouvrez l'entrée qui correspond à votre serveur d'application sous l'entrée "Servers".
  3. Ouvrez l'entrée Applications et faites clic droit > Undeploy sur l'application que vous voulez supprimer du serveur.

Après ce undeploy, Clean and Build devrait marcher.

]

Modification de la page JSF

Pour pouvoir utiliser des tags provenant de PrimeFaces dans une page JSF il faut ajouter le namespace suivant : (cf https://www.primefaces.org/gettingstarted/)

xmlns:p="http://primefaces.org/ui"

Remarque : vous pouvez aussi laisser NetBeans ajouter cet espace de nom : remplacer <h:dataTable par <p:dataTable (et idem pour la balise fermante). NetBeans vous signale une erreur et, si vous cliquez sur le point rouge de l'erreur, il vous propose d'ajouter un espace de noms en vous donnant le choix entre 2 espaces de noms ; choisissez l'espace de noms de PrimeFaces.

A partir de là on pourra utiliser des tags PrimeFaces avec le préfixe p:

Redéployez le projet pour que PrimeFaces fasse partie du projet déployé (clic droit sur le projet et "Run") . Un simple "Run" devrait suffire mais, si ça ne marche pas, lancez un "Clean and Build" avant.

Si vous avez un message vous disant que le jar de PrimeFaces a un format zip invalide, faites un "Clean and build" du projet principal avant de le relancer et tout devrait alors fonctionner.

Remarque : si vous êtes gêné par des "warnings" du type "WARNING: JSF1091 : Aucun type mime détecté pour le fichier primeicons/primeicons.eot. ..." dans la fenêtre des logs du serveur d'application, ajoutez ces lignes à la fin de votre fichier web.xml (juste avant </web-app>). Il faut relancer l'application par Run pour que les messages disparaissent.

Le format de la table peut changer suivant le goût des développeurs de PrimeFaces. Si vous connaissez CSS vous pouvez aussi modifier la présentation. Vous devriez obtenir ce type d'affichage :

Liste avec PrimeFaces

C'est déjà mieux présenté non ? Ce qui est surtout intéressant, c'est que le composant <p:dataTable> de PrimeFaces possède de nombreuses options pour la pagination, rendre les colonnes triables, éditables, etc.

Vous allez utiliser quelques unes des possibilités des tables PrimeFaces. Pour vous aider vous pouvez consulter la documentation de PrimeFaces. Cliquez sur le "user guide" de la version que vous utilisez, puis sur "Get Started" et descendez dans la page jusqu'à la section "Components" dans le menu de gauche et cliquez sur "DataTable". Vous pouvez aussi aller voir les démos. Le code est fourni ; cliquez sur DataTable dans la section Data dans le menu de gauche. Après chaque modification, rechargez la page pour tester.

Il vous faut commencer par insérer la table dans un formulaire JSF pour profiter de toutes ces fonctionnalités :

<h:form>
   <p:dataTable value="#{customerBean.customers}">
               ...
   </p:dataTable>
</h:form>

Remettez en forme le code de la page en tapant Alt-Shift-F.

Voici quelques modifications à faire (remarquez la complétion de NetBeans dans les expressions EL, avec Ctrl+[espace]) :

  1. Ajoutez dans le tag <p:dataTable ...> les attributs paginator="true" et rows="10", sauvez, rechargez la page, ça y est, les résultats sont paginés. Remarquez que PrimeFaces est connu de NetBeans et vous pouvez utiliser la complétion de code ; par exemple, tapez pag et Ctrl-espace et paginator vous est proposé.
  2. Faites en sorte que les lignes soient triables par nom : attribut sortBy="#{item.name}" dans le tag p:column de la colonne des noms. Attention, pour que le tri fonctionne, le backing bean doit être de portée "vue" et la méthode qui récupère les valeurs à afficher dans la page doit utiliser un champ qui contient la liste des objets affichés par la table, ce qui est bien le cas avec le champ customerList. Testez.
  3. Exercice à faire : Ajoutez une colonne pour l'état et permettez le tri sur l'état et la ville. Faites en sorte que le tri sur l'état trie aussi par ville dans chaque état.
  4. Exercice à faire : Installez un filtre (filterBy) sur la colonne pour l'état et sur la colonne pour le nom, ce qui permettra à l'utilisateur de sélectionner seulement les clients d'un état ou d'un certain nom (par défaut, la sélection est effectuée sur le début de la valeur). Pour avoir des exemples d'utilisation de filterBy, allez dans la démo de cette page.

Compléments : voici le code du début d'une table avec quelques options supplémentaires pour la pagination dans les attributs de la balise <p:dataTable>.

Git s'il n'y a pas d'erreurs...

Affichage des détails d'un client lorsqu'on clique sur une ligne

Maintenant nous allons voir comment afficher dans une autre page les détails d'un client lorsqu'on clique sur l'id du client dans une ligne de la table.

Ajout d'un lien dans la table pour déclencher l'affichage des détails d'un client

Modifiez la page customerList.xhtml de manière à ce que lorsqu'on clique sur une valeur de la colonne Id on affiche le détail d'un client. Pour cela, remplacez la balise <h:ouputText> par la balise <h:link> , comme dans le listing ci-dessous.

Pour vous faire découvrir 2 possibilités de PrimeFaces :

<p:dataTable value="#{customerBean.customers}" var="item" widgetVar="customerTable"
             emptyMessage="Aucun client avec ce critère"
             paginator="true"
             rows="10"
             rowsPerPageTemplate="2,4,8,10"
             paginatorTemplate="{CurrentPageReport} {FirstPageLink} {PreviousPageLink} {PageLinks} {NextPageLink} {LastPageLink} {RowsPerPageDropdown}">
          
  <f:facet name="header">
     <p:outputPanel>
        <h:outputText value="Recherche dans tous les champs de recherche"/>
        <p:inputText id="globalFilter" onkeyup="PF('customerTable').filter()"
                     style="width:150px"/>
     </p:outputPanel>
  </f:facet>
  
  <p:column>
     <f:facet name="header">
        <h:outputText value="CustomerId"/>
     </f:facet>
     <h:link outcome="customerDetails?idCustomer=#{item.customerId}" 
             value="#{item.customerId}"/>
  </p:column>

  ...   

La ligne modifiée (<h:link> à la place de <h:outputText>) ajoute un lien hypertexte dans la colonne ; le texte du lien sera l'id du client (attribut "value=...") et lorsque l'utilisateur cliquera sur la colonne, une requête GET (<h:link> envoie une requête GET) sera envoyée au serveur pour afficher la page customerDetails.xhtml, avec l'id du client en paramètre. Par exemple "customerDetails?idCustomer=2" si l'id du client est 2. JSF ajoutera automatiquement le suffixe ".xhtml" au fichier et ajoutera au début l'adresse de l'application "http://localhost:8080/..."). On verra dans le cours et les TPs suivants d'autres moyens d'ajouter des paramètres à une requête GET.

Si vous sauvegardez la page JSF et lancez l'exécution de l'application, vous verrez que la colonne id affiche des erreurs car la page customerDetails.xhtml n'existe pas encore. Remarque : ces messages d'erreur apparaissent parce que vous êtes en mode "développement de projet" (voir fichier web.xml). Attention, si vous étiez en mode "production", ils n'apparaitraient pas parce que vous n'avez pas encore ajouté explicitement dans la page une balise pour indiquer où afficher les messages d'erreur. Testez en mode "Production" et revenez ensuite en mode "Development" en modifiant le fichier web.xml.

Vous pouvez créer une page customerDetails.xhtml vide ou bien attendre la suite du TP qui va montrer comment la créer, mais avant, on va commencer par ajouter son backing bean (pour profiter d'une facilité de NetBeans pour générer du code dans la page JSF).

Ajout d'un backing bean pour la page des détails

La classe CustomerDetailsBean de ce bean contient

La portée du backing bean aurait pu être Request si on avait voulu seulement afficher les données du client (vous pourrez vérifier si vous voulez en remplaçant @ViewScoped par @RequestScoped). A la fin de ce TP la page customerDetails.xhtml qui utilise ce backing bean servira aussi à modifier les propriétés du client ce qui oblige à mettre la portée View.

Vous ajoutez ce backing bean customerDetailsBean comme vous avez fait pour le backing bean de la page customerList.xhtml (pour aller plus vite vous pouvez faire un clic droit directement sur le package dans lequel sera le backing bean).

Voici le code :

package xx.xxxxx.xxxxx.jsf; // A MODIFIER...

import xx.xxxxx.xxxxx.entity.Customer;
import xx.xxxxx.xxxxx.service.CustomerManager;
import java.io.Serializable;
import jakarta.inject.Inject;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Named;

/**
 * Backing bean pour la page customerDetails.xhtml.
 */
@Named
@ViewScoped
public class CustomerDetailsBean implements Serializable {
  private int idCustomer;
  private Customer customer;

  @Inject
  private CustomerManager customerManager;

  public int getIdCustomer() {
    return idCustomer;
  }

  public void setIdCustomer(int idCustomer) {
    this.idCustomer = idCustomer;
  }

  /**
   * Retourne les détails du client courant (contenu dans l'attribut customer de
   * cette classe).
   */
    public Customer getCustomer() {
      return customer;
    }

  /**
   * Action handler - met à jour dans la base de données les données du client
   * contenu dans la variable d'instance customer.
   * @return la prochaine page à afficher, celle qui affiche la liste des clients.
   */
  public String update() {
    // Modifie la base de données.
    // Il faut affecter à customer (sera expliqué dans le cours).
    customer = customerManager.update(customer);
    return "customerList";
  }

  public void loadCustomer() {
    this.customer = customerManager.findById(idCustomer);
  }
}

Ce backing bean a 2 propriétés :

La méthode loadCustomer appelle une méthode qu'il va falloir rajouter dans le bean CDI CustomerManager. Cette méthode recherche un Customer par son id. Dans CustomerManager (vous pouvez y accéder par l'onglet Projects ou bien plus simplement par Ctrl-clic sur le type CustomerManager de la variable d'instance customerManager de la classe CustomerDetailsBean) ajouter cette méthode :

public Customer findById(int idCustomer) {  
  return em.find(Customer.class, idCustomer);  
}

Sauvegardez bien le fichier CustomerDetailsBean.java avant de passer à la suite.

Ajout d'une page JSF pour afficher les détails d'un client

Sur le projet faites clic droit > New > Jsf Page et créez une page customerDetails.xhtml dans la racine des pages Web du projet (ne tapez pas le .xhtml final ; il sera rajouté par NetBeans).

Ajout formulaire JSF

Vous allez ajouter un formulaire pour afficher, et même modifier, toutes les informations sur le client choisi par l'utilisateur dans la page listCustomers.xhtml.

A l'intérieur du body de la nouvelle page, tapez Alt-Insert et choisissez "JSF Form From Entity". Une autre façon de faire : faites un drag and drop de "JSF Form From Entity" (dans la palette) dans le body de la page. Pour faire afficher la palette : menu Window > IDE Tools > Palette.

Form from entity

Indiquez ensuite le nom de la classe entité dont vous voulez afficher les informations dans un formulaire, ainsi que le nom de la propriété du backing bean dont le getter retourne les détails d'un client. Vous pouvez choisir d'utiliser PrimeFaces ou d'utiliser la librairie standard pour "Template Style" ; pour faire simple je vous conseille de laisser la valeur "Standard JavaServer Faces". Vous auriez pu cocher "Generate read only view" si cette page n'avait servi qu'à afficher les informations sur le client mais on va aussi s'en servir pour modifier ses propriétés.

Remarque : il se peut que les entités ne s'affichent pas tout de suite. En ce cas, attendez un peu et recommencez l'opération. Peut-être qu'un clean and build du projet aide...

Entity pour form

Examinez le code de la page customerDetails.xhtml. On voit qu'elle affiche les propriétés du client retourné par la méthode getCustomer() du backing bean CustomerDetailsBean. On a, par exemple, value="#{customerDetailsBean.customer.name}".

La table est insérée dans un formulaire (<h:form>). En effet cette page permettra non seulement d'afficher toutes les informations sur un client, mais aussi de les modifier. Le formulaire est lui-même inséré dans la balise <f:view> mais elle n'est pas indispensable. Remettez en forme le code de la page, avec les bonnes indentations.

Pour le moment, faites une petite modification dans les lignes générées par NetBeans pour éviter que l'utilisateur ne modifie l'id d'un client : ajoutez readonly="true" dans la balise <h:inputText> de l'id.

Enlevez aussi tout ce qui concerne le microMarket (c'est-à-dire ce qui concerne la propriété "zip") : le label et la liste déroulante (<h:selectOneMenu>) pour la saisie.

Comme la méthode getCustomer() retourne le client qui est dans la propriété "customer" du backing bean, il reste à mettre dans cette propriété le Customer qui a l'id de la ligne de la table des Customer sur laquelle l'utilisateur aura cliqué. Ca va être le but de la section metadata que l'on va écrire.

Ajout section metadata

On a vu que si l'utilisateur clique sur la ligne de la table des clients qui correspond au client d'id 2, une requête GET sera envoyée à l'URL ".../customerDetails?idCustomer=2".

Pour que la page customerDetails.xml affiche bien les détails sur le client dont l'id est passé en paramètre de la requête HTTP GET, il reste 2 étapes :

  1. Récupérer la valeur du paramètre idCustomer de la requête HTTP GET (2 dans l'exemple ci-dessus) dans la propriété idCustomer du backing bean.
  2. Lancer la méthode loadCustomer pour placer le Customer d'id idCustomer dans la propriété customer du backing bean.

Pour cela, comme on le verra dans le cours sur JSF, il faut modifier le code de la page customerDetails.xml en ajoutant ceci juste avant la balise <h:head> et après la balise <html xmlns=...> :

<f:metadata>
   <f:viewParam name="idCustomer" value="#{customerDetailsBean.idCustomer}"
                required="true"/>
   <f:viewAction action="#{customerDetailsBean.loadCustomer}"/>
</f:metadata>

La section <f:metadata> peut servir à plusieurs choses. Ici elle sert à indiquer ce qui sera exécuté juste avant que la page ne soit affichée.

<f:viewParam> dit à JSF de récupérer la valeur du paramètre de nom "idCustomer" de la requête HTTP GET (la valeur sera 2 pour l'exemple ci-dessus) et de la mettre dans la propriété idCustomer du backing bean customerDetailsBean (avec le setter setIdCustomer).

<f:viewAction> dit à JSF d'exécuter la méthode loadCustomer du backing bean customerDetailsBean, après avoir rangé l'id dans la propriété idCustomer du backing bean, et avant l'affichage de la page.

En résumé, tout se passe donc comme on veut ; par exemple, si l'URL contient  customerDetails.xhtml?idCustomer=2, la valeur 2 est mise dans la propriété idCustomer du backing bean et ensuite, la méthode loadCustomer charge le client d'id 2 depuis la base de données et le met dans la variable d'instance customer du backing bean. La page des détails affichera les informations sur ce customer ; on peut le voir en lisant le code généré par NetBeans pour la page des détails ; par exemple <h:inputText id="name" value="#{customerDetailsBean.customer.name}" title="Name" /> avec la propriété customer du backing bean qui correspond à la variable d'instance customer.

Vous pouvez tester tout de suite l'affichage de la page des détails à partir de la liste de tous les clients.

Voici ce qui devrait s'afficher pour la page des détails du client d'id 1. Les détails du client numéro 1 s'affichent bien, cependant le code de discount (remise faite au client) n'est pas correctement affiché.

Problème avec discountcode

Remarque : on peut même modifier l'URL dans la barre d'adresse du navigateur de la page des détails à partir de la liste des clients en changeant "à la main" la valeur du paramètre idCustomer, et en faisant réafficher la page. Par exemple en changeant idCustomer=1 en idCustomer=36 (il faut choisir un id de client qui existe dans la base de données).

Nous allons voir maintenant comment afficher les codes de discount dans la liste déroulante. Nous ajouterons ensuite un bouton de navigation pour revenir à la liste des clients et un bouton pour permettre à l'utilisateur de modifier les informations sur un client en soumettant le formulaire.

Affichage des Discount dans la page des détails d'un client

Dans la base de données, la table DISCOUNT contient juste 4 valeurs correspondants aux quatre types de réductions possibles. On va remplir la liste déroulante avec ces valeurs. La liste permettra aussi à l'utilisateur de modifier la réduction accordée au client en choisissant une de ces 4 valeurs. Pour le moment le code affecte la valeur #{fixme} à <f:selectItems>, balise interne de la liste déroulante <h:selectOneMenu>, qui indique les valeurs à afficher. Vous allez donc commencer par remplacer cette expression EL #{fixme} par la référence à une méthode du backing bean qui retourne les 4 valeurs possibles pour une réduction.

Snap40.jpg

Ajout d'un bean CDI pour gérer les données de la base sur les codes de réduction

Ce bean DiscountManager va gérer les Discount, comme le bean CustomerManager gére les Customer.

La récupération des Discount se fait dans la base de données. Selon le partage des tâches habituel de JSF, un backing bean ne doit pas accéder directement aux bases de données. Il doit faire appel à un bean CDI qui utilise un EntityManager injecté pour interagir avec la base de données. Vous allez donc commencer par ajouter un bean CDI DiscountManager dans le sous-package service, pour gérer les données associées aux Discount dans la base de données. Inspirez-vous du bean CDI que vous avez déjà créé.

Dans ce bean, injectez un EntityManager et ajoutez une méthode publique List<Discount> getAllDiscounts() qui retourne toutes les réductions. Inspirez-vous de la méthode qui retourne tous les clients dans le bean CDI CustomerManager. Essayez de refaire les étapes que vous aviez faites lors de l'écriture du gestionnaire de clients (ajoutez l'Entity Manager, puis les méthodes). Pas besoin de méthode persist puisque vous n'allez pas créer de nouveau Discount.

Ajoutez-y une méthode qui retourne le Discount qui a un certain code (le code est H, M, L ou N). Cette méthode servira dans la section suivante pour convertir une String en Discount :

public Discount findById(String code) {
  return em.find(Discount.class, code);
}

Donc, maintenant on a un bean CDI qui renvoie toutes les valeurs possibles de Discount et qui permet d'avoir un Discount si on connait son code ("H" par exemple).

Modification du backing bean de la page des détails

Dans le backing bean CustomerDetailsBean, écrivez la méthode qui servira à remplir le menu déroulant de la page JSF customerDetails.xhtml. Insérez dans le bean la méthode suivante :

  /**
   * Retourne la liste de tous les Discount.
   */
  public List<Discount> getDiscounts() {
    return discountManager.getAllDiscounts();
  }

D'où vient ce discountManager ? A vous de jouer... Vous avez déjà vu ce type de code quand vous avez écrit le backing bean CustomerDetailsBean.

Modification de la page des détails

Remplacez #{fixme} dans la page customerDetails.xml par la référence à cette méthode (en fait par une référence à la propriété dont cette méthode est le getter).

Vous pouvez maintenant faire exécuter l'application (par Run) et vous verrez que les Discount s'affichent dans la liste déroulante. Cependant, comme pour l'affichage de la liste de tous les clients, l'affichage ne convient pas vraiment.

Il est facile d'améliorer cet affichage en utilisant les attributs suivants de la balise <f:selectItems> (Rappel : NetBeans peut vous aider pour compléter les noms d'attributs tels que itemLabel ; vous tapez "item" suivi de Ctrl-[Barre espace].)

Inspirez-vous de ce que vous avez déjà fait pour la page qui liste tous les clients. Un simple rechargement de la page devrait suffire pour voir l'amélioration.

Si tout est correct, Git...

Exercice optionnel : Faites afficher les réductions par ordre croissant (et ensuite par ordre décroissant) des taux de réduction. Vous pouvez réviser le tri sur les collections Java et les expressions lambda (mais c'est une moins bonne solution) ou bien (meilleure solution) ajoutez un "NamedQuery" avec un "order by" dans l'entité Discount.

Utilisation d'un convertisseur pour envoyer au serveur le code de réduction choisi par l'utilisateur

La page JSF customerDetails.xhtml va être utilisée par l'utilisateur pour modifier des informations sur un customer, y compris la réduction (discount) à laquelle il a droit.

Il y a plusieurs solutions pour envoyer au serveur le choix de l'utilisateur pour la réduction. On pourrait envoyer le code de la réduction (par exemple "M") et le serveur pourrait récupérer ce code et charger l'instance de Discount qui a ce code, comme on a récupéré un Customer à partir de son id.

Je vais vous montrer une autre façon de faire qui utilise un convertisseur qui va s'occuper de la transformation d'un code de réduction en instance d'entité Discount.

Lorsque l'utilisateur modifie les informations sur un client et soumet le formulaire, son choix pour la valeur de la réduction va être envoyé au serveur comme une entité entière, une instance de Discount. Quand le serveur reçoit la requête HTTP envoyée par le client HTTP à la soumission du formulaire, il récupère donc une instance de Discount. Évidemment, le protocole HTTP ne permet pas de transmettre une entité Java du client vers le serveur ; ce sont des String qui sont transmises. En fait, JSF va intervenir "sous le manteau" en utilisant un convertisseur qui sait transformer une String en instance de Discount, et vice-versa. Ce qui va transiter sur le réseau est en fait l'id (le code) de l'entité Discount (de type String).

Traduction en code dans la page JSF :

On a ainsi ce code :

    <h:selectOneMenu 
        id="discount" value="#{customerDetailsBean.customer.discount}" 
        title="Discount" required="true" requiredMessage="The Discount field is required.">
        <f:selectItems value="#{customerDetailsBean.discounts}"
                       var="discount"
                       itemLabel="#{discount.code} (#{discount.rate} %)" 
                       itemValue="#{discount}"/>
    </h:selectOneMenu>

Vous devriez obtenir ceci :

Modification discountcode

L'affichage est correct. Il reste à ajouter les 2 boutons pour revenir à la liste et soumettre le formulaire.

Ajout de boutons pour la mise à jour et retour à la liste

Il suffit de rajouter ces deux lignes dans le fichier customerDetails.xhtml, juste après le menu déroulant pour les discounts :

  <h:button id="back" value="Revenir à la liste" outcome="customerList"/>
  <h:commandButton id="update" value="Enregistrer" action="#{customerDetailsBean.update}"/>

Voici ce que cela signifie :

Testez :

  1. Faites afficher les détails sur un des clients. Est-ce que le code de réduction s'affiche correctement ? Il doit correspondre à ce qui est affiché dans la page qui affiche la liste de tous les clients.
  2. Modifiez le discount d'un customer. Soumettez le formulaire en cliquant sur le bouton "Enregistrer". Rien ne semble se passer ; on ne sait pas si la modification a été enregistrée ou pas. Si vous revenez à la liste de tous les clients vous verrez que la modification n'a pas été enregistrée et cependant aucun message d'erreur n'est affiché, ce qui n'est vraiment pas ergonomique.
  3. Ajoutez une balise pour faire afficher les messages d'erreur juste avant la fin du formulaire : <h:messages/>
  4. Modifiez une information sur le client affiché et cliquez sur le bouton pour enregistrer cette modification. Vous devriez voir maintenant un message d'erreur du type "Erreur de conversion lors de la définition de la valeur «xx.xxxxx.xxxxxx.entity.Discount[ code=N ]» pour «null Converter».".

Quel est le problème ? Pour le comprendre, faites afficher le code de la page HTML qui contient le formulaire (Ctrl-U sur Chrome), et examinez le code qui concerne la liste déroulante. En HTML, une liste déroulante est représentée par la balise <select>. Les valeurs envoyées sont contenues dans l'attribut des balises <option>.

Le problème est que la String "xx.xxxxx.xxxxxx.entity.Discount[ code=N ]" (pour cet exemple) a été envoyée au serveur pour le Discount . Quand cette String arrive sur le serveur, elle ne peut être affectée à une variable de type Discount ; en effet l'attribut value de <f:selectItems> indique que la valeur de type String va être affectée à #{customerDetailsBean.customer.discount} qui est de type Discount, comme on le voit dans la classe entité Customer.

Choix d'un Discount - convertisseur

Vous allez résoudre le problème en écrivant un convertisseur JSF pour les Discount, qui va convertir les String en Discount (et vice-versa).

Ce convertisseur va intervenir 2 fois :

  1. Au moment de générer le code de la page HTML qui contient le formulaire (en transformant les Discount en String).
  2. Quand le code choisi par l'utilisateur va arriver sur le serveur, après la soumission du formulaire (en transformant la String en Discount).

Il existe plusieurs façons de créer et d'utiliser un convertisseur JSF. Pour ce TP vous allez écrire une classe à part qui va implémenter l'interface jakarta.faces.convert.Converter<T> (attention, n'importez pas la classe Converter du paquetage jakarta.persistence). Cette classe sera annotée par @FacesConverter , avec un paramètre qui va identifier le convertisseur, par exemple @FacesConverter(value=converterDiscount). L'identificateur du convertisseur sera utilisé quand il sera utilisé par un composant JSF (avec la liste déroulante ce cas).

Comment convertir une String en Discount et vice-versa ? Le plus souvent pour une entité, l'id de l'entité (l'attribut code de l'entité Discount) est utilisé. Par exemple, la String "2" va être convertie en l'objet Discount qui a l'id 2 (il faudra aller chercher les données de l'entité dans la base de données) ; la conversion d'un objet Discount en une String est plus simple car il suffit de retourner la valeur de l'id de l'entité (la valeur de code).

Puisqu'un accès à la base de données va être nécessaire, le bean CDI DiscountManager écrit dans la section précédente sera utilisé (il faudra l'injecter dans le convertisseur). Pour des raisons techniques, un convertisseur ne peut injecter un bean CDI que si l'annotation @FacesConverter a un attribut managed=true (voir code complet de la classe ci-dessous).

Voici le code de la classe qui fait la conversion (mettez-la dans le même paquetage que les backing beans) :

@FacesConverter(value = "discountConverter", managed=true)
public class DiscountConverter implements Converter<Discount> {
  @Inject
  private DiscountManager discountManager;

  /**
   * Convertit une String en Discount.
   *
   * @param value valeur à convertir
   */
  @Override
  public Discount getAsObject(FacesContext context, UIComponent component, String value) {
    if (value == null) return null;
    return discountManager.findById(value);
  }

  /**
   * Convertit un Discount en String.
   *
   * @param value valeur à convertir
   */
  @Override
  public String getAsString(FacesContext context, UIComponent component, Discount discount) {
    if (discount == null) return "";
    return discount.getCode();
  }
}

Rappel : attention à bien importer le bon paquetage pour Converter (jakarta.faces.convert).

On va pouvoir ajouter l'attribut converter à la balise <h:selectOneMenu> de la page customerDetails.xhtml afin d'utiliser le convertisseur que nous venons d'écrire : il faut lui donner pour valeur l'id du convertisseur qu'on vient d'écrire (discountConverter).

Voici le code qu'il faut modifier dans customerDetails.xhtml :

   <h:selectOneMenu id="discount" value="#{customerDetailsBean.details.discount}"   
                    title="Discount" required="true" 
                    requiredMessage="The Discount field is required."  
                    converter="discountConverter">   
       <f:selectItems value="#{customerDetailsBean.discounts}"  
                      var="discount"   
                      itemLabel="#{discount.code} : #{discount.rate} %" 
                      itemValue="#{discount}" />  
   </h:selectOneMenu>

Sauvegardez, testez, ça doit fonctionner. Vérifiez que les modifications ont été prises en compte dans la base de données.

Git...

En résumé :

Section optionnelle : Soumission du formulaire, cycle de vie JSF

Voyons un peu plus en détails ce qui se passe quand le formulaire est soumis. C'est un exemple pratique du déroulement du "cycle de vie JSF" qui contient plusieurs phases et que vous étudierez dans le cours sur JSF.

N'oubliez pas que, normalement, le client HTTP et le serveur d'application sont hébergés par 2 ordinateurs différents reliés par Internet.

A la soumission du formulaire les valeurs qui sont dans les champs de saisie (composants <h:inputText> et <h:selectOneMenu>) sont transmises au serveur sous la forme de texte par une requête POST. Ces valeurs sont transmises par des paramètres inclus dans le corps de la requête POST.

Quand elles arrivent sur le serveur, JSF les récupère, les convertit dans le bon type Java. JSF vérifie que les valeurs sont valables ; par exemple, certaines valeurs sont requises et la valeur null ne sera pas valable. Ça n'est pas le cas ici, mais il pourrait aussi y avoir des contraintes plus complexes, par exemple sur le format d'un champ "email" ou un âge supérieur à 18 ans.

Si au moins une des valeurs n'est pas valable, des messages d'erreur pour toutes les valeurs non valables sont insérés dans la page qui contient le formulaire et cette page est retournée au client HTTP en réponse à la requête POST. Quand elle arrive sur le client HTTP, la page avec les messages d'erreur s'affiche dans le navigateur. L'utilisateur prendra ainsi connaissance des erreurs et pourra corriger ce qu'il a saisi pour soumettre à nouveau le formulaire.

Si toutes les valeurs sont valables, elles sont rangées par JSF à l'endroit indiqué par les expressions EL du backing bean. Par exemple,
<h:inputText id="name" value="#{customerDetailsBean.customer.name}" title="Name" />
indique que la valeur saisie par l'utilisateur sera rangée dans la propriété "name" du customer en cours (celui qui est retourné par la méthode getCustomer() et dont les propriétés ont été affichées lorsque la page a été affichée).

Sur le serveur la propriété customer du backing bean contient donc les valeurs modifiées par l'utilisateur. La méthode update() est alors exécutée (à cause de action="#{customerDetailsBean.update}" du bouton qui a soumis le formulaire).

Regardons la méthode update() :

  public String update() {  
    customer = customerManager.update(customer);
    return "customerList";
  }

Cette méthode prend la propriété "customer", de type Customer et appelle la méthode update() de le bean CDI customerManager, qui va mettre à jour le client dans la base de données (la méthode merge sera étudiée dans le cours sur JPA) avec les valeurs contenues dans customer. La méthode retourne la String "customerList" et c'est donc la page customerList.xhtml qui sera retournée en réponse à la requête POST (règle de JSF), et affichée dans le navigateur du client HHTP.

Pour la réduction (discount) : Il existe une relation entre les clients et les codes de remise. Dans la classe entité Customer, on voit que le code de remise est un attribut discount de type Discount. Lorsque l'utilisateur modifie le discount avec le menu déroulant dans la page de détails, une String est envoyée pour ce discount. C'est le travail du convertisseur de transformer sur le serveur cette String en objet de type Discount.

Quelques petites améliorations

Que se passe-t-il si l'utilisateur modifie la valeur du paramètre de l'URL et donne un id qui ne correspond à aucun client ? Et si l'utilisateur tape alors sur le bouton "Enregistrer" ? Vérifiez qu'un formulaire vide est affiché. Un produit professionnel ne doit pas avoir ce comportement. Pour l'éviter vous pourriez ajouter un attribut au formulaire pour qu'il ne soit affiché que si un client a été trouvé :
<h:form rendered='#{customerDetailsBean.customer != null}'>

Il faut aussi sortir le bouton "Revenir à la liste" du formulaire pour que l'utilisateur puisse revenir à la liste dans ce cas où le formulaire n'est pas rendu.

Encore mieux : ajouter un message disant "Aucun client n'a été trouvé avec cet id" dans le cas où le formulaire n'est pas affiché :
<h:outputText value="Aucun client avec l'id #{customerDetailsBean.idCustomer} !" rendered='#{customerDetailsBean.customer == null}'/>

On pourrait aussi améliorer la présentation, par exemple pour la dataTable. Les TPs suivants montreront comment faire.

Il faudrait aussi évidemment ajouter des composants pour afficher les messages d'erreurs éventuelles et pour afficher les messages de succès (par exemple si les modifications ont bien été enregistrées). Vous verrez comment faire dans le cours sur JSF.

Git...

Problèmes devant encore être réglés :

Dans le prochain TP et en cours nous verrons comment corriger ces problèmes avec le modèle PRG.

Retour TPs