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éations de classes entités à partir d'une base de données existante
  6. Création d'un EJB Session Bean Stateless CustomerManager pour la gestion des clients
  7. Ajout de méthodes dans le session bean CustomerManager
  8. Présentation de la partie front-end web
  9. Configuration de JSF
  10. Création d'un bean géré par CDI (backing bean de la page CustomerList.xhtml)
  11. Modification du fichier web.xml
  12. Ajout d'une page JSF pour afficher la liste des clients
  13. Ajout d'une DataTable JSF dans la page
  14. Exécution du projet et premier test
  15. Déploiement d'une application
  16. 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
  17. Affichage des détails d'un client lorsqu'on clique sur une ligne
    1. Ajout d'un lien dans le tableau 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 DiscountCodes 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 DiscountCode - convertisseur
    7. Soumission du formulaire, cycle de vie JSF
  18. Quelques petites améliorations
  19. 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 et violente avec les technologies Maven, CDI, EJB, JPA, et JSF.

Il est vraiment important que vous terminiez ce TP. Vous aurez plus de facilités à comprendre le cours si vous avez bien étudié ce TP 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.

Parties optionnelles : ce TP et les suivants comportent des parties optionnelles pour approfondir certains points du cours. L'examen ne pourra 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 de NetBeans. Ignorez les options supplémentaires qui sont éventuellement affichées.

Création d'un projet de type Web Application

Dans ce TP vous allez développer une application de type "Web", qui correspond au profile Web de Jakarta EE.

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 qui seront utilisés se retrouveront dans l'entrepôt (repository) local de Maven qui est sur votre ordinateur (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 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 bizarres dans le chemin ou dans le nom, pas d'accents (é, à,...) par exemple. On ne sait jamais... ;-).

Modifiez le nom du projet : tpCustomerApplicationXxxxx par exemple (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 "Artfact 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. Ca 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 5 que vous avez installé) et la version Jakarta EE 8 Web :

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 (vous devez être connecté à Internet si vous n'avez pas déjà chargé ces artefacts)

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. Vous n'utiliserez pas REST dans ce cours. 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.

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 (serveurs d'application, SGBD, etc.).

L'onglet Services vous sera utile pour gérer le SGBD et le serveur d'application, en particulier pour les démarrer et les arrêter :

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 une petite application RESTful de test (entrée "RESTful Web Services" et code associé dans "Source Packages") que nous n'utiliserons pas. Il contient aussi les fichiers de configuration

L'onglet Files 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 les 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.

Les 2 autres onglets montrent des structures "logiques" du projet, pas des structures physiques. 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 en compilant 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.

pom.xml contient plusieurs sections:

Par exemple,

<dependency>
    <groupId>jakarta.platform</groupId>
    <artifactId>jakarta.jakartaee-web-api</artifactId>
    <version>${jakartaee}</version>
    <scope>provided</scope>
</dependency>

indique que le projet dépend de l'API Jakarta EE 8 (la variable "jakartaee" est définie dans la section properties du fichier ; d'autres variables comme "project.build.directory" sont fournies par NetBeans) et que cette API sera fournie par le serveur d'application (Payara pour votre cas). Si cette API n'était pas fournie (provided) par le serveur, il faudrait l'intégrer au fichier d'archive de l'application. Cette API est identifiée par les valeurs de groupId et de artifactId et elle est contenue dans l'entrepôt standard de Maven sur le Web, qui contient la plupart des librairies Java et Jakarta EE.

Changer la version du compilateur

Si ça n'est déjà fait, dans la section <properties>, changez 1.8 en 11 pour <maven.compiler.source> et <maven.compiler.target>.

Le compilateur utilisera toutes les possibilités de Java 11 et produira du bytecode Java 11.

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).

Ensuite, démarrez Payara (clic droit sur le serveur dans l'onglet Services et Start) et tapez cette adresse dans le navigateur :
http://localhost:8080/<context path>/resources/sample (le plus simple est de faire un clic droit sur le projet et Run et ensuite de compléter).

Le message "Hello World Jakarta EE 8" qui est affiché dans la page, est la valeur de la propriété message du fichier microprofile-config.properties placé dans le répertoire META-INF ; dans l'onglet "Projects" sous "Other Sources" > "src/main/resources" ; dans l'onglet "Files" sous src/main/resources/META-INF. L'API de configuration de MicroProfile est utilisée (pas étudiée dans le cours). Cette API pourra vous rendre de grands services si vous développez des applications ; elle sera intégrée dans les prochaines versions de Jakarta EE.

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.

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.

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

Vous allez utiliser une base de données relationnelle. Comme vous le verrez dans le cours sur JPA, vous ne manipulerez pas des données directement en SQL mais sous 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).

Dans un premier temps nous allons utiliser un wizard de NetBeans pour générer une classe entité à partir d'une table. Ce cours n'est pas un cours sur NetBeans et vous ne devez utiliser un wizard pour vous faire gagner du temps que si vous comprenez le code généré. D'autant plus que ces wizards génèrent parfois du code non optimisé qui n'est pas adapté au projet particulier que vous écrivez.

Pour lancer le wizard, clic droit sur le nom du projet (dans onglet Projects) puis New > Entity Class from Database. Si cette option n'apparait pas, cliquez sur l'option "Other" qui vous présente d'autres options classées par thèmes ; le thème pour cette option est "Persistence".

Une fenêtre apparait dans laquelle vous allez indiquer la source de données (la base de données sur laquelle vous allez travailler). Vous allez utiliser la base de données "customer" que vous avez créée dans l'installation de l'environnement des TPs. Le nom JNDI (répertoire de noms utilisé par Jakarta EE) de la source de données liée à cette base est "jdbc/customer". Dans la fenêtre qui s'ouvre, choisissez la connexion pour la base customer que vous avez créée dans l'installation de l'environnement ; quelque chose comme "jdbc:mysql://localhost:3306/customer?zeroDateTimeBehavior=CONVERT_TO_NULL&useTimezone=true&serverTimezone=UTC" (pas le nom de l'image ci-dessous qui a été capturée avec un ancienne version de ce TP ; d'autres images ci-dessous correspondent à cette ancienne version).

Dès que vous avez choisi le nom de la base, les tables s'affichent dans la colonne de gauche.

Choisissez "CUSTOMER" (client en français) et cliquez sur le bouton "Add". Cela va ajouter en fait 3 tables parce que l'option "Include Related Tables" est cochée : la table CUSTOMER mais aussi les tables DISCOUNT_CODE et MICRO_MARKET car il existe une jointure entre ces deux tables et la table CUSTOMER.

EntityClassesFromDatabase.jpg

Cliquez sur le bouton "Next". Maintenant on va vous proposer de changer le nom des classes entités correspondants à chacune des tables. Laissez tout par défaut, sauf le nom du paquetage ; ajoutez ".entities" à la fin du nom du paquetage proposé :

Classes entités

Cliquez sur le bouton "Next". L'étape suivante propose de modifier les valeurs par défaut pour les types de données liés aux associations entre les deux tables :

Options de mapping

Laissez tout par défaut et cliquez sur le bouton Finish.

NetBeans va un peu travailler puis vous verrez des nouvelles choses dans le projet :

  1. Les classes correspondant aux 3 tables, dans le package "entities". Regardez donc à quoi elles ressemblent. En particulier 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...".
    Contenu des tables
  2. Le fichier persistence.xml (que vous trouvez dans Other Sources > src/main/resources > META-INF) a été modifié.

Fichier persistence.xml

Faites afficher le contenu du fichier persistence.xml pour voir s'il tient compte de votre action.

2 façons de voir ce fichier : Design (vue "logique" du fichier) ou Source (vue XML). Cliquez sur Source. Le contenu de persistence.xml devrait être (vous n'aurez peut-être pas les balises <class> ; ça dépend de la version de NetBeans que vous utilisez)

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.2" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
  <!-- Define Persistence Unit -->
  <persistence-unit name="my_persistence_unit">
    <class>xxxx.entities.DiscountCode</class>
    <class>xxxx.entities.MicroMarket</class>
    <class>xxxx.entities.Customer</class>
  </persistence-unit>
</persistence>

Enlevez toutes les balises <class>. Elles ne sont pas nécessaires car vous n'avez qu'une seule unité de persistance. Si vous aviez plusieurs unités de persistance il faudrait énumérer les classes prises en compte par chaque unité de persistance.

Modifiez le nom de l'unité de persistance, par exemple customerPU.

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 aussi 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, peut se trouver 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 qui est EclipseLink (information fournie dans la documentation de EclipseLink), implémentation de JPA.

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="2.2" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.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 (utile si on veut travailler 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 ; utile si une application travaille avec plusieurs sources de données).
  3. On peut définir des propriétés pour configurer l'unité de persistance, par exemple pour faire générer des tables relationnelles si elles n'existent pas déjà, 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).

Rappel : 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 ; pour cela, 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

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 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 EJB Session Bean Stateless CustomerManager pour la gestion des clients

On va centraliser la gestion des Customers dans un EJB Stateless. De manière classique on crée une "façade" pour les opérations élémentaires sur les clients : création, suppression, recherche, modification.

Dans une application Jakarta EE il est bon de séparer les tâches. Quand il s'agit de gérer les données d'une base de données, on fait appel à un EJB session. Le plus souvent, comme dans cette application, un EJB session sans état (il ne conserve pas d'information entre 2 appels de ses méthodes).

Faire clic droit sur le projet et New > Session Bean (si "Session Bean" n'apparait pas, "Other..." > "Enterprise JavaBans" > "Session Bean") :

New session bean

Donnez un nom à votre gestionnaire de client, et indiquez que vous le voulez dans le sous-paquetage "session" du paquetage de base de votre application (changez ce qu'il faut dans le nom du paquetage donné ci-dessus) qui contiendra les session beans. Ne cochez pas "Local" pour ne pas créer une interface Java pour cet EJB ; l'interface sera donc l'ensemble des méthodes public, comme on le verra dans le cours :

Nom session bean

Ajout de méthodes liées à JPA dans le session bean CustomerManager

Double cliquez sur "CustomerManager.java" dans le projet pour voir le source dans l'éditeur. Il n'y a pas grand chose à voir, à part l'annotation @Stateless qui indique que la classe correspond à un EJB session sans état.

Ajoutez 2 méthodes à l'EJB, comme dans le code ci-dessous :

package xx.xxxxx.tpcustomerapplication.session; // A MODIFIER suivant le paquetage de base...  
        
import javax.ejb.Stateless;
      
@Stateless  
public class CustomerManager {  
        
    public List<Customer> getAllCustomers() {  
      return null;  
    }  
        
    public Customer update(Customer customer) {
      return null;  
    }          
}

Un point rouge va apparaitre dans la marge du code source à côté des 2 en-têtes de méthode. Si vous passez la souris sur ce point rouge, un message vous dit, par exemple, que le symbole "class List" n'est pas trouvé, ainsi que le symbole Customer. En effet, il faut importer ces classes qui sont dans un autre paquetage. Vous pouvez taper vous-même les imports mais le plus simple est de taper Ctrl-Shift-I (ou touche commande-Shift-I pour les Mac) ou bien clic droit/fix imports. 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é peut ne pas être le bon. Par exemple, il faut choisir java.util.List pour List.

Vous devriez avoir un code source comme celui-ci :

package xx.xxxxx.tpcustomerapplication.session; // A MODIFIER suivant le paquetage de base...  
        
import entities.Customer;
import java.util.Collection;
import javax.ejb.Stateless;

@Stateless
public class CustomerManager {

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

    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 et qui 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.

[ Ne marche PLUS dans la dernière version de NetBeans, ajoutez directement le code ci-dessous : Placez le curseur à l'intérieur de la classe et faites Alt-Insert (ou clic droit dans le source/insert code) et choisissez "Use Entity Manager...". Cela va ajouter automatiquement dans le code une variable avec une annotation, et une méthode persist (une entité passée à cette méthode sera rendue persistante) - attention, le nom de unitName doit correspondre au nom donné dans persistence.xml : ]

Ajoutez ce code au début du corps de la classe (c'est le code qui aurait dû être ajouté par Alt-Insert) :

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

public void persist(Object object) {
  em.persist(object);
}

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

Pour importer les classes, vous pouvez utiliser Ctrl-Shift-I ou bien cliquer sur le bouton rouge dans la marge du code.

Une autre facilité offerte par NetBeans : vous pouvez taper "EntityM" et Ctrl-espace et NetBeans vous propose de compléter. Choisissez la classe EntityManager du paquetage jakarta.persistence et l'import sera ajouté automatiquement.

"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 persitence.xml, vous auriez pu écrire plus simplement

@PersistenceContext
private EntityManager em;

Commencez par modifier la méthode persist puisque les seuls objets passés à cette méthode seront de type Customer. Changez aussi le nom de la variable object pour rendre le code plus lisible. En passant, voici une facilité de NetBeans bien utile pour modifier le nom d'une variable partout où elle apparait : cliquez sur le paramètre object de persist et tapez Ctrl-R (R comme Rename ; on aurait aussi pu faire un clic droit sur object et choisir Refactor > Rename...) ; changez object en customer et tapez la touche "Entrée" ; vous voyez que le object du corps de la méthode est aussi modifié !

Modifiez aussi 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 "." (même, par exemple, pour le nom de la requête nommée Customer.findAll). Essayez avec la méthode getAllCustomers.

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

import entities.Customer;
import java.util.Collection;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

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

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

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

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

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

Importez la classe javax.persistence.Query (utilisée dans getAllCustomers). Attention encore, un piège classique est d'ajouter le mauvais import (normalement NetBeans devrait vous proposer le bon import mais méfiance...). Faites clic droit/Fix Import (ou clic sur le point rouge dans la marge en face de Query et choisissez "Add import for ...") :

Fix all imports

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.

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

Git (vraiment le dernier rappel...)

Clean and Build du projet.

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

Présentation de la partie front-end web

Dans cette partie on va utiliser JSF (Jakarta Server Faces), 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 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 (Context and Dependency Injection) 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 l'EJB session). 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 le cours sur JSF.

Un serveur d'application contient plusieurs types de containers. Vous avez déjà utilisé un container EJB pour gérer les EJB et les EntityManager et il existe aussi le container CDI qui gère les backing beans. Un container CDI peut aussi gérer d'autres instances Java, indépendantes d'une page JSF. Par exemple, une classe Java peut demander à CDI d'injecter une instance d'une autre classe Java. Dans les TPs de ce cours vous n'utiliserez CDI que pour des backing beans de 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 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 propriété d'un Java Bean est définie par un getter et/ou par un setter ; par exemple la propriété "nom" définie par getNom() et setNom(String nom).

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 profiter des fonctionnalités des dernières versions de JSF (à partir de la version 2.3) pour les injections CDI, il faut annoter une des classes CDI de l'application avec @FacesConfig.

Une bonne pratique est d'ajouter une classe de configuration pour éventuellement 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) :

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

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

@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 CustomerMBean, qui sera utilisé par la page CustomerList.xhtml. Vous utiliserez ensuite un wizard de NetBeans pour générer automatiquement une partie du code de la page JSF CustomerList.xhtml à partir du backing bean.

Ce backing bean aura une méthode getCustomers() pour retourner la liste de tous les clients (ce "getter" définira la propriété customers). Il 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 #{customerMBean.customers}.

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

New CDI bean

Ensuite, renseignez :

CustomerMBean.jpg

Vous devriez obtenir une classe CustomerMBean dans le package "managedbeans".

Le code de la classe CustomerMBean devrait être le code ci-dessous. Remarquez l'annotation @Named qui donne le nom qu'il faudra utiliser dans une page JSF pour désigner ce backing bean dans une expression EL : #{customerMBean.customers} désignera la propriété customers du backing bean. "@Named" aurait suffi plus simplement puisque customerMBean 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, donc une plus courte durée de vie, ne l'imposerait pas.

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

import java.io.Serializable;
import javax.inject.Named;
import javax.faces.view.ViewScoped;
        
@Named(value = "customerMBean")
@ViewScoped
public class CustomerMBean implements Serializable {

  public CustomerMBean() {
  }
}

Pour écrire la méthode getCustomers(), vous allez ajouter du code pour que le bean puisse communiquer avec l'EJB session stateless CustomerManager déjà écrit dans le module " EJB".

Dans le code du backing bean faites clic droit et "Insert code..." > "Call Enterprise Bean...". Ouvrez l'entrée de l'application dans la nouvelle fenêtre et sélectionnez l'EJB CustomerManager du module ejb ; ceci devrait insérer dans le source les lignes suivantes :

@EJB  
private CustomerManager customerManager;

L'annotation @EJB 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 qui va fournir une instance de CustomerManager. Voir le cours pour 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.customerApplication.managedbeans; // A MODIFIER suivant le paquetage de base...
        
import xx.xxxxx.customerApplication.entities.Customer;  
import javax.ejb.EJB;  
import javax.inject.Named;  
import javax.faces.view.ViewScoped;  
import java.io.Serializable;  
import java.util.List;  
import xx.xxxxx.customerApplication.session.CustomerManager;

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

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

Modification du fichier web.xml

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.

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

Il peut indiquer que

Vous allez modifier ce fichier

Le fichier web.xml devient

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="4.0" 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-app_4_0.xsd">
  <context-param>
      <param-name>javax.faces.PROJECT_STAGE</param-name>
      <param-value>Development</param-value>
  </context-param>

  <!-- Faces Servlet -->
<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>*.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 faites "next" et renseignez le nom de la page (pas de .xhtml final ; il sera rajouté par NetBeans) :

Nom page JSF

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

Ajout d'une DataTable JSF dans la page

Faites apparaître la Palette dans NetBeans (menu Window > IDE Tools > Palette, ou raccourci ctrl-shift-8). Ouvrez la partie JSF puis faites un drag-and-drop de "JSF Data Table from Entity", en dessous du body.

Vous pouvez aussi, plus simplement, taper "Ctrl-espace" à l'endroit choisi pour insérer la table, pour faire apparaitre une liste de composants et choisir "JSF Data Table From Entity". Ctrl-espace est la touche "magique" de NetBeans pour insérer ou compléter du code dans une classe Java, une page JSF, ou ailleurs.

Ajout datatable

  1. Une fenêtre de dialogue va apparaitre demandant de préciser pour quelle classe entité vous voulez une table ; indiquez la classe entité Customer. Comme property, indiquez le nom du backing bean pour les clients suivi du nom de la propriété correspondant à une liste de clients : customerMbean.customers.
    En effet, quand une page JSF référence une "propriété" utilisée en lecture, le getter de la propriété sera utilisé. Par exemple, #{customerMBean.customers} désigne la méthode getCustomers() qui retourne la liste des Customer (la liste affichée par la dataTable). Remarquez qu'il n'est pas nécessaire qu'une variable "customers" existe dans le bean.
  2. Si vous êtes passé par la palette , il est possible de demander avec une liste déroulante une table de la librairie de composants PrimeFaces plutôt qu'une table standard de JSF. Ne le faites pas car nous le ferons nous-mêmes par la suite.

Voici donc la fenêtre avec les bonnes valeurs (le nom du paquetage de Customer sera différent pour vous) :

 Table from entities

Etudiez les nombreuses lignes qui sont alors insérées dans la page JSF.

Une table (<h:dataTable>) est insérée dans un formulaire (<h:form>), lui-même inséré dans une vue. <f:view> est en fait optionnel ; on pourrait enlever cette balise dans le contexte de cet exemple. On pourrait aussi enlever la balise <h:form> si on se contentait d'afficher la table mais on en aura besoin pour permettre des tris sur des colonnes de la table.

Une table contient des colonnes (<h:column>) . Pour chaque ligne de la liste fournie à la table, toutes les valeurs des colonnes seront affichées. Une ligne de la table correspond à une valeur contenue dans la liste.

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

item est la "variable de boucle" définie dans l'attribut var de la balise JSF <h:datatable>. Puisque la valeur de <h:datatable> est la valeur de la propriété customers du backing bean (value="#{customerMBean.customers}"), c'est-à-dire une liste de Customer, var contient un Customer.

Exécution du projet et premier test

Pour exécuter le projet, 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.

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 sera très utile pour la mise au point de l'application et pour connaître la configuration de Payara et les versions des modules qu'il utilise Faites une recherche de Mojarra (avec Ctrl F) pour savoir quelle version de Mojarra Payara utilise (Mojarra est l'implémentation de référence de JSF) ; vous devriez avoir au moins la version 2.3.9 (un bug mineur des dernières versions de Payara fait que cette version n'est plus affichée).

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.

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.

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/tpCustomerApplication/.
    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 ; tpCustomerApplication 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/tpCustomerApplication/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 fait afficher le code HTML généré à partir de CustomerList.xhtml. 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

Remarque : Si vous modifiez le code de la page (par exemple en remplaçant "List" par "Liste des clients") 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 pouvez décocher cette case si ce comportement ne vous convient pas ; il faudra alors relancer l'application pour voir vos modifications car un simple reload de la page ne suffira pas.

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

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.

Les données proviennent de la base 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]. 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.

Enlever les références à MicroMarket

Remarquez l'affichage particulier des 2 dernières colonnes de la table. Ces 2 colonnes correspondent à des clés étrangères relationnelles de la table CUSTOMER vers les tables DISCOUNT_CODE et MICRO_MARKET (si vous voulez le vérifier, allez voir le code de l'entité Customer ou bien directement la section "Foreign Keys" de la table relationnelle CUSTOMER). Nous allons améliorer cette présentation.

Comme le problème est le même pour les 2 colonnes, pour simplifier vous allez enlever la colonne liée à MicroMarket (celle qui contient #{item.zip}) dans la dataTable. Optionnel : Quand vous aurez terminé le TP, pour voir si vous avez bien compris la fin du TP, vous pourrez rajouter cette colonne dans la table et aussi dans la page qui affiche les détails du client choisi et refaire sur cette colonne tout le travail qui a été fait pour discountCode.

Refaites afficher la page (vous savez qu'il suffit de recharger la page dans le navigateur pour voir les modifications).

Améliorer l'affichage des données

Maintenant on va modifier l'affichage du discountCode ; en effet, voir "entities.DiscountCode[discountCode=M]" n'est pas très satisfaisant. Si vous avez compris le concept de "propriétés", vous pouvez modifier la ligne qui affiche le code. remplacez :

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

par :

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

Explications :

Remarquez la complétion de code de NetBeans qui vous aide. Par exemple, pour "rate" : commencez par taper "#{item" puis tapez"." (vous auriez aussi pu taper "Ctrl-barre d'espace" si le point était déjà tapé auparavant) ; dans la liste affichée choisissez discountCode. Continuez en tapant encore "." et choisissez rate dans la liste.

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

L'affichage sera bien 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.

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 payara5\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 payara5\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 10.0.0 ; la version 10.0.1 ne sera pas gratuite. Après 10.0.0, la prochaine version gratuite sera 10.1.0. La version 10.1.1 ne sera pas gratuite. Utilisez donc la dernière version gratuite dans vos projets.

Pour connaître la dernière version gratuite de PrimeFaces, allez sur à la page http://www.primefaces.org/downloads/. Il faut descendre dans la page jusqu'à la section "Community Downloads" pour avoir les versions gratuites.

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) :

<dependency>
   <groupId>org.primefaces</groupId>
   <artifactId>primefaces</artifactId>
   <version>10.0.0</version>
</dependency>

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.

Remarque : si vous tapez Ctrl-espace entre les balises "version", NetBeans vous propose toutes les possibilités disponibles sur le dépôt Maven.

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 (il vous donne 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, "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.

Vous devriez obtenir ce type d'affichage :

Liste avec PrimeFaces

C'est déjà mieux présenté non ? Ce qui est 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. Allez donc voir les démos et sources sur : http://www.primefaces.org/showcase/ (cliquez sur DataTable dans la section Data). Vous pouvez aussi consulter la documentation complète.

Vous allez utiliser quelques unes de ces possibilités. Après chaque modification rechargez la page pour tester.

  1. Par exemple, 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 (en mémoire, nous verrons plus tard comment faire des requêtes paginées sur la base de données). 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. Ensuite, faites en sorte que les lignes soient triables par nom : attribut sortBy="#{item.name}" dans le tag p:column de la colonne concernée. Attention, pour que le tri fonctionne, le backing bean doit être de portée "vue" et il doit avoir un champ qui contient la liste des objets affichés par la table (ici une liste de Customer).
  3. De même, ajoutez 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. Installez un filtre (filterBy) sur l'état et sur 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.

Voici le code de la page CustomerList.xhtml et le code du backing bean.

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 le tableau 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, j'ai ajouté dans le code ci-dessous un message pour le cas où aucune ligne ne correspondrait à un critère de recherche. J'ai aussi ajouté un champ de recherche global (celui à qui j'ai donné l'id "globalfilter") sur tous les champs de recherche/filtres (donc le nom et l'état si vous avez bien fait tout ce qui vous a été demandé ici) dans l'en-tête de la table (n'oubliez pas l'attribut widgetVar dans l'en-tête de la table). Vous pouvez le tester et chercher les explications dans la documentation PrimeFaces sur <p:dataTable>. Par défaut les lignes sélectionnées sont celles qui contiennent les caractères tapés dans le champ global de recherche, pas seulement s'ils sont au début de la valeur comme pour les filtres sur une seule colonne, mais on peut évidemment changer ce comportement par défaut.

<p:dataTable value="#{customerMBean.customers}" var="item"
             emptyMessage="Aucun client avec ce critère"
             widgetVar="customerTable"
             paginator="true"
             rows="10">
          
  <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 l'exécutez, 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. Comme vous n'avez pas modifié une page JSF ou une classe Java, il faut relancer l'application et pas seulement faire un reload de la page.

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 CustomerDetailsMBean 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 CustomerDetailsMBean comme vous avez fait pour le backing bean de la page CustomerList.xhtml. Voici son code :

package xx.xxxxx.customerApplication.managedbeans; // A MODIFIER...

import entities.Customer;
import java.io.Serializable;
import javax.ejb.EJB;
import javax.faces.view.ViewScoped;
import javax.inject.Named;
import session.CustomerManager;

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

  @EJB
  private CustomerManager customerManager;

  public int getIdCustomer() {
    return idCustomer;
  }

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

  /**
   * Retourne les détails du client courant (celui dans l'attribut customer de
   * cette classe), qu'on appelle une propriété (property)
   */
    public Customer getDetails() {
      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.
    customer = customerManager.update(customer);
    return "CustomerList";
  }

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

La méthode loadCustomer appelle une méthode qu'il va falloir rajouter dans l'EJB 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 CustomerDetailsMBean) :

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

Sauvegardez bien le fichier CustomerDetailsMBean.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 (ne tapez pas le .xhtml final ; il sera rajouté par NetBeans).

Ajout formulaire JSF

Faites un drag and drop de "JSF Form From Entity" (dans la palette) dans le body de la page.

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 méthode du managedBean qui renvoie 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.

Entity pour form

Examinez le code généré dans la page CustomerDetails.xhtml. On voit qu'elle affiche les propriétés du client retourné par la méthode details du backing bean CustomerDetailsMBean. On a, par exemple, value="#{customerDetailsMBean.details.name}"

Comme la méthode getDetails 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 (voir "Ajout section metadata" ci-dessous).

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 comme pour la table de la 1ère page (c'est-à-dire ce qui concerne la propriété "zip") : le label et la liste déroulante (<h:selectOneMenu>) pour la saisie.

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

  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. Il suffira ensuite de 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="#{customerDetailsMBean.idCustomer}"
                required="true"/>
   <f:viewAction action="#{customerDetailsMBean.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> indique à 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 customerDetailsMBean (avec le setter setIdCustomer).

<f:viewAction> indique à JSF de lancer une action représentée par la méthode loadCustomer du backing bean customerDetailsMBean, après avoir rangé l'id dans la propriété idCustomer du backing bean, et avant l'affichage de la page.

Ces 2 actions sont effectuées dans cet ordre lorsque la requête HTTP GET arrive sur le serveur.

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 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="#{customerDetailsMBean.details.name}" title="Name" /> avec la propriété details qui correspond à la variable customer dans le backing bean (getDetails() retourne customer).

Il reste à ajouter le code pour passer de la page CustomerDetails à la page CustomerList mais 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 DiscountCodes dans la page des détails d'un client

Dans la base de données la table des DISCOUNT_CODE 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 généré par NetBeans 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.

La récupération des DiscountCode 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 EJB. Vous allez donc commencer par ajouter un EJB pour gérer les données associées aux DiscountCode dans la base de données. Créez un EJB session sans état DiscountCodeManager pour cela. Inspirez-vous de l'EJB que vous avez déjà créé.

Dans cet EJB ajoutez une méthode List<DiscountCode> getAllDiscountCodes() qui retourne toutes les réductions. Inspirez-vous de la méthode qui retourne tous les clients dans l'EJB CustomerManager. Plutôt que copier/coller le code ci-dessous, essayez de refaire les étapes que vous aviez faites lors de l'écriture du gestionnaire de clients (avec Insert Code > Use Entity Manager).

Ajoutez-y une méthode qui retourne le DiscountCode 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 DiscountCode :

public DiscountCode findById(String discountCode) {
  return em.find(DiscountCode.class, discountCode);
}

Vous devriez avoir ce code (vérifiez que le nom donné pour unitName correspond bien au nom de votre persistence.xml) :

package xx.xxxxx.customerApplication.session;  // A MODIFIER...
          
import entities.DiscountCode;
import java.util.List;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;

@Stateless
public class DiscountCodeManager {

    @PersistenceContext
    private EntityManager em;

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

    public DiscountCode findById(String discountCode) {
        return em.find(DiscountCode.class, discountCode);
    }

    public void persist(DiscountCode discountCode) {
        em.persist(discountCode);
    }
}

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

Finalement, dans le backing bean CustomerDetailsMBean, é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 DiscountCode.
   */
  public List<DiscountCode> getDiscountCodes() {
    return discountCodeManager.getAllDiscountCodes();
  }

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

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 et vous verrez que les DiscountCode 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 var (nom d'une variable qui désigne un des éléments de la liste, c'est-à-dire un DiscountCode) et itemLabel (ce qui sera affiché dans la liste déroulante pour un élément) de <f:selectItems>. Prenez exemple sur var de <h:dataTable> et sur ce qui est affiché pour les DiscountCode dans la page qui contient la liste de tous les clients. Voir https://javaserverfaces.github.io/docs/2.3/vdldoc/f/selectItems.html. Rappel : NetBeans peut vous aider pour compléter les noms d'attributs tels que itemLabel ; vous tapez "i" suivi de Ctrl-[Barre espace].

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 discountCodes :

  <h:button id="back" value="Revenir à la liste" outcome="CustomerList"/>
  <h:commandButton id="update" value="Enregistrer" action="#{customerDetailsMBean.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 discountCode d'un customer. La ligne du discountCode est sans doute entourée de rouge et le formulaire n'est pas soumis. 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 «entities.DiscountCode[ discountCode=N ]» pour «null Converter».". Le problème est que la String "N" (pour cet exemple) a été envoyée au serveur pour le DiscountCode et une String ne peut être affectée à une variable de type DisccountCode ; en effet l'attribut value de <f:selectItems> indique que la valeur "N" va être affectée à #{customerDetailsMBean.details.discountCode} qui est de type DiscountCode comme on le voit dans la classe entité Customer.
    Remarque : pour voir ce qui est envoyé au serveur, vous pouvez lire le code source HTML de la page (Ctrl-U sur la page avec Chrome). En HTML, une liste déroulante est représentée par la balise <select>.

Choix d'un DiscountCode - convertisseur

Il existe plusieurs façons de régler ce problème. Une des façons est d'ajouter un convertisseur pour convertir la String en DiscountCode.

Un convertisseur JSF (converter) permet de transformer une String en un objet (ici de type entities.DiscountCode), et vice versa.

Vous verrez dans le cours plusieurs façons de créer et d'utiliser un convertisseur JSF. Pour ce TP la classe du convertisseur sera une classe Java interne anonyme du backing bean CustomerDetailsMBean, et le convertisseur sera désigné dans <h:selectOneMenu> par la propriété discountCodeConverter de ce backing bean.

Un convertisseur est un objet de type javax.faces.convert.Converter<T>. Attention, n'importez pas la classe du paquetage javax.persistence.

Comme le plus souvent pour une classe qui concerne une entité, ce convertisseur va utiliser l'id de l'entité (l'attribut discountCode de l'entité DiscountCode) pour faire la conversion entre une String et un DiscountCode. Par exemple, la String "2" va être convertie en l'objet DiscountCode qui correspond aux données d'id 2 de la base de données (il faudra donc aller chercher les données dans la base de données) ; la conversion d'un objet DiscountCode en une String est plus simple car il suffit de retourner la valeur de l'id de l'entité.

Puisqu'un accès à la base de données va être nécessaire, l'EJB DiscountCodeManager écrit dans la section précédente sera utilisé (il devrait déjà être injecté dans le backing bean depuis cette question). Ensuite, ajoutez dans le backing bean une méthode getDiscountCodeConverter() qui définira la propriété discountCodeConverter qui sera utilisée dans la page JSF (attribut converter de la balise <h:selectOneMenu>).

Voici le code à ajouter dans le managed bean CustomerDetailsMBean (attention aux importations, il s'agit de la classe Converter du paquetage javax.faces.convert) :

  @EJB
  private DiscountCodeManager discountCodeManager;

  /**
* getter pour la propriété discountCodeConverter.
*/        public Converter<DiscountCode> getDiscountCodeConverter() { return new Converter<DiscountCode>() { /** * Convertit une String en DiscountCode. * * @param value valeur à convertir */ @Override public DiscountCode getAsObject(FacesContext context, UIComponent component, String value) { return discountCodeManager.findById(value); } /** * Convertit un DiscountCode en String. * * @param value valeur à convertir */ @Override public String getAsString(FacesContext context, UIComponent component, DiscountCode value) { return value.getDiscountCode(); } }; }

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 désigner la propriété discountCodeConverter qui correspond à la méthode "getter" getDiscountCodeConverter()).

Voici le code qu'il faut modifier dans CustomerDetails.xhtml (faites les mêmes modifications si vous avez choisi d'utiliser PrimeFaces pour cette liste déroulante) :

   <h:selectOneMenu id="discountCode" value="#{customerDetailsMBean.details.discountCode}"   
                    title="DiscountCode" required="true" 
                    requiredMessage="The DiscountCode field is required."  
                    converter="#{customerDetailsMBean.discountCodeConverter}">   
       <f:selectItems value="#{customerDetailsMBean.discountCodes}"  
                      var="item"   
                      itemLabel="#{item.discountCode} : #{item.rate} %" itemValue="#{item}" />  
   </h:selectOneMenu>

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

En résumé, voici ce qui se passe un peu plus en détails :

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 HHTP et le serveur d'application (ou le serveur HTTP) 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".

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="#{customerDetailsMBean.details.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 getDetails() 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="#{customerDetailsMBean.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 l'EJB session 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 le code de remise (discountCode) : 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 discountCode de type DiscountCode. Lorsque l'utilisateur modifie le discountCode avec le menu déroulant dans la page de détails, une String est envoyée pour ce discountCode. C'est le travail du convertisseur de transformer sur le serveur cette String en objet de type DiscountCode.

Snap40.jpg

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'une erreur se produit ou 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='#{customerDetailsMBean.details != 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 #{customerDetailsMBean.idCustomer} !" rendered='#{customerDetailsMBean.details == null}'/>

Autres améliorations possibles : enlever les informations moins importantes de la liste des clients (par exemple l'adresse). En effet, l'ergonomie est moins bonne si la ligne d'un client ne s'affiche pas en entier sur un écran d'une taille habituelle. Il suffit d'avoir les informations essentielles dans la liste et l'utilisateur peut cliquer sur l'id d'un client s'il veut toutes les informations.

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.

Projet final obtenu comme ceci : clic sur le projet global puis menu File > Export Project > To ZIP...

Pour récupérer un projet exporté sous la forme d'un fichier zip :

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