Vous allez écrire une application qui permet de gérer des clients (customers en anglais) qui sont enregistrés dans une base de données.
Ce TP permet une prise de contact directe avec les technologies Maven, Git, CDI, JPA, et JSF.
Il est conseillé de terminer ce TP avant le début des cours. Vous aurez ainsi plus de facilités à comprendre le cours car vous aurez des illustrations concrètes des notions exposées dans le cours. Prenez le temps de bien lire les explications.
Il ne servirait à rien de terminer le TP en recopiant le code donné dans le TP, sans essayer de comprendre les explications. Ne vous inquiétez pas si vous ne maitrisez pas tout car tous les points abordés dans ce TP seront approfondis dans les différents cours et TPs à venir.
IMPORTANT : s'il vous est demandé de finir un TP et de le mettre dans un repository GitHub, le projet doit contenir tous les commits demandés dans le TP, et tous les commits après chaque étape importante du TP. Attention, à bien cliquer sur le projet avant de faire un commit pour que tous les fichiers modifiés soient pris en compte. Un projet qui ne contient pas ces commits ne sera pas pris en compte.
Parties optionnelles : ce TP et les suivants comportent des parties optionnelles pour approfondir certains points du cours. L'examen ne pourra pas porter sur ces parties optionnelles qui, cependant, pourront vous être utiles si vous souhaitez développer un projet Jakarta EE.
Si vous ne l'avez pas déjà fait il est temps d'installer les logiciels.
Rappel : les captures d'écrans de ce TP peuvent ne pas correspondre exactement à ce que vous verrez car les options proposées peuvent varier selon les versions des logiciels utilisés.
Dans ce TP vous allez développer une application de type "Web", qui correspond au profile Web de Jakarta EE. Un profile est un sous-ensemble de la plateforme Jakarta EE complète.
Lancez NetBeans.
Vous allez créer un projet Maven. Maven est le logiciel qui va vous permettre de créer le projet, de le construire (construire en particulier le fichier jar pour l'exécution du projet), de gérer les dépendances du projet (par exemple, le projet dépendra de l'API Jakarta EE). Les fichiers jar des dépendances utilisées par le projet seront récupérés automatiquement par Maven sur l'entrepôt (repository) central de Maven et enregistrés dans l'entrepôt local de Maven qui est sur votre ordinateur. Sous NetBeans vous pourrez voir la liste de ces fichiers dans l'onglet "Services", entrée "Maven Repositories".
Si toutes les dépendances utilisées par votre projet ne sont pas déjà dans l'entrepôt local, vous devez être connecté à Internet pour créer le projet.
Pour créer un projet "Web" utilisez le menu File > New Project de NetBeans puis choisissez la catégorie "Java with Maven", et un projet de type "Web Application" :
.
Cliquez sur Next.
Une nouvelle fenêtre pour la création du projet est affichée. Choisissez l'emplacement du projet et son nom. Il vaut mieux ne pas mettre de caractères particuliers dans le chemin ou dans le nom, pas d'accents (é, à,...) par exemple. On ne sait jamais... ;-).
Modifiez le nom du projet : par exemple tpCustomerXxxxx ; Xxxx pour votre nom ; arrangez-vous pour que ce nom vous identifie parmi tous les autres étudiants de votre classe ; relisez ce guide. Ce nom sera le nom affiché par NetBeans pour le projet.
Si vous voulez regrouper tous les projets de ce cours dans un répertoire spécial, désignez-le en cliquant sur le bouton "Browse". Ce répertoire sera placé au-dessus des répertoires qui contiendront les projets.
Modifiez aussi "Group Id" (prenez exemple sur l'image ci-dessous, en adaptant à votre cas). Group Id devrait identifier l'organisme qui a créé le projet, si on l'enregistrait sur l'entrepôt central de Maven. Ça ne sera pas votre cas et vous avez donc la liberté de choisir ce que vous voulez mais il déterminera le début des noms des paquetages des classes de votre projet et je vous conseille donc d'utiliser un nom significatif. Optionnel : voir https://maven.apache.org/guides/mini/guide-naming-conventions.html.
NetBeans attribue automatiquement la valeur de "Artefact Id" à partir du nom du projet.
Le nom du jar généré par la construction du projet (le build) sera formé avec la valeur de "Artifact Id" suivi du numéro de version. Ça sera un fichier war pour une application Jakarta EE Web.
Par défaut, la base des noms des paquetages des classes générées est définie par les valeurs de "Group Id" et du nom du projet (sans majuscules). Vous pouvez modifier le paquetage de base, par exemple en enlevant votre nom à la fin.
Par convention de Maven, le nom de la version est suffixée par SNAPSHOT si la version est encore en développement.
Une fois le projet créé, si vous le souhaitez, vous pourrez modifier indépendamment les valeurs de GroupId, ArtifactId, Version, Name par un clic droit sur le projet et Properties. Vous pourrez aussi changer le répertoire qui contient le répertoire du projet par un clic droit sur le projet et Move.
Cliquez sur Next.
Dans la fenêtre suivante, indiquez le nom du serveur (Payara 6 que vous avez installé) et la version Jakarta EE 10 Web (ne tenez pas compte de cette ancienne capture d'écran) :
Cliquez ensuite sur le bouton "Finish". NetBeans génère alors un projet pour votre application. Le temps mis dépend des artefacts (des jars) que Maven devra charger de l'entrepôt central.
Le message "BUILD SUCCESS" doit apparaître dans la fenêtre "Output - Project Creation" en bas de la fenêtre de NetBeans.
Le code généré par NetBeans produit une application REST très simple, avec aussi une page Web qui affiche un message du type "Hello World!" (ce message dépend de la version de NetBeans). Vous pourriez supprimer du code généré mais il n'est pas gênant pour la suite et je vous conseille de le garder.
Dans la partie gauche de la fenêre de NetBeans, 3 onglets Projects, Files et Services contiennent les informations sur une vue du projet, les fichiers du projets (y compris ceux qui ont été utilisés par NetBeans) et sur les services offerts aux applications: bases de données, serveurs d'application, etc.
Ces onglets peuvent être en haut ou à gauche de la zone gauche. S'ils sont à gauche, il suffit de déplacer la souris sur un des onglets pour faire afficher son contenu dans la zone de NetBeans en haut à gauche. Pour conserver cette zone ouverte, il suffit de cliquer sur les 2 carrés superposés en haut à droite de la zone; pour ne plus la voir (et donc avoir plus de place pour, par exemple, le code d'un fichier), il suffit de cliquer sur le trait horizontal en haut à droite de la zone. Un double clic affiche la zone sur tout l'espace dédié à NetBeans ; un nouveau double clic remet la zone à sa taille initiale. Ce type d'action fonctionne pour toutes les zones affichées par NetBeans. Vous pouvez fermer une des fenêtres, par exemple la fenêtre des projets, en cliquant sur la croix en haut à droite de la fenêtre ; pour rouvrir ensuite la fenêtre, utilisez le menu "Window" en haut de NetBeans.
L'onglet Services vous sera utile pour gérer le SGBD MySQL et le serveur d'application Payara :
Voyons rapidement le contenu de l'onglet Projects (après avoir déployé les entrées) :
Le projet créé contient des fichiers de configuration :
bean-discovery-mode="all"
de la balise beans indique que toutes les classes pourront être considérées comme des beans CDI (pourront être injectées et pourront injecter des beans). Par défaut, la valeur de cet attribut est "annotated"
, ce qui signifie que seules les classes qui ont une annotation CDI (par exemple une portée CDI) seront considérées comme des beans CDI.L'onglet Files (Menu Window > Files s'il n'est pas affiché) présente la structure des fichiers du projet. Vous ne verrez pas tous les fichiers de la copie d'écran ci-dessous, car ils seront écrits plus loin dans ce TP, mais vous verrez la plupart des répertoires :
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. Pour générer ce fichier jar du projet, il faut lancer une clean and build du projet.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.
L'onglet Project montre la structure "logique" du projet, pas la structure physique montrée par l'onglet Files. Par exemple, dans l'onglet Projects vous trouverez une entrée pour les dépendances du projet (les jar qu'il utilise)
Vous travaillerez le plus souvent avec l'onglet Projects.
Maven facilite la création et la gestion des projets Java. Nous n'aurons pas le temps de l'étudier en détails dans ce cours mais il vous sera sans doute nécessaire d'en connaître au moins les bases pour développer plus tard vos propres projets.
Le fichier pom.xml
(Project Object Model) contient des informations sur le projet. Il est écrit par le développeur (ou généré par l'IDE) et il est utilisé par Maven pour gérer le projet, par exemple pour construire le projet (build), en particulier pour compiler les classes Java. Les dépendances qui n'existent pas déjà en local seront automatiquement téléchargées depuis l'entrepôt central de Maven. La plupart des librairies "classiques" utilisées par des projets Java sont enregistrées dans cet entrepôt central. Si une librairie n'est pas fournie par le serveur, elle est intégrée au fichier d'archive de l'application. Les librairies sont identifiées par les valeurs de groupId
et de artifactId
.
pom.xml
contient plusieurs sections :
<groupId>
, <artifactId>
suivi de la version du projet (SNAPSHOT indique qu'il est encore en cours de développement), du type de packaging (<packaging>
qui indique que le fichier généré sera un fichier ".war", fichier "jar" pour les applications web).jakartaee
définie la version de Jakarta EE qui sera utilisée.<scope>provided</scope>
), Payara pour votre cas. Remarquez l'utilisation de la valeur de la propriété jakartaee : ${jakartaee}
. Vous ajouterez d'autres dépendances pendant l'écriture du projet. Vous pouvez aussi voir ces dépendances dans la vue du projet fournie par l'entrée Dependencies de l'onglet Projects. Vous pouvez voir les paquetages et les classes fournies par ces dépendances en ouvrant les entrées de chaque jar. <build>
permet de configurer les plugins utilisés par Maven. Par exemple, il peut y avoir une configuration du plugin qui permettra la compilation des classes Java (les plugins utilisés dépendent de l'archétype utilisé pour générer le projet). Vous devez donner les dernières versions des plugins pour éviter des erreurs. Par exemple, pour le plugin "maven-compiler-plugin", effacez le numéro de version et tapez Ctrl-[Espace] (Command-[Espace] sous macOS) à l'intérieur de la balise <version>
, et choisissez la dernière version. N'oubliez pas de le faire pour tous vos nouveaux projets. Une façon sure d'avoir la toute dernière version d'un plugin Maven ou d'un artefact Maven (fichier jar le plus souvent) est de chercher l'entité sur https://central.sonatype.com/ ou sur https://mvnrepository.com/. NetBeans récupère les versions en arrière-plan à intervalles réguliers et il peut parfois ne pas fournir la toute dernière version.Remarque importante : si vous faites un copier/coller dans votre code, reformattez le code (menu Source > Format ou Alt-Maj-F). Pour avoir un code plus facile à lire, faites-le pour tout copier/coller, dans un fichier Java, XML, HTML ou autre. Si vous avez besoin d'aide et que vous envoyez du code pour vous faire aider, la moindre des choses est d'envoyer un code bien formatté.
Cette version peut-être définie de plusieurs façons. Soit par une propriété, soit dans la configuration du plugin Maven de compilation.
Donnez la valeur "17" pour le source et le target. Le compilateur utilisera toutes les possibilités de Java 17 et produira du bytecode Java 17.
Dans l'onglet Projects, clic droit sur le projet, et Properties pour voir les propriétés du projet.
Les propriétés qui vous seront le plus souvent utiles :
Pour vérifier si tout va bien, faites un clean and build du projet (clic droit sur le projet et Clean and Build). Une fenêtre "Output - Build" s'affiche. Vous pouvez avoir un message d'erreur, par exemple sur un des plugins utilisés par Maven si vous n'avez pas changé les versions des plugins.
N'oubliez pas de faire ces modifications pour les projets des autres TPs.
Si vous faites un clic droit sur le projet et choisissez Run, Payara est lancé (s'il ne l'était pas déjà) et une page doit s'ouvrir sur votre navigateur préféré avec l'affichage "Hello World!". Le lancement de Payara peut prendre un peu de temps. Un onglet pour Payara doit être ouvert en bas dans "Output". Cet onglet contient les logs de Payara et il sera très utile pour comprendre les éventuels bugs et problèmes lors du développement.
Une page s'ouvre dans le navigateur. Son URL (<context-path>
est le nom affiché par clic droit sur le projet et Properties > Run dans le champ "Context Path") :
http://localhost:8080/<context path>/
. C'est le contenu de la page Web index.html de votre projet qui est affiché (valeur par défaut si un nom de page n'est pas donné dana l'URL).
Si ce message ne s'affiche pas c'est que vous avez fait une erreur. N'allez pas plus loin dans le TP avant d'avoir réparé cette erreur.
Un peu de REST en action avant de passer à la suite.
Git est inclus dans NetBeans.
Cette page décrit comment utiliser Git pour gérer les versions de votre code, et GitHub pour sauvegarder ces versions à distance. Vous devez appliquer ce qui y est dit avant d'aller plus loin.
Notez bien : Si vous voulez profiter des bonus accordés pour votre travail dans les TPs, vous devez effectuer tous les commits, à chacune des étapes importantes des TPs. Un projet GitHub sans ces commits ne pourra pas vous rapporter ces bonus.
Vous allez utiliser la base de données relationnelle "customer" que vous avez définie pendant l'installation des logiciels.
Comme vous le verrez dans le cours sur JPA, vous ne manipulerez pas des données directement en SQL. Les données seront manipulées sous la forme d'objets particuliers qu'on appelle des entités, instances d'une classe entité. Dans les cas les plus simples, une classe entité correspond à une table, porte le même nom (aux majuscules/minuscules près), et ses attributs correspondent aux colonnes de la table. Une instances d'une classe entité correspond souvent à une ligne dans cette table.
La base de données sera gérée par le SGBD MySQL. MySQL doit être déjà démarré. Démarrez-le si ça n'est pas déjà fait (avec un service Windows, voir installation de MySQL).
Il existe un wizard de NetBeans pour générer des classes entités à partir de tables de la base de données "customer". Vous allez utiliser ce wizard pour générer les classes entités qui correspondent à la table CUSTOMER et aux 2 tables DISCOUNT et MICRO_MARKET qui y sont associées
Pour le lancer, clic droit sur le projet puis New > Other > Persistence > Entity Classes from Database. Cochez "Local Data Source" et désignez la connexion à la base "customer" que vous avez créée pendant l'installation des logiciels dans la section "Intégration de MySQL à NetBeans" de la page "MySQL". Les 3 tables customer, discount et micro_market doivent apparaitre dans la zone "Available Tables". Clic sur "customer" et ensuite sur "Add". Les 3 tables doivent apparaitre dans la zone "Selected Tables" car la case "Include Related Tables" est cochée. Clic sur Next.
Changez le nom du package. Donnez le nom du sous-paquetage "entity" du paquetage de base du projet : ajoutez ".entity" au nom proposé (par exemple, pour moi c'est fr.grin.tpcustomer.entity). Clic sur Next.
Clic sur Finish.
Vérifiez que les 3 classes entités ont bien été créées dans le package "entity" de votre projet.
[ Remarque : si vous n'arrivez pas à faire fonctionner ce wizard, ajoutez directement ces 3 classes entités dans votre projet et passez à la suite. ]
Etudiez le code des classes entités :
@Entity
.@Table
indique à quelle table de la base de données est liée l'entité.@NamedQuery
).name
, de type String
, de la classe Customer
, correspond à la colonne NAME
, de type VARCHAR(30)
.@Id
correspond à la clé primaire de la table relationnelle.@Column
indique le nom de la colonne de la table qui correspond à chaque champ de la classe. Cette annotation pourrait aussi contenir d'autres informations sur la colonne ; par exemple, une autre définition que la définition par défaut si la table est créée.@ManyToOne
ou @OneToMany
traduisent pour JPA les associations entre les tables relationnelles Customer, Discount et Micromarket. Par exemple, un Customer est associé à 0 ou 1 Discount et un Discount peut être associé à plusieurs Customers. Dans le code de entités générées, les associations sont bidirectionnelles : dans Discount il y a un champ pour une collection de Customer et dans Customer il y a un champ pour un Discount. Dans le cours sur JPA vous apprendrez comment modifier le code pour n'avoir qu'une association unidirectionnelle de Customer vers Discount, si on n'a pas besoin de l'association dans l'autre sens, de Discount vers Customer.Modifications dans le code de l'entité Customer pour améliorer la lisibilité du code Java :
discountCode
en discount
. Ne changez pas l'attribut name
de l'annotation @JoinColumn
qui indique le nom dans la table relationnelle. Utiliser Ctrl-R de NetBeans pour changer le nom partout dans l'entité (3 endroits à changer). Une option vous permet de changer aussi les noms des getter et setter associés à ce champ : getDiscount
et setDiscount
au lieu de getDiscountCode
et setDiscountCode
.Discount
, changez la valeur de l'attribut mappedBy
de l'annotation @OneToMany
en "discount" (au lieu de "discountCode"). En effet, cette valeur désigne "l'autre bout" de l'association entre Customer
et Discount
, que vous venez de renommer.Regardez comment les attributs des classes correspondent aux colonnes des tables. Pour voir la structure de la base de données, cliquez sur l'onglet "Services" en haut à gauche (on a travaillé jusqu'à maintenant avec l'onglet "Projects") sous l'entrée de la connexion pour customer de Databases. Vous pouvez voir les données enregistrées dans les tables par un clic droit sur une table ; vous choisissez "View Data...".
Sous les noms des colonnes retrouvez aussi les colonnes clés étrangères dans l'entrée "Foreign Keys". Elles correspondent aux champs "associations" entre les entités.
Faites afficher le contenu du fichier persistence.xml. Ce fichier est dans le projet, entrée Other Sources > src/main/resources > META-INF/persistence.xml. Son emplacement suit les conventions de Maven ; vous pouvez voir son emplacement dans l'onglet "Files" ; si cet onglet n'apparait pas, utilisez le menu Window de NetBeans pour le faire apparaitre.
Si vous faites un double clic sur l'entrée du fichier, vous pouvez le voir de 2 façons : Design (vue "logique" du fichier) ou Source (vue XML). Cliquez sur Source. Le contenu de persistence.xml
devrait être (ça dépend de la version de NetBeans que vous utilisez) :
<?xml version="1.0" encoding="UTF-8"?> <persistence version="3.0" xmlns="https://jakarta.ee/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd"> <!-- Define Persistence Unit --> <persistence-unit name="my_persistence_unit"> </persistence-unit> </persistence>
Modifiez le nom de l'unité de persistance, par exemple customerPU, à la place de my_persistence_unit.
Pour la suite NetBeans devrait vous aider à de nombreuses reprises en complétant ce que vous écrivez. Profitez-en, ce qui vous évitera des fautes de frappe.
Il faut ajouter l'information sur la source de données. Payara doit être démarré (clic droit sur Payara et Start sinon). Le nom de la source de données, qui correspond à la base de données customer et qui est déclarée dans le serveur Payara, se trouve dans l'onglet Services, entrée Servers pour Payara : ouvrez l'entrée Resources > JDBC > JDBC Resources. Clic droit sur la ressource "jdbc/customer" et Properties. On voit que cette ressource utilise le pool de connexions JDBC nommé mySQLPool. Les informations sur ce pool de connexions se trouvent dans les propriétés sous l'entrée "Connection Pool". On voit que la base de données est bien customer que l'on a utilisé pour générer les entités.
Il faut aussi ajouter l'information sur le fournisseur de persistance (provider) qui est EclipseLink (information fournie dans la documentation de EclipseLink), implémentation de JPA. Jakarta EE est un ensemble de spécifications qui doivent être implémentées dans des produits. Hibernate et Eclipse Link sont 2 implémentations de JPA.
Enfin, ajoutez la propriété non standard eclipselink.logging.level de EclipseLink. Le niveau FINE permet de voir les requêtes SQL générées par JPA, ce qui peut être bien utile en cas de mauvaises performances.
Le fichier persistence.xml devient :
<?xml version="1.0" encoding="UTF-8"?> <persistence version="3.0" xmlns="https://jakarta.ee/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd""> <!-- Define Persistence Unit --> <persistence-unit name="customerPU"> <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider> <jta-data-source>jdbc/customer</jta-data-source> <properties> <property name="eclipselink.logging.level" value="FINE"/> </properties> </persistence-unit> </persistence>
Sans entrer dans les détails :
Remarque importante : L'association entre le nom "jdbc/customer" et la base de données "customer" ne peut pas se voir uniquement en lisant le fichier persistence.xml. Si on veut la base de données, il faut aller dans l'entrée Servers > Payara > Resources > JDBC, clic bouton droit et Properties puis regarder les propriétés de mySQLPool dans "Connection Pools" sous JDBC.
Faite un "Clean and Build" du projet.
S'il n'y a pas d'erreurs, 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 systèmatiquement rappelée mais, à chaque étape importante de l'écriture de l'application, utilisez Git pour garder une version :
On nomme bean CDI une instance d'une classe, que l'on peut injecter avec CDI dans une autre classe ou dans une page JSF. "Bean CDI" peut aussi désigner la classe de l'instance, suivant le contexte.
Dans une application Jakarta EE il est bon de séparer les tâches. On va centraliser la gestion des Customers dans un bean CDI. De manière classique on crée une "façade" pour les opérations élémentaires sur les clients, de type "CRUD" : création (Create), recherche (Research), modification (Update), suppression (Delete).
Quand il s'agit de gérer les données d'une base de données, on fait appel à des beans CDI avec des méthodes annotées par @Transactional
, car elles ont des facilités pour travailler avec une base de données relationnelle, en délégant au serveur d'application le traitement des transactions.
Ajoutez une nouvelle classe au projet : clic droit sur le projet et New > Java Class. Donnez le nom de la classe CustomerManager et le paquetage (sous-paquetage "service" du paquetage de base du projet).
Ecrivez ce squelette de code pour la classe. N'oubliez pas d'ajouter un commentaire javadoc au début de toutes vos classes :
package xx.xxxxx.xxxxx.service; // A MODIFIER suivant le paquetage de base... import jakarta.enterprise.context.RequestScoped; /** * Façade pour gérer les Customers. * @author xxxx */ @RequestScoped public class CustomerManager { }
L'annotation @RequestScoped
indique qu'une instance de la classe sera un bean CDI (elle pourra être injectée dans une autre classe). Si elle est créée pendant le traitement d'une requête HTTP, elle sera supprimée quand la réponse à la requête sera retournée au client HTTP. La portée @SessionScoped
aurait aussi pu être choisie pour indiquer que le bean CDI durera jusqu'à la fin de la session HTTP.
Vous pouvez écrire la ligne pour importer l'annotation ou bien vous pouvez écrire d'abord l'annotation. NetBeans vous prévient alors d'un problème en affichant un rond rouge à gauche de la ligne. Si passez la souris sur ce point (sans cliquer), le message "cannot find symbol class RequestScoped" s'affiche. Si vous cliquez sur le rond rouge, NetBeans vous propose des solutions pour résoudre le problème. Cliquez sur "Add import for jakarta.enterprise.context.RequestScoped".
Une autre façon de faire : tapez "@Requ" et Ctrl-Barre espace. NetBeans vous propose alors de compléter. Faites un double clic sur RequestScoped ou descendez dessus avec les flèches, et [Enter].
Vous pouvez aussi écrire @RequestScoped et taper Ctrl-Shift-I (ou -Shift-I pour les Mac) pour que NetBeans vous propose des importations (vous pouvez aussi faire un clic droit et fix imports). S'il n'y a pas plusieurs propostions, les importations sont directement écrites.
Attention à ne pas aller trop vite ! Si la fenêtre "Fix All Imports" s'affiche, c'est qu'il peut y avoir plusieurs possibilités pour certains imports, et le choix proposé en premier peut ne pas être le bon.
Ajoutez 2 méthodes au bean CDI, 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. L'annotation @Transactional
de la méthode update
est obligatoire car la méthode va modifier des données dans la base de données et toute modification doit être obligatoirement faite dans une transaction. Dans certaines conditions il peut arriver d'avoir à annoter une méthode qui correspond à un "select" dans la base de données, comme la méthode getAllCustomers()
, mais ça ne sera pas nécessaire pour ce TP.
Remarque importante pour toutes vos classes : lorsque vous écrivez votre code, et en particulier si vous faites un copier/coller pour ajouter du code, n'oubliez pas de bien respecter les indentations des lignes préconinées par le langage Java. Avec un IDE c'est très simple car une commande permet d'indenter automatiquement une classe. Avec NetBeans, il suffit de cliquer sur le menu "Source" et de choisir "Format" ; le raccourci clavier est Alt-Shift-F. Lorsque les classes de vos projets seront examinées, n'obligez pas le correcteur à le faire pour vous, ça va le rendre de mauvaise humeur ! ;-)
@RequestScoped public class CustomerManager { public List<Customer> getAllCustomers() { return null; } @Transactional public Customer update(Customer customer) { return null; } }
Faites les importations automatiquement comme décrit ci-dessus. Remarquez que les importations ne sont pas écrites tout de suite car il y a plusieurs possibilités pour "List". Cliquez "OK" car java.util.List convient. Sinon, il suffit de faire un autre choix dans la liste déroulante avant de cliquer sur "OK".
Vous devriez avoir un code source comme celui-ci :
package xx.xxxxx.xxxxx.service; // A MODIFIER suivant le paquetage de base... import xx.xxxxx.xxxxx.entity.Customer; import java.util.List; import jakarta.enterprise.context.RequestScoped; import jakarta.transaction.Transactional; @RequestScoped public class CustomerManager { public List<Customer> getAllCustomers() { return null; } @Transactional public Customer update(Customer customer) { return null; } }
Maitenant vous allez modifier le contenu de ces méthodes. Les explications détaillées seront vues dans le cours sur JPA.
Commencez par injecter un entity manager (gestionnaire d'entités) JPA. C'est un objet lié à l'unité de persistance référencée par le fichier persistence.xml. Il va servir à envoyer des requêtes, insérer/supprimer/modifier des données dans la base de données, en utilisant des instances des classes entités que vous avez créées au début de ce TP.
Au début du corps de la classe, ajoutez l'injection de l'EntityManager (c'est l'annotation @PersistenceContext
qui provoquera l'injection) et une méthode persist
pour ajouter un nouveau customer dans la base de données, avec les valeurs contenues dans l'entité passée en paramètre :
@PersistenceContext(unitName = "customerPU") private EntityManager em; @Transactional public void persist(Customer customer) { em.persist(customer); }
La variable em
ne doit pas être initialisée car sa valeur va être "injectée" par le serveur d'application. C'est comme si le code disait "Serveur d'application, donne-moi un objet pour gérer les données de la base customer.".
L'annotation et la classe sont dans le paquetage jakarta.persistence
.
"customerPU", correspond au nom donné dans le fichier persistence.xml ; il correspond à l'unité de persistance qui représente la source de données de nom JNDI "jdbc/customer". Cette source de données correspond à la base de données "customer" de MySQL (dans l'installation des logiciels vous avez indiqué cette correspondance lorsque vous avez défini le pool de connexions sur lequel s'appuie la ressource JDBC). Comme c'est la seule unité de persistance définie dans persistence.xml, vous auriez pu écrire plus simplement
@PersistenceContext private EntityManager em;
Modifiez getAllCustomers
et update
pour qu'elles jouent leur rôle :
Customer
déjà écrite (la requête correspond à un "select *" sur les clients).merge
dont les finesses seront étudiées dans le cours JPA). Au moment du commit qui aura lieu à la fin de la méthode, comme vous le verrez dans le cours sur CDI, 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 SQL 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 SQL 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 "." (s'il connait l'objet à qui est envoyé le message). Essayez avec la méthode getAllCustomers.
package xx.xxxxx.xxxxx.service; // A MODIFIER suivant le paquetage de base... import xx.xxxxx.xxxxx.entities.Customer; import java.util.List; import jakarta.enterprise.context.RequestScoped; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; import jakarta.transaction.Transactional; /** * Gère la persistance des Customers. */ @RequestScoped public class CustomerManager { @PersistenceContext(unitName = "customerPU") private EntityManager em; public List<Customer> getAllCustomers() { Query query = em.createNamedQuery("Customer.findAll"); return query.getResultList(); } @Transactional public Customer update(Customer customer) { return em.merge(customer); } @Transactional public void persist(Customer customer) { em.persist(customer); } }
Importez la classe jakarta.persistence.Query
(utilisée dans getAllCustomers
). Ne vous trompez pas de paquetage.
N'oubliez pas d'ajouter un commentaire javadoc sur la classe. Il est bon d'ajouter un commentaire javadoc à toutes les classes d'un projet, ainsi qu'à chaque méthode dont le code n'est pas évident à comprendre. Vous pouvez d'ailleurs le faire pour les 3 entités, si ça n'est déjà fait.
Voilà, on a terminé pour la partie bean CDI transactionnel (processus métier et accès aux bases de données) dans ce projet. On va passer au front-end (interface utilisateur) web.
Clean and Build du projet.
Si tout va bien, commit avec le message "Ajout CustomerManager" et push sur GitHub.
Dans cette partie on va utiliser JSF, le framework standard MVC de Jakarta EE, qui se base sur l'utilisation de backing beans et de pages JSF écrites avec des composants.
Une page JSF est en gros une page HTML avec des balises HTML et des balises qui représentent des composants JSF similaires aux composants HTML (par exemple une liste déroulante ou une table) ou plus complexes (par exemple un calendrier ou un arbre). Elle peut avoir des emplacements délimités par #{ ... }
, qui contiennent des expressions "EL".
EL (Expression Language) est un langage qui permet de lier des propriétés ou méthodes de classes Java à une page JSF. Une expression EL peut désigner une propriété d'un backing bean dont la valeur est affichée dans la page ou dont la valeur est saisie par l'utilisateur dans un formulaire. Une propriété d'un Java Bean est définie par un getter et/ou par un setter ; par exemple la propriété "nom" sera définie par les méthodes String getNom()
et/ou void setNom(String nom)
. Une expression EL peut aussi désigner une méthode du backing bean qui sera exécutée à la suite d'une action de l'utilisateur (par exemple quand l'utilisateur soumet un formulaire).
Une page JSF ne contient pas de code Java. Pour exécuter du code elle utilise un (ou plusieurs) backing bean, instance d'une classe Java. Un backing bean (backing = épauler, aider, en anglais) s'appelle aussi un bean géré (managed bean) car il est géré par le container CDI du serveur d'application. C'est le container CDI qui le crée automatiquement et l'injecte quand l'application en a besoin (injection, comme pour l'EntityManager qu'on a vu plus haut dans le bean CDI). C'est aussi le container qui le supprime quand il le faut (durée de vie liée à la portée du backing bean) comme on le verra dans les cours sur CDI et JSF.
Un container CDI peut aussi gérer d'autres instances Java, indépendantes d'une page JSF. Une classe Java peut demander à CDI d'injecter une instance d'une autre classe Java. Par exemple, des instances de CustomerManager
seront injectées dans des backing beans.
L'application aura 2 pages JSF :
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 activer JSF, vous allez ajouter une classe CDI et l'annoter avec @FacesConfig
. Il y a d'autres moyens d'activer JSF. Une telle classe de configuration permet d'ajouter d'autres configurations, par exemple pour la sécurité.
Ajoutez donc une nouvelle classe ConfigJSF
que vous placez dans le sous-paquetage config
du paquetage de base du projet.
L'annotation CDI @ApplicationScoped
permet d'indiquer que la classe sera gérée par CDI et qu'une instance de la classe durera tout le temps que l'application s'exécutera.
package xx.xxxxx.xxxxx.config; // A MODIFIER suivant le paquetage de base... import jakarta.enterprise.context.ApplicationScoped; import jakarta.faces.annotation.FacesConfig; /** * Configuration JSF * @author xxxx */ @ApplicationScoped @FacesConfig public class ConfigJSF { }Pour ajouter la classe vous pouvez faire un clic droit sur "Source Package" et choisir New > "Java Class...".
Vous allez commencer par écrire le backing bean CustomerBean
, qui sera utilisé par la page JSF customerList.xhtml
.
Par exemple, pour afficher la liste des customers elle aura besoin d'aller chercher les données dans la base de données. Ce backing bean aura une méthode getCustomers()
pour retourner la liste de tous les clients (ce "getter" définira la propriété customers
).
Le backing bean sera géré par le container CDI du serveur d'application, qui pourra le créer automatiquement, en particulier quand il sera référencé par une page JSF, par exemple si la page JSF contient l'expression EL #{customerBean.customers}
qui référence la propriété customers
du bean. Quand la valeur de la propriété customers
devra être lue, la méthode getCustomers()
sera appelée. customerBean
est le nom JSF du backing bean qui est donné par l'annotation @Named
de la classe du backing bean.
Pour créer ce backing bean vous allez utiliser un "wizard" de NetBeans.
Sur le projet faites clic droit et New > Other ; dans la catégorie "JavaServer Faces" choisissez "JSF CDI Bean" :
Ensuite, clic sur Next et renseignez :
CustomerBean
;Clic sur Finish.
Le code de la classe CustomerBean
devrait être le code ci-dessous. Remarquez l'annotation @Named
qui donne le nom CDI qu'il faudra utiliser dans une page JSF pour désigner ce backing bean dans une expression EL : #{customerBean.customers}
désignera la propriété customers
du backing bean. "@Named
" aurait suffi plus simplement puisque "customerBean" est le nom donné par défaut au backing bean (le nom de la classe, avec une minuscule au début).
La portée "view" oblige à implémenter Serializable
car le bean pourra être sérialisé en mémoire secondaire si le container l'estime nécessaire. Une plus courte portée (comme la portée requête que vous avez déjà utilisé pour CustomerManager
), donc une plus courte durée de vie, ne l'imposerait pas.
Ajoutez un commentaire Javadoc : "Backing bean pour la page JSF customerList."
Pour écrire la méthode getCustomers()
, vous allez ajouter du code pour que le bean puisse communiquer avec le bean CDI CustomerManager
déjà écrit. Insérez les lignes suivantes au début du corps de la classe :
@Inject private CustomerManager customerManager;
L'annotation @Inject
permettra d'injecter une instance de la classe CustomerManager
quand le backing bean sera créé. Vous n'avez pas à faire de "new" (vous ne DEVEZ PAS faire de "new" !) puisque que c'est le serveur d'application qui va fournir une instance de CustomerManager
. Le cours vous donnera plus de détails sur ce mécanisme fondamental d'injection de dépendance.
Vous allez compléter la classe du bean :
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, ajoutez 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.xxxxx.jsf; // A MODIFIER suivant le paquetage de base... import xx.xxxxx.xxxxx.entity.Customer; import jakarta.inject.Inject; import jakarta.inject.Named; import jakarta.faces.view.ViewScoped; import java.io.Serializable; import java.util.List; import xx.xxxxx.xxxxx.service.CustomerManager; /** * Backing bean de la page customerList.xhtml. */ @Named(value = "customerBean") @ViewScoped public class CustomerBean implements Serializable { private List<Customer> customerList; @Inject private CustomerManager customerManager; public CustomerBean() { } /** * Retourne la liste des clients pour affichage dans une DataTable. */ public List<Customer> getCustomers() { if (customerList == null) { customerList = customerManager.getAllCustomers(); } return customerList; } }
Vous trouverez web.xml dans l'onglet des projets, en ouvrant les entrées Web Pages > WEB-INF.
Le contenu du fichier web.xml généré diffère suivant les versions de NetBeans.
Si ce fichier n'a pas été généré, vous le créez : clic droit sur le projet > New > Other... > Web > Standard Deployment Descriptor (web.xml). Ensuite, vous le modifier pour avoir le code donné ci-dessous.
Il peut indiquer que
Production
lorsque l'application sera en production.<welcome-file-list>
).Modifier ce fichier pour que
Le fichier web.xml devient
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="https://jakarta.ee/xml/ns/jakartaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd" version="6.0"> <context-param> <param-name>jakarta.faces.PROJECT_STAGE</param-name> <param-value>Development</param-value> </context-param> <!-- Faces Servlet -->
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>jakarta.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>*.xhtml</url-pattern>
</servlet-mapping> <session-config> <session-timeout> 30 </session-timeout> </session-config> <welcome-file-list> <welcome-file>customerList.xhtml</welcome-file> </welcome-file-list> </web-app>
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 Cliquez "Next" et renseignez le nom de la page (pas de .xhtml final ; il sera rajouté par NetBeans) qui est cutomerList.xhtml (attention, la copie d'écran ci-dessous a un C majuscule au lieu du c minuscule qui sera utilisé dans la suite du TP) :
Notez que cela ajoute un fichier customerList.xhtml
dans le projet, à côté de la page index.html
. Double clic pour voir le source. Modifiez la valeur de <title>
pour mettre "Liste des clients" (ce qui sera affiché dans l'onglet de la page dans le navigateur).
Remarque : La balise <html>
définit 2 espaces de noms. L'espace de noms par défaut (pas d'alias) correspond à HTML. L'alias "h" correspond à un espace de noms de JSF, utilisé pour les composants JSF qui correspondent plus ou moins aux balises HTML (par exemple <h:body>
dans le code généré).
Vous allez faire afficher les informations sur les customer dans un composant de type "datatable" dans la page customerList.xhtml. Le composant datatable est un composant standard fourni avec JSF.
Ajoutez le code d'une table dans le body de la page JSF. Cette table a 2 colonnes pour l'id et le nom des customers.
Voici le code de la table :
<h:dataTable value="#{customerBean.customers}" var="item"> <h:column> <f:facet name="header"> <h:outputText value="Id"/> </f:facet> <h:outputText value="#{item.customerId}"/> </h:column> <h:column> <f:facet name="header"> <h:outputText value="Nom"/> </f:facet> <h:outputText value="#{item.name}"/> </h:column> </h:dataTable>
Un point rouge devrait s'afficher à gauche de la ligne pour le header des colonnes car vous n'avez pas déclaré l'alias "f". Ajoutez sa définition dans la balise <html>
:
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="jakarta.faces.html" xmlns:f="jakarta.faces.core">
Normalement vous auriez pu cliquer sur le point rouge et ajoutez l'alias XML, ou tapez Ctrl-Shit-i, comme pour les importations dans une classe, mais cet alias a été modifié récemment pas Jakarta EE et il n'est pas encore pris en compte dans NetBeans.
Ignorez les éventuels avertissements pour <title>
ou </html>
.
L'attribut value
de la table désigne ce qui va être affiché dans la table. La valeur de cet attribut est une expression EL qui utilise la propriété du backing bean customers
. Ici cette propriété correspond à la méthode getCustomers()
(on veut lire une valeur, donc c'est le getter qui est utilisé) qui va retourner la liste de tous les customers. L'attribut var
donne le nom de la "variable de boucle" (comme dans une boucle "for") qui désigne une instance de la liste et donc les valeurs affichées dans une ligne de la table. Par exemple, la valeur #{item.name}
désigne le nom de l'employé de la ligne.
Une table contient des colonnes (<h:column>
) . Pour chaque ligne de la liste fournie à la table (une liste de Customer
), toutes les valeurs des colonnes seront affichées (customerId
et name
de chaque Customer
).
Chaque colonne a un en-tête (<f:facet name="header">
) qui contient le titre affiché en haut de la colonne. La valeur de la colonne est affichée avec une balise <h:ouputText>
qui contient une expression EL.
Le composant standard <h:ouputText>
permet d'afficher une valeur dans la page. A la place de <h:outputText value="Id"/>
on aurait pu écrire plus simplement Id
. Ce composant standard a des attributs qui peuvent être utiles dans certains cas.
Pour voir si vous avez compris, ajoutez une colonne pour la ville dans cette table.
Ajouter aussi la propriété "discount" de l'entité, qui correspond à une association entre les customers (clients) et les discounts (réductions à appliquer sur les montants des factures de certains clients).
Commencez par démarrer MySQL si ça n'est pas déjà fait.
Vous pouvez choisir le navigateur Web qui sera utilisé pour l'affichage des pages Web du projet : clic droit sur le projet et Properties. Clic sur Run et choisir le browser.
Commencez par un clean and build du projet.
S'il n'y a pas d'erreurs, clic droit sur le projet et Run. Le lancement est long pour la première exécution car il faut lancer le serveur d'application Payara.
IMPORTANT : Remarquez l'ouverture de l'onglet Payara dans la fenêtre "Output" du bas. Cette fenêtre est importante car elle affiche les logs de Payara et elle est très utile pour la mise au point de l'application. Si rien ne s'affiche dans votre navigateur ou s'il s'affiche un message d'erreur, il vous faut chercher les erreurs dans cet onglet.
Avant chaque nouvelle exécution il est conseillé d'effacer le contenu de l'onglet "Payara" (Clic bouton droit et Clear, ou Ctrl-L dans le contenu de l'onglet) pour que les nouvelles erreurs éventuelles soient plus faciles à repérer.
Ce qu'il se passe quand vous lancez l'exécution de l'application :
localhost:8080/tpCustomer/
.
localhost
, car l'application est démarrée sur un serveur d'application (Payara) hébergée par votre ordinateur ; 8080
est le port sur lequel le serveur d'application écoute ; tpCustomer
est le chemin du contexte de l'application qui désigne l'application à Payara (un serveur d'application peut gérer plusieurs applications). <welcome-file-list>
est ajouté pour savoir quelle page afficher (customerList.xhtml pour ce cas).localhost:8080/tpCustomer/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 :
Pour le moment cette table n'est pas très bien présentée car il n'y a aucune fioriture de mise en page et l'affichage du code de réduction n'est pas ce qu'on peut attendre d'une application professionnelle ; on va arranger ça dans la suite du TP.
Remarque : Si vous modifiez le code de la page (par exemple en ajoutant "Liste des clients" comme titre de niveau 1 avant votre table) et que vous sauvegardez la page, il suffit de recharger la page dans le navigateur pour voir la modification. C'est parce que, dans les propriétés du projet affichées par clic droit sur le projet et "Properties > Run", la case "Deploy on Save" est cochée. Parfois il faut quand même relancer l'application car les modifications sont telles que la page ne peut pas être réaffichée.
Vous n'avez pas oublié Git ?...
Les données proviennent de la base dont le nom JNDI est "jdbc/customer". Vous pouvez vérifier que ces données sont les bonnes, allez dans l'onglet "Services" de NetBeans qui comprend un gestionnaire de base de données assez simple, mais très pratique. Ouvrez Databases > jdbc:mysql://localhost:3306/customer?zeroDateTimeBehavior=CONVERT_TO_NULL&useTimezone=true&serverTimezone=UTC [root on Default schema] (Remarque : le nom de l'entrée peut être différent mais le début sera le même "jdbc:mysql://localhost:3306/customer?...). Clic droit sur l'entrée et "Connect". Ensuite, ouvrez la base de données "customer" et clic droit sur la table "customer" et choisissez "View Data..." :
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 pour discount. Cette colonne correspondent à une clé étrangère relationnelle de la table CUSTOMER vers la table DISCOUNT. Pour le vérifier, allez voir le code de l'entité Customer
ou bien directement la section "Foreign Keys" de la table relationnelle CUSTOMER. Vous allez améliorer cette présentation.
Si vous avez compris le concept de "propriétés", vous pouvez modifier la ligne qui affiche le code. remplacez :
<h:outputText value="#{item.discount}"/>
par :
<h:outputText value="#{item.discount.code} : #{item.discount.rate} %" />
Explications :
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.discount
correspond à la propriété discount
de cette entité (elle correspond à l'appel de la méthode getDiscount()
de la classe Customer
qui retourne une instance de Discount
). Donc item.discount.code
correspond à la propriété code
de la classe Discount
, donc à l'appel de la méthode getCode()
de la classe Discount
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.discount.code += ' : ' += item.discount.rate += ' %'}
] Sauvegardez vos modifications et faites un reload de la page JSF pour voir le nouvel affichage.
L'affichage sera 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.
Le problème est le même pour la propriété "zip" de Customer. Après avoir fini le TP, pour voir si vous avez bien compris, vous pourrez rajouter une colonne pour "zip" dans la table en refaisant sur cette colonne un travail semblable à ce qui a été fait pour discount.
Avant d'ajouter PrimeFaces, voyons ce qui se passe au moment du déploiement de l'application sur le serveur d'application. Le plus souvent ce serveur est sur un autre ordinateur mais, pendant le développement, un serveur local est souvent utilisé, et ça sera votre cas.
Ouvrez l'onglet "Files" dans la fenêtre en haut à gauche de NetBeans. Vous retrouvez la structure standard des applications Maven en développement.
Intéressons-nous au répertoire target. Il contient
C'est le fichier war qui sera déployé sur le serveur d'application.
Le répertoire du nom de l'application peut contenir un sous-répertoire WEB-INF/lib qui contient les librairies utilisées par l'application, qui ne sont pas déjà sur le serveur d'application. Dès que vous allez ajouter PrimeFaces au projet, il y aura le fichier jar pour PrimeFaces dans ce répertoire (et aussi dans le fichier war).
Vous déploierez vos applications directement depuis NetBeans (car vous avez intégré Payara à NetBeans).
Il est aussi possible de les déployer depuis le serveur d'application, par exemple en utilisant la console d'administration de Payara. La console d'administration de Payara sert à administrer Payara, en particulier à gérer les applications (entrée "Applications"). Si Payara est déjà démarré, on peut la faire afficher avec l'onglet Services > Servers, clic droit sur Payara et choix "View Domain Admin Console". On peut aussi, tout simplement, taper l'URL localhost:4848 dans un navigateur. Avec la console d'administration on peut, par exemple, changer l'URL associé à l'application : ouvrir l'entrée "Applications", puis clic sur l'application et changer la valeur de "Context Root".
[ Encore plus optionnel... : Quand on déploie une application depuis NetBeans sur un serveur Payara qui est situé sur le même ordinateur, Payara utilise directement les fichiers du répertoire target. Vous pouvez aller voir le répertoire payara6\glassfish\domains\domain1\applications\__internal du répertoire d'installation de Payara et vous y trouverez un répertoire associé à l'application que vous venez de déployer, mais ce répertoire est vide (puisque Payara va utiliser directement les fichiers du répertoire target). Si vous allez dans l'onglet Services, puis Server > Payara (ce nom dépend du nom que vous avez donné) > Applications, clic droit sur le nom de l'application et Undeploy, vous verrez que le répertoire vide sera supprimé. ]
On pourrait aussi déployer sur un serveur Payara distant et, dans ce cas, il faudrait commencer par faire un upload du fichier war de target sur l'ordinateur qui héberge le serveur. [ Optionnel : Au moment du déploiement, les fichiers du fichier war sont décompactés dans un répertoire du serveur, sous le répertoire payara6\glassfish\domains\domain1\applications (cette fois-ci, ce répertoire n'est pas vide car il contient les fichiers de l'application). ]
PrimeFaces propose des composants évolués/complémentaires pour JSF, le site web de référence est : http://www.primefaces.org.
Vous allez l'utiliser pour ajouter simplement de nouvelles fonctionnalités à vos pages JSF et pour améliorer la présentation des pages.
Toutes les versions de PrimeFaces ne sont pas gratuites. Par exemple à la date de l'écriture de ces lignes la dernière version gratuite est la version 13.0.0 ; la version 13.0.1 ne sera pas gratuite. Après 13.0.0, la prochaine version gratuite sera 13.1.0. La version 13.1.1 ne sera pas gratuite. Utilisez donc la dernière version gratuite dans vos projets.
Pour ajouter la librairie PrimeFaces au projet, il suffit d'ajouter une dépendance dans le fichier pom.xml. Indiquez la dernière version gratuite de PrimeFaces ; NetBeans vous aide en affichant les versions disponibles sur le dépôt central Maven si vous tapez Ctrl-espace entre <version>
et </version>
. Par exemple :
<dependency> <groupId>org.primefaces</groupId> <artifactId>primefaces</artifactId> <version>13.0.0</version> <classifier>jakarta</classifier> </dependency>
Remarque : <classifier>
est indispensable pour avoir une version de PrimeFaces adaptée aux dernières versions de Jakarta.
Cette librairie sera téléchargée par Maven depuis l'entrepôt central. Vous devez être connecté à Internet et la vitesse de transfert dépend de l'état de votre réseau.
Faites un build du projet pour voir si tout va bien (clic droit sur le projet > Clean and Build).
[ Remarque pour la suite : Dans les builds suivants, si vous avez une erreur disant que le fichier jar de PrimeFaces ne peut être supprimé, c'est que vous avez sans doute déjà déployé le projet sur le serveur et Windows refuse de supprimer un fichier utilisé par une application. En ce cas, il suffit de faire un undeploy du projet :
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 en vous donnant le choix entre 2 espaces de noms ; choisissez l'espace de noms de PrimeFaces.
A partir de là on pourra utiliser des tags PrimeFaces avec le préfixe p:
<h:dataTable>
et </h:dataTable>
par <p:dataTable>
et </p:dataTable>
<h:column>
et </h:column>
, remplacez les h:column par p:column. É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, lancez un "Clean and Build" avant.
Si vous avez un message vous disant que le jar de PrimeFaces a un format zip invalide, faites un "Clean and build" du projet principal avant de le relancer et tout devrait alors fonctionner.
Remarque : si vous êtes gêné par des "warnings" du type "WARNING: JSF1091 : Aucun type mime détecté pour le fichier primeicons/primeicons.eot. ..." dans la fenêtre des logs du serveur d'application, ajoutez ces lignes à la fin de votre fichier web.xml (juste avant </web-app>). Il faut relancer l'application par Run pour que les messages disparaissent.
Le format de la table peut changer suivant le goût des développeurs de PrimeFaces. Si vous connaissez CSS vous pouvez aussi modifier la présentation. Vous devriez obtenir ce type d'affichage :
C'est déjà mieux présenté non ? Ce qui est surtout intéressant, c'est que le composant <p:dataTable>
de PrimeFaces possède de nombreuses options pour la pagination, rendre les colonnes triables, éditables, etc.
Vous allez utiliser quelques unes des possibilités des tables PrimeFaces. Pour vous aider vous pouvez consulter la documentation de PrimeFaces. Cliquez sur le "user guide" de la version que vous utilisez, puis sur "Get Started" et descendez dans la page jusqu'à la section "Components" dans le menu de gauche et cliquez sur "DataTable". Vous pouvez aussi aller voir les démos. Le code est fourni ; cliquez sur DataTable dans la section Data dans le menu de gauche. Après chaque modification, rechargez la page pour tester.
Il vous faut commencer par insérer la table dans un formulaire JSF pour profiter de toutes ces fonctionnalités :
<h:form> <p:dataTable value="#{customerBean.customers}"> ... </p:dataTable> </h:form>
Remettez en forme le code de la page en tapant Alt-Shift-F.
Voici quelques modifications à faire (remarquez la complétion de NetBeans dans les expressions EL, avec Ctrl+[espace]) :
<p:dataTable ...>
les attributs paginator="true"
et rows="10"
, sauvez, rechargez la page, ça y est, les résultats sont paginés. Remarquez que PrimeFaces est connu de NetBeans et vous pouvez utiliser la complétion de code ; par exemple, tapez pag et Ctrl-espace et paginator vous est proposé.sortBy="#{item.name}"
dans le tag p:column
de la colonne des noms. Attention, pour que le tri fonctionne, le backing bean doit être de portée "vue" et la méthode qui récupère les valeurs à afficher dans la page doit utiliser un champ qui contient la liste des objets affichés par la table, ce qui est bien le cas avec le champ customerList
. Testez.filterBy
) sur la colonne pour l'état et sur la colonne pour le nom, ce qui permettra à l'utilisateur de sélectionner seulement les clients d'un état ou d'un certain nom (par défaut, la sélection est effectuée sur le début de la valeur). Pour avoir des exemples d'utilisation de filterBy
, allez dans la démo de cette page.Compléments : voici le code du début d'une table avec quelques options supplémentaires pour la pagination dans les attributs de la balise <p:dataTable>
.
Git s'il n'y a pas d'erreurs...
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 :
widgetVar
de <p:dataTable>
; sa valeur "customerTable" doit correspondre paramètre de "PF" dans l'attribut onkeyup
du filtre). 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, dans l'un des champs 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="#{customerBean.customers}" var="item" widgetVar="customerTable" emptyMessage="Aucun client avec ce critère" paginator="true" rows="10" rowsPerPageTemplate="2,4,8,10" paginatorTemplate="{CurrentPageReport} {FirstPageLink} {PreviousPageLink} {PageLinks} {NextPageLink} {LastPageLink} {RowsPerPageDropdown}"> <f:facet name="header"> <p:outputPanel> <h:outputText value="Recherche dans tous les champs de recherche"/> <p:inputText id="globalFilter" onkeyup="PF('customerTable').filter()" style="width:150px"/> </p:outputPanel> </f:facet> <p:column> <f:facet name="header"> <h:outputText value="CustomerId"/> </f:facet> <h:link outcome="customerDetails?idCustomer=#{item.customerId}" value="#{item.customerId}"/> </p:column> ...
La ligne modifiée (<h:link>
à la place de <h:outputText>
) ajoute un lien hypertexte dans la colonne ; le texte du lien sera l'id du client (attribut "value=...
") et lorsque l'utilisateur cliquera sur la colonne, une requête GET (<h:link>
envoie une requête GET) sera envoyée au serveur pour afficher la page customerDetails.xhtml
, avec l'id du client en paramètre. Par exemple "customerDetails?idCustomer=2"
si l'id du client est 2. JSF ajoutera automatiquement le suffixe ".xhtml
" au fichier et ajoutera au début l'adresse de l'application "http://localhost:8080/..."). On verra dans le cours et les TPs suivants d'autres moyens d'ajouter des paramètres à une requête GET.
Si vous sauvegardez la page JSF et lancez l'exécution de l'application, vous verrez que la colonne id affiche des erreurs car la page customerDetails.xhtml
n'existe pas encore. Remarque : ces messages d'erreur apparaissent parce que vous êtes en mode "développement de projet" (voir fichier web.xml). Attention, si vous étiez en mode "production", ils n'apparaitraient pas parce que vous n'avez pas encore ajouté explicitement dans la page une balise pour indiquer où afficher les messages d'erreur. Testez en mode "Production" et revenez ensuite en mode "Development" en modifiant le fichier web.xml.
Vous pouvez créer une page customerDetails.xhtml
vide ou bien attendre la suite du TP qui va montrer comment la créer, mais avant, on va commencer par ajouter son backing bean (pour profiter d'une facilité de NetBeans pour générer du code dans la page JSF).
La classe CustomerDetailsBean
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 le bean CDI 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 customerDetailsBean
comme vous avez fait pour le backing bean de la page customerList.xhtml
(pour aller plus vite vous pouvez faire un clic droit directement sur le package dans lequel sera le backing bean).
Voici le code :
package xx.xxxxx.xxxxx.jsf; // A MODIFIER... import xx.xxxxx.xxxxx.entity.Customer; import xx.xxxxx.xxxxx.service.CustomerManager; import java.io.Serializable; import jakarta.inject.Inject; import jakarta.faces.view.ViewScoped; import jakarta.inject.Named; /** * Backing bean pour la page customerDetails.xhtml. */ @Named @ViewScoped public class CustomerDetailsBean implements Serializable { private int idCustomer; private Customer customer; @Inject private CustomerManager customerManager; public int getIdCustomer() { return idCustomer; } public void setIdCustomer(int idCustomer) { this.idCustomer = idCustomer; } /** * Retourne les détails du client courant (contenu dans l'attribut customer de * cette classe). */ public Customer getCustomer() { return customer; } /** * Action handler - met à jour dans la base de données les données du client * contenu dans la variable d'instance customer. * @return la prochaine page à afficher, celle qui affiche la liste des clients. */ public String update() { // Modifie la base de données. // Il faut affecter à customer (sera expliqué dans le cours). customer = customerManager.update(customer); return "customerList"; } public void loadCustomer() { this.customer = customerManager.findById(idCustomer); } }
Ce backing bean a 2 propriétés :
idCustomer
(avec getter et setter) qui va conserver un id de customer ;Customer
(avec getter) qui va conserver les données sur le customer qui a l'id idCustomer
(ces informations sont récupérées en exécutant la méthode loadCustomer
.La méthode loadCustomer
appelle une méthode qu'il va falloir rajouter dans le bean CDI CustomerManager
. Cette méthode recherche un Customer
par son id. Dans CustomerManager
(vous pouvez y accéder par l'onglet Projects ou bien plus simplement par Ctrl-clic sur le type CustomerManager
de la variable d'instance customerManager
de la classe CustomerDetailsBean
) ajouter cette méthode :
public Customer findById(int idCustomer) { return em.find(Customer.class, idCustomer); }
Sauvegardez bien le fichier CustomerDetailsBean.java
avant de passer à la suite.
Sur le projet faites clic droit > New > Jsf Page et créez une page customerDetails.xhtml
dans la racine des pages Web du projet (ne tapez pas le .xhtml final ; il sera rajouté par NetBeans).
Vous allez ajouter un formulaire pour afficher, et même modifier, toutes les informations sur le client choisi par l'utilisateur dans la page listCustomers.xhtml.
A l'intérieur du body de la nouvelle page, tapez Alt-Insert et choisissez "JSF Form From Entity". Une autre façon de faire : faites un drag and drop de "JSF Form From Entity" (dans la palette) dans le body de la page. Pour faire afficher la palette : menu Window > IDE Tools > Palette.
Indiquez ensuite le nom de la classe entité dont vous voulez afficher les informations dans un formulaire, ainsi que le nom de la propriété du backing bean dont le getter retourne les détails d'un client. Vous pouvez choisir d'utiliser PrimeFaces ou d'utiliser la librairie standard pour "Template Style" ; pour faire simple je vous conseille de laisser la valeur "Standard JavaServer Faces". Vous auriez pu cocher "Generate read only view" si cette page n'avait servi qu'à afficher les informations sur le client mais on va aussi s'en servir pour modifier ses propriétés.
Remarque : il se peut que les entités ne s'affichent pas tout de suite. En ce cas, attendez un peu et recommencez l'opération. Peut-être qu'un clean and build du projet aide...
Examinez le code de la page customerDetails.xhtml
. On voit qu'elle affiche les propriétés du client retourné par la méthode getCustomer()
du backing bean CustomerDetailsBean
. On a, par exemple, value="#{customerDetailsBean.customer.name}"
.
La table est insérée dans un formulaire (<h:form>
). En effet cette page permettra non seulement d'afficher toutes les informations sur un client, mais aussi de les modifier. Le formulaire est lui-même inséré dans la balise <f:view>
mais elle n'est pas indispensable. Remettez en forme le code de la page, avec les bonnes indentations.
Pour le moment, faites une petite modification dans les lignes générées par NetBeans pour éviter que l'utilisateur ne modifie l'id d'un client : ajoutez readonly="true
" dans la balise <h:inputText>
de l'id.
Enlevez aussi tout ce qui concerne le microMarket (c'est-à-dire ce qui concerne la propriété "zip") : le label et la liste déroulante (<h:selectOneMenu>
) pour la saisie.
Comme la méthode getCustomer()
retourne le client qui est dans la propriété "customer
" du backing bean, il reste à mettre dans cette propriété le Customer
qui a l'id de la ligne de la table des Customer sur laquelle l'utilisateur aura cliqué. Ca va être le but de la section metadata que l'on va écrire.
On a vu que si l'utilisateur clique sur la ligne de la table des clients qui correspond au client d'id 2, une requête GET sera envoyée à l'URL ".../customerDetails?idCustomer=2".
Pour que la page customerDetails.xml
affiche bien les détails sur le client dont l'id est passé en paramètre de la requête HTTP GET, il reste 2 étapes :
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="#{customerDetailsBean.idCustomer}" required="true"/> <f:viewAction action="#{customerDetailsBean.loadCustomer}"/> </f:metadata>
La section <f:metadata>
peut servir à plusieurs choses. Ici elle sert à indiquer ce qui sera exécuté juste avant que la page ne soit affichée.
<f:viewParam>
dit à JSF de récupérer la valeur du paramètre de nom "idCustomer
" de la requête HTTP GET (la valeur sera 2 pour l'exemple ci-dessus) et de la mettre dans la propriété idCustomer
du backing bean customerDetailsBean
(avec le setter setIdCustomer
).
<f:viewAction>
dit à JSF d'exécuter la méthode loadCustomer
du backing bean customerDetailsBean
, après avoir rangé l'id dans la propriété idCustomer
du backing bean, et avant l'affichage de la page.
En résumé, tout se passe donc comme on veut ; par exemple, si l'URL contient customerDetails.xhtml?idCustomer=2
, la valeur 2 est mise dans la propriété idCustomer
du backing bean et ensuite, la méthode loadCustomer
charge le client d'id 2 depuis la base de données et le met dans la variable d'instance customer
du backing bean. La page des détails affichera les informations sur ce customer ; on peut le voir en lisant le code généré par NetBeans pour la page des détails ; par exemple <h:inputText id="name" value="#{customerDetailsBean.customer.name}" title="Name" />
avec la propriété customer
du backing bean qui correspond à la variable d'instance customer
.
Vous pouvez tester tout de suite l'affichage de la page des détails à partir de la liste de tous les clients.
Voici ce qui devrait s'afficher pour la page des détails du client d'id 1. Les détails du client numéro 1 s'affichent bien, cependant le code de discount (remise faite au client) n'est pas correctement affiché.
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 DISCOUNT contient juste 4 valeurs correspondants aux quatre types de réductions possibles. On va remplir la liste déroulante avec ces valeurs. La liste permettra aussi à l'utilisateur de modifier la réduction accordée au client en choisissant une de ces 4 valeurs. Pour le moment le code affecte la valeur #{fixme}
à <f:selectItems>
, balise interne de la liste déroulante <h:selectOneMenu>
, qui indique les valeurs à afficher. Vous allez donc commencer par remplacer cette expression EL #{fixme}
par la référence à une méthode du backing bean qui retourne les 4 valeurs possibles pour une réduction.
Ce bean DiscountManager
va gérer les Discount
, comme le bean CustomerManager
gére les Customer
.
La récupération des Discount
se fait dans la base de données. Selon le partage des tâches habituel de JSF, un backing bean ne doit pas accéder directement aux bases de données. Il doit faire appel à un bean CDI qui utilise un EntityManager
injecté pour interagir avec la base de données. Vous allez donc commencer par ajouter un bean CDI DiscountManager
dans le sous-package service
, pour gérer les données associées aux Discount
dans la base de données. Inspirez-vous du bean CDI que vous avez déjà créé.
Dans ce bean, injectez un EntityManager
et ajoutez une méthode publique List<Discount> getAllDiscounts()
qui retourne toutes les réductions. Inspirez-vous de la méthode qui retourne tous les clients dans le bean CDI CustomerManager
. Essayez de refaire les étapes que vous aviez faites lors de l'écriture du gestionnaire de clients (ajoutez l'Entity Manager, puis les méthodes). Pas besoin de méthode persist
puisque vous n'allez pas créer de nouveau Discount
.
Ajoutez-y une méthode qui retourne le Discount
qui a un certain code (le code est H, M, L ou N). Cette méthode servira dans la section suivante pour convertir une String
en Discount
:
public Discount findById(String code) { return em.find(Discount.class, code); }
Donc, maintenant on a un bean CDI qui renvoie toutes les valeurs possibles de Discount
et qui permet d'avoir un Discount
si on connait son code ("H" par exemple).
Dans le backing bean CustomerDetailsBean
, écrivez la méthode qui servira à remplir le menu déroulant de la page JSF customerDetails.xhtml
. Insérez dans le bean la méthode suivante :
/** * Retourne la liste de tous les Discount. */ public List<Discount> getDiscounts() { return discountManager.getAllDiscounts(); }
D'où vient ce discountManager
? A vous de jouer... Vous avez déjà vu ce type de code quand vous avez écrit le backing bean CustomerDetailsBean
.
Remplacez #{fixme}
dans la page customerDetails.xml
par la référence à cette méthode (en fait par une référence à la propriété dont cette méthode est le getter).
Vous pouvez maintenant faire exécuter l'application (par Run) et vous verrez que les Discount
s'affichent dans la liste déroulante. Cependant, comme pour l'affichage de la liste de tous les clients, l'affichage ne convient pas vraiment.
Il est facile d'améliorer cet affichage en utilisant les attributs suivants de la balise <f:selectItems>
(Rappel : NetBeans peut vous aider pour compléter les noms d'attributs tels que itemLabel
; vous tapez "item" suivi de Ctrl-[Barre espace].)
var
: nom d'une variable qui désigne un des éléments de la liste, c'est-à-dire un Discount
; appelez-la, par exemple, "discount" ;itemLabel
: ce qui sera affiché dans la liste déroulante pour un élément. Inspirez-vous de ce que vous avez déjà fait pour la page qui liste tous les clients. Un simple rechargement de la page devrait suffire pour voir l'amélioration.
Si tout est correct, Git...
Exercice optionnel : Faites afficher les réductions par ordre croissant (et ensuite par ordre décroissant) des taux de réduction. Vous pouvez réviser le tri sur les collections Java et les expressions lambda (mais c'est une moins bonne solution) ou bien (meilleure solution) ajoutez un "NamedQuery" avec un "order by" dans l'entité Discount.
Utilisation d'un convertisseur pour envoyer au serveur le code de réduction choisi par l'utilisateur
La page JSF customerDetails.xhtml va être utilisée par l'utilisateur pour modifier des informations sur un customer, y compris la réduction (discount) à laquelle il a droit.
Il y a plusieurs solutions pour envoyer au serveur le choix de l'utilisateur pour la réduction. On pourrait envoyer le code de la réduction (par exemple "M") et le serveur pourrait récupérer ce code et charger l'instance de Discount
qui a ce code, comme on a récupéré un Customer
à partir de son id.
Je vais vous montrer une autre façon de faire qui utilise un convertisseur qui va s'occuper de la transformation d'un code de réduction en instance d'entité Discount
.
Lorsque l'utilisateur modifie les informations sur un client et soumet le formulaire, son choix pour la valeur de la réduction va être envoyé au serveur comme une entité entière, une instance de Discount
. Quand le serveur reçoit la requête HTTP envoyée par le client HTTP à la soumission du formulaire, il récupère donc une instance de Discount
. Évidemment, le protocole HTTP ne permet pas de transmettre une entité Java du client vers le serveur ; ce sont des String
qui sont transmises. En fait, JSF va intervenir "sous le manteau" en utilisant un convertisseur qui sait transformer une String
en instance de Discount
, et vice-versa. Ce qui va transiter sur le réseau est en fait l'id (le code) de l'entité Discount
(de type String
).
Traduction en code dans la page JSF :
value
de <h:selectOneMenu>
(qui indique où sera rangée la réduction choisie par l'utilisateur pour le Customer
affiché) va désigner une instance de Discount
: #{customerDetailsBean.customer.discount}
itemValue
) enverra l'instance de Discount
(#{discount}
) au serveur et cette instance sera donc rangée dans #{customerDetailsBean.customer.discount}
.On a ainsi ce code :
<h:selectOneMenu id="discount" value="#{customerDetailsBean.customer.discount}" title="Discount" required="true" requiredMessage="The Discount field is required."> <f:selectItems value="#{customerDetailsBean.discounts}" var="discount" itemLabel="#{discount.code} (#{discount.rate} %)" itemValue="#{discount}"/> </h:selectOneMenu>
Vous devriez obtenir ceci :
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 discounts :
<h:button id="back" value="Revenir à la liste" outcome="customerList"/> <h:commandButton id="update" value="Enregistrer" action="#{customerDetailsBean.update}"/>
Voici ce que cela signifie :
<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>
génère une requête GET<h:commandButton>
génère une requête POST et soumet les valeurs saisies dans le formulaire qui le contient.Testez :
<h:messages/>
Quel est le problème ? Pour le comprendre, faites afficher le code de la page HTML qui contient le formulaire (Ctrl-U sur Chrome), et examinez le code qui concerne la liste déroulante. En HTML, une liste déroulante est représentée par la balise <select>
. Les valeurs envoyées sont contenues dans l'attribut des balises <option>
.
Le problème est que la String
"xx.xxxxx.xxxxxx.entity.Discount[ code=N ]" (pour cet exemple) a été envoyée au serveur pour le Discount
. Quand cette String
arrive sur le serveur, elle ne peut être affectée à une variable de type Discount
; en effet l'attribut value
de <f:selectItems>
indique que la valeur de type String
va être affectée à #{customerDetailsBean.customer.discount}
qui est de type Discount
, comme on le voit dans la classe entité Customer
.
Vous allez résoudre le problème en écrivant un convertisseur JSF pour les Discount
, qui va convertir les String
en Discount
(et vice-versa).
Ce convertisseur va intervenir 2 fois :
Discount
en String
).String
en Discount
).Il existe plusieurs façons de créer et d'utiliser un convertisseur JSF. Pour ce TP vous allez écrire une classe à part qui va implémenter l'interface jakarta.faces.convert.Converter<T>
(attention, n'importez pas la classe Converter du paquetage jakarta.persistence). Cette classe sera annotée par @FacesConverter
, avec un paramètre qui va identifier le convertisseur, par exemple @FacesConverter(value=converterDiscount)
. L'identificateur du convertisseur sera utilisé quand il sera utilisé par un composant JSF (avec la liste déroulante ce cas).
Comment convertir une String
en Discount
et vice-versa ? Le plus souvent pour une entité, l'id de l'entité (l'attribut code
de l'entité Discount
) est utilisé. Par exemple, la String
"2" va être convertie en l'objet Discount
qui a l'id 2 (il faudra aller chercher les données de l'entité dans la base de données) ; la conversion d'un objet Discount
en une String
est plus simple car il suffit de retourner la valeur de l'id de l'entité (la valeur de code
).
Puisqu'un accès à la base de données va être nécessaire, le bean CDI DiscountManager
écrit dans la section précédente sera utilisé (il faudra l'injecter dans le convertisseur). Pour des raisons techniques, un convertisseur ne peut injecter un bean CDI que si l'annotation @FacesConverter
a un attribut managed=true
(voir code complet de la classe ci-dessous).
Voici le code de la classe qui fait la conversion (mettez-la dans le même paquetage que les backing beans) :
@FacesConverter(value = "discountConverter", managed=true) public class DiscountConverter implements Converter<Discount> { @Inject private DiscountManager discountManager; /** * Convertit une String en Discount. * * @param value valeur à convertir */ @Override public Discount getAsObject(FacesContext context, UIComponent component, String value) { if (value == null) return null; return discountManager.findById(value); } /** * Convertit un Discount en String. * * @param value valeur à convertir */ @Override public String getAsString(FacesContext context, UIComponent component, Discount discount) { if (discount == null) return ""; return discount.getCode(); } }
Rappel : attention à bien importer le bon paquetage pour Converter (jakarta.faces.convert).
On va pouvoir ajouter l'attribut converter
à la balise <h:selectOneMenu>
de la page customerDetails.xhtml
afin d'utiliser le convertisseur que nous venons d'écrire : il faut lui donner pour valeur l'id du convertisseur qu'on vient d'écrire (discountConverter)
.
Voici le code qu'il faut modifier dans customerDetails.xhtml
:
<h:selectOneMenu id="discount" value="#{customerDetailsBean.details.discount}" title="Discount" required="true" requiredMessage="The Discount field is required." converter="discountConverter"> <f:selectItems value="#{customerDetailsBean.discounts}" var="discount" itemLabel="#{discount.code} : #{discount.rate} %" itemValue="#{discount}" /> </h:selectOneMenu>
Sauvegardez, testez, ça doit fonctionner. Vérifiez que les modifications ont été prises en compte dans la base de données.
Git...
En résumé :
value
de <h:selectOneMenu>
. value
de <f:selectItems>
. itemLabel
; la valeur qui est envoyée au serveur est déterminée par l'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>
". (remarquez que la valeur de value
a changé car le convertisseur est intervenu). La valeur "M" a été obtenue en utilisant la méthode getAsString
du convertisseur avec comme paramètre l'instance de Discount
.String
envoyée au serveur ("M" si l'option écrite ci-dessus a été choisie) est traduite en Discount
en utilisant la méthode getAsObject
du convertisseur. Cette valeur est donnée à la variable d'instance discount
de Customer
et donc la nouvelle réduction sera enregistrée dans la base de données au moment du commit qui suivra la méthode update
de le bean CDI (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 HTTP et le serveur d'application sont hébergés par 2 ordinateurs différents reliés par Internet.
A la soumission du formulaire les valeurs qui sont dans les champs de saisie (composants <h:inputText>
et <h:selectOneMenu>
) sont transmises au serveur sous la forme de texte par une requête POST. Ces valeurs sont transmises par des paramètres inclus dans le corps de la requête POST.
Quand elles arrivent sur le serveur, JSF les récupère, les convertit dans le bon type Java. JSF vérifie que les valeurs sont valables ; par exemple, certaines valeurs sont requises et la valeur null
ne sera pas valable. Ça n'est pas le cas ici, mais il pourrait aussi y avoir des contraintes plus complexes, par exemple sur le format d'un champ "email" ou un âge supérieur à 18 ans.
Si au moins une des valeurs n'est pas valable, des messages d'erreur pour toutes les valeurs non valables sont insérés dans la page qui contient le formulaire et cette page est retournée au client HTTP en réponse à la requête POST. Quand elle arrive sur le client HTTP, la page avec les messages d'erreur s'affiche dans le navigateur. L'utilisateur prendra ainsi connaissance des erreurs et pourra corriger ce qu'il a saisi pour soumettre à nouveau le formulaire.
Si toutes les valeurs sont valables, elles sont rangées par JSF à l'endroit indiqué par les expressions EL du backing bean. Par exemple,
<h:inputText id="name" value="#{customerDetailsBean.customer.name}" title="Name" />
indique que la valeur saisie par l'utilisateur sera rangée dans la propriété "name
" du customer en cours (celui qui est retourné par la méthode getCustomer()
et dont les propriétés ont été affichées lorsque la page a été affichée).
Sur le serveur la propriété customer
du backing bean contient donc les valeurs modifiées par l'utilisateur. La méthode update()
est alors exécutée (à cause de action="#{customerDetailsBean.update}"
du bouton qui a soumis le formulaire).
Regardons la méthode update()
:
public String update() { customer = customerManager.update(customer); return "customerList"; }
Cette méthode prend la propriété "customer
", de type Customer
et appelle la méthode update()
de le bean CDI customerManager
, qui va mettre à jour le client dans la base de données (la méthode merge
sera étudiée dans le cours sur JPA) avec les valeurs contenues dans customer
. La méthode retourne la String
"customerList" et c'est donc la page customerList.xhtml
qui sera retournée en réponse à la requête POST (règle de JSF), et affichée dans le navigateur du client HHTP.
Pour la réduction (discount) : Il existe une relation entre les clients et les codes de remise. Dans la classe entité Customer
, on voit que le code de remise est un attribut discount
de type Discount
. Lorsque l'utilisateur modifie le discount avec le menu déroulant dans la page de détails, une String
est envoyée pour ce discount. C'est le travail du convertisseur de transformer sur le serveur cette String
en objet de type Discount
.
Que se passe-t-il si l'utilisateur modifie la valeur du paramètre de l'URL et donne un id qui ne correspond à aucun client ? Et si l'utilisateur tape alors sur le bouton "Enregistrer" ? Vérifiez qu'un formulaire vide est affiché. Un produit professionnel ne doit pas avoir ce comportement. Pour l'éviter vous pourriez ajouter un attribut au formulaire pour qu'il ne soit affiché que si un client a été trouvé :
<h:form rendered='#{customerDetailsBean.customer != null}'>
Il faut aussi sortir le bouton "Revenir à la liste" du formulaire pour que l'utilisateur puisse revenir à la liste dans ce cas où le formulaire n'est pas rendu.
Encore mieux : ajouter un message disant "Aucun client n'a été trouvé avec cet id" dans le cas où le formulaire n'est pas affiché :
<h:outputText value="Aucun client avec l'id #{customerDetailsBean.idCustomer} !"
rendered='#{customerDetailsBean.customer == null}'/>
On pourrait aussi améliorer la présentation, par exemple pour la dataTable. Les TPs suivants montreront comment faire.
Il faudrait aussi évidemment ajouter des composants pour afficher les messages d'erreurs éventuelles et pour afficher les messages de succès (par exemple si les modifications ont bien été enregistrées). Vous verrez comment faire dans le cours sur JSF.
Git...
Dans le prochain TP et en cours nous verrons comment corriger ces problèmes avec le modèle PRG.