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.
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.
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" :
.
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.
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 :
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.
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 :
Voyons rapidement le contenu de l'onglet Projects (après avoir déployé les entrées) :
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) :
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 :
src
pour le code source, avec les sous-répertoires main/java
pour les sources des classes Java, main/resources
pour les fichiers de ressources, main/webapp
pour les fichiers liés au Web (pages HTML, pages JSF, fichiers JavaScript,...).target
pour les classes compilées. Tout ce qu'il faut pour créer le jar final (ça sera un war pour un projet "Web") sera enregistré dans ce répertoire. Le jar final sera enregistré dans la racine de ce répertoire.pom.xml
à la racine du projet, fichier essentiel pour Maven, qui décrit le projet. 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.
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.
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.
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 :
<maven.compiler.source>
et <maven.compiler.target>
sont bien toujours à 11 dans pom.xml.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 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 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.
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é :
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 :
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 :
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 :
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.
Commit du projet dans NetBeans :
Push 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 :
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") :
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 :
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 :
getAllCustomers()
qui retourne la liste de tous les customers ;update
qui met à jour un customer dans la base de données. 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 -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 :
Customer
générée par NetBeans (la requête correspond à un "select *" sur les clients). Juste pour voir, vous pouvez utiliser NetBeans (Ctrl-espace à l'intérieur des parenthèses de la méthode createNamedQuery
) pour vous proposer des noms de requêtes (elles n'apparaissent pas nécessairement au début de la liste proposée).merge
dont les finesses seront étudiées dans le cours JPA). Au moment du commit (on verra dans le cours sur les EJB qu'il aura lieu à la fin de la méthode) les modifications apportées à customer
depuis sa création seront enregistrées dans la base de données. Le plus souvent update
génèrera un UPDATE dans la base de données.Customer
ne doit pas correspondre à des données déjà dans la base de données. persist
génèrera un INSERT dans la base de données. 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 ...") :
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.
Clean and Build du projet.
Si tout va bien, commit avec le message "Ajout EJB CustomerManager" et push sur GitHub.
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 :
CustomerList.xhtml
qui affiche la liste de tous les clients dans une table ;CustomerDetails.xhtml
qui affiche les détails sur un client particulier ; elle permet aussi de modifier les informations sur ce client.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é.
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...".
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" :
Ensuite, renseignez :
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 :
getCustomers()
utilise une variable d'instance customerList
. L'avantage d'utiliser la variable customerList
est que, pour des raisons techniques, JSF va appeler plusieurs fois la méthode getCustomers()
pour afficher la table des clients et il faut éviter l'accès à la base de données pour chacun de ces appels (si vous voulez le vérifier, ajouter un println dans la méthode et vérifiez que l'affichage apparaitra plusieurs fois dans les logs du serveur pendant l'exécution).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; } }
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
Production
lorsque l'application sera en production.<welcome-file-list>
).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>
Pour ajouter une page JSF, sur le projet web faire clic droit et New > Other ; dans la catégorie "JavaServer Faces" choisir "JSF Page" :
Puis faites "next" et renseignez le nom de la page (pas de .xhtml final ; il sera rajouté par NetBeans) :
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).
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.
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.#{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. Voici donc la fenêtre avec les bonnes valeurs (le nom du paquetage de Customer sera différent pour vous) :
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
.
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 :
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). <welcome-file-list>
est ajouté pour savoir quelle page afficher (CustomerList.xhtml pour ce cas).localhost:8080/tpCustomerApplication/CustomerList.xhtml
.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 :
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..." :
Vous pouvez vérifier que ce sont bien les mêmes données qui sont affichées dans la page JSF.
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).
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 :
item
est la variable qui représente une ligne de la table (défini par l'attribut "var
" de l'en-tête de la dataTable) ; elle correspond donc à la classe entité Customer
. item.discountCode
correspond à la propriété discountCode
de cette entité (elle correspond à l'appel de la méthode getDiscountCode()
de la classe Customer
qui retourne une instance de DiscountCode
). Donc item.discountCode.discountCode
correspond à la propriété discountCode
de la classe DiscountCode
, donc à l'appel de la méthode getDiscountCode()
de la classe DiscountCode
qui retourne l'id du code de réduction. rate
. On affiche donc l'id du code de réduction suivi du taux de ce code de réduction, et un "%" juste pour faire joli... value
, on aurait aussi pu n'utiliser qu'une seule expression EL avec l'opérateur de concaténation "+=" du langage EL :
#{item.discountCode.discountCode += ' : ' += item.discountCode.rate += '%'}
]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 :
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.
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). ]
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 :
Après ce undeploy, Clean and Build devrait marcher.
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:
<h:dataTable>
et </h:dataTable>
par <p:dataTable>
et </p:dataTable>
<h:column>
et </h:column>
, remplacez les h: par p:. Évidemment utilisez le menu Edit > Replace (ou Ctrl-H) de NetBeans pour cela.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 :
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.
<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é.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).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.
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.
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).
La classe CustomerDetailsMBean
de ce bean contient
idCustomer
pour récupérer le paramètre idCustomer
de l'URL (CustomerDetails.xhtml?idCustomer=2
) ;loadCustomer()
pour récupérer un client à partir de idCustomer
(en utilisant l'EJB CustomerManager
), ainsi qu'une propriété customer
pour conserver ce client ;details
(on n'a que la méthode getDetails
, pas setDetails
) qui retourne le client récupéré par loadCustomer()
, dont les détails seront affichés par la page CustomerDetails
;update()
qui sera utilisée pour enregistrer les modifications apportées au client par l'utilisateur de l'application. Comme cette méthode retourne "CustomerList", la page CustomerList.xml
sera affichée après son exécution ; c'est la façon de fonctionner de JSF comme vous le verrez dans le cours.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.
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).
Faites un drag and drop de "JSF Form From Entity" (dans la palette) dans le body de la page.
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.
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.
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
idCustomer
du backing bean. 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é.
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.
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 :
L'affichage est correct. Il reste à ajouter les 2 boutons pour revenir à la liste et soumettre le formulaire.
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 :
<h:button>
), on ne soumet pas les éventuelles modifications faites par l'utilisateur et on revient à la liste des clients (requête GET).<h:commandButton>
) on soumet le formulaire par une requête POST et on appelle update()
; les modifications sont enregistrées dans la base de données et on revient ensuite à la liste des clients (car la méthode update()
retourne "CustomerList").<h:button>
pour un bouton qui génère une requête GET et <h:commandButton>
pour un bouton qui génère une requête POST (en soumettant les valeurs saisies dans un formulaire).Testez :
<h:messages/>
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
. <select>
.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 :
value
de <h:selectOneMenu>
. value
de <f:selectItems>
. itemLabel
; la valeur qui est envoyée au serveur est déterminée par l'attribut var
puisqu'il n'y a pas d'attribut itemValue
.converter
) est d'abord utilisé au moment où la page est générée sur le serveur avant d'être envoyée au navigateur en réponse à la requête GET. Si on regarde le code source de la page, on aura, par exemple "<option value="M">M : 11.00 %</option>
". La valeur "M" a été obtenue en utilisant la méthode getAsString
du convertisseur pour passer de DiscountCode
(itemValue="#{item}"
) à String
.String
envoyée au serveur ("M" si l'option écrite ci-dessus a été choisie) est traduite en DiscountCode
en utilisant la méthode getAsObject
du convertisseur. Cette valeur est donnée à la variable d'instance discountCode
de Customer
et donc le nouveau code de réduction sera enregistré dans la base de données au moment du commit qui suivra la méthode update
de l'EJB session (appelée par la méthode update
du backing bean).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
.
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 :
Dans le prochain TP et en cours nous verrons comment corriger ces problèmes avec le modèle PRG.