Looking for Computer Science & Information Technology online
courses ?
Check my new web site: https://www.yesik.it !
Dans ce tutoriel nous allons voir comment utiliser JBoss Seam dans le contexte du développement d'une (petite) application d'entreprise. On suppose dans ce tutoriel une première expérience des technologies Facelets et Hibernate.
Objectifs | A la fin de ce tutoriel, vous saurez:
|
---|---|
Prérequis | |
Moyens |
|
Sommaire
Seam en deux mots
JBoss Seam est un ensemble de bibliothèques destinées à faciliter le développement d'application d'entreprises autour de Java Enterprise Edition (Java EE, ou JEE). Un des objectifs principaux de Seam est d'assurer une intégration élégante et sans accroc (seamless) des technologies JEE majeures:
- JSF (destiné à la couche présentation),
- EJB3 (destiné à la couche business logic) et
- JPA (Hibernate, par ex, destiné à assurer la persistance des données).
Seam peut être téléchargé sur le site de JBoss http://labs.jboss.com
Le projet
Le client
Notre client va être la société KerBeurre. Il s'agit d'une biscuiterie industrielle qui fournit ses produits en gros pour que les distributeurs les vendent sous leur propre marque.
Analyse de l'existant
Pour chaque sorte de biscuit fabriqué, KerBeurre utilise toujours la même recette. Par contre, d'un client à l'autre, la cuisson peut être différente. En effet, certains clients préfèrent les sablés "bien dorés", alors que d'autres les préfèrent "encore blonds".
Jusqu'à présent, le suivi des temps de cuisson en fonction des clients se faisait sur papier: Une fiche cuisson était associée au bon de livraison de chaque lot de biscuits. Ainsi, quand un client commande un nouveau lot de biscuit, il est possible de régler le four de la même manière que pour le lot précédemment livré et donc d'obtenir des biscuits ayant un aspect identique.
Problématique
Le problème des fiches papier est qu'elles se perdent, qu'elles ne sont pas toujours bien remplies, et qu'il est difficile de les exploiter, par exemple pour effectuer des statistiques. C'est pourquoi KerBeurre a décidé d'informatiser le suivi de ces données. Dans un premier temps, on souhaite pouvoir saisir les paramètres cuisson pour un lot. C'est ce que nous allons traiter dans ce tutoriel.
Domain model
De la description ci-dessus, il découle en première analyse un domain model composé de trois classes d'objets:
- client
- lot
- cuisson
Dans ce modèle, un lot peut avoir 0 ou 1 paramètres de cuisson associé. Les réglages de cuisson sont relevés en fin de production. Par conséquent, les lots en cours de production ne sont encore associés à aucun paramètre de cuisson.
Scénario
Le scénario d'utilisation que nous allons donc mettre en œuvre dans ce tutoriel est le suivant:
Scénario 1: Fin de production |
Qui: Gégé, l'opérateur |
Quand: A la fin d'une production |
|
Néanmoins, afin de limiter la complexité, nous allons dans un premier temps nous contenter du sous-scénario suivant. Dans l'esprit d'un développement itératif, le scénario 1 complet sera développé par raffinement du scénario 1.1.
Scénario 1.1: Liste des lots en production |
Qui: Gégé, l'opérateur |
Remarque: dans ce sous-scénario, la liste n'est pas cliquable. |
Interface utilisateur
Immédiatement, du scénario 1 découle 3 pages (ou 3 écrans):
- liste des lots en production
- fiche de saisie des paramètres de cuisson
- feu vert de fin de production
Seule la liste des lots en production est nécessaire à la réalisation du scénario 1.1.
Travail préliminaire
Technologies retenues
Avec toutes les informations recueillies précédemment, il est possible de commencer la réalisation du logiciel. Les développeurs ont opté pour une application Web multi-tiers utilisant la pile Java EE et JBoss Seam. Comme souvent dans ce genre d'architecture:
- La couche présentation sera mise en oeuvre avec Facelets,
- la couche métier utilisera EJB3,
- et la persistance sera assurée par Hibernate.
Configurer le projet
Avant de pouvoir commencer à coder, il est nécessaire de préparer notre environnement de travail. C'est vraisemblablement la partie la plus fastidieuse, et peut-être même celle qui prendra la plus de temps lors de la mise en oeuvre de ce premier scénario. Néanmoins, une fois ceci fait, nous n'aurons plus à y revenir pour développer les autres scénarios de l'application. Pour parler clairement, la configuration se fait une fois pour toute au début du projet!
Notre application va être déployée dans une archive EAR, ce qui explique le nombre de fichiers de configuration nécessaire. Si vous avez suivi les tutoriels sur JFS, ou si vous êtes familiers des Servlet vous connaissez les fichiers .war (Web ARchive). Ce sont des archives qui renferment tous les fichiers nécessaires pour une application Web Java EE. L'équivalent dans le cadre d'une application d'entreprise est le .ear (Enterprise ARchive).

War ou Ear?
Ce sont les spécifications Java EE qui imposent d'utiliser un format d'archive plutôt que l'autre - c'est à dire de se diriger plutôt vers une application web ou vers une application d'entreprise. En simplifiant, on peut dire que:
- si votre application est destinée à être déployée sur un container de servlet il vous faut une archive WAR;
- si votre application contient surtout des pages d'interface et peu de logique, ou uniquement des servlets, une archive WAR suffit;
- si votre application utilise un domain model complexe, il est préférable d'utiliser une archive EAR;
- si votre application utilise EJB3, il vous faut une archive EAR.
Où les choses deviennent subtiles, c'est qu'une archive EAR contient souvent une ou plusieurs archives WAR. Par contre, l'inverse est impossible.
Hormis cette rapide introduction, il n'y a pas grand chose de plus à savoir sur les archives EAR: en effet, c'est notre buildfile Ant qui se chargera de fabriquer l'archive. Par contre, il va être nécessaire de passer un peu plus de temps sur l'arborescence des répertoires de notre projet de développement. La structure recommandée est la suivante:
KerBeurreProject/build KerBeurreProject/build/... (fichiers compilés, jars, wars, ears) KerBeurreProject/src KerBeurreProject/src/... (Fichiers sources Java) KerBeurreProject/view KerBeurreProject/view/... (Fichiers xhtml, CSS, images) KerBeurreProject/resources KerBeurreProject/resources/WEB-INF KerBeurreProject/resources/WEB-INF/web.xml KerBeurreProject/resources/WEB-INF/components.xml KerBeurreProject/resources/WEB-INF/faces-config.xml KerBeurreProject/resources/WEB-INF/navigation.xml KerBeurreProject/resources/WEB-INF/pages.xml KerBeurreProject/resources/META-INF KerBeurreProject/resources/META-INF/persistence.xml KerBeurreProject/resources/META-INF/application.xml KerBeurreProject/resources/META-INF/jboss-app.xml KerBeurreProject/resources/META-INF/ejb-jar.xml KerBeurreProject/resources/seam.properties KerBeurreProject/lib KerBeurreProject/lib/... (Bibliothèques JAR utilisées) KerBeurreProject/build.xml
Vous pouvez créer cette arborescence à l'aide des commandes mkdir et touch. Pour l'instant, vous pouvez laisser les fichiers vides.
La première étape pour configurer notre projet va être de saisir le fichier build.xml:
<?xml version="1.0" ?> <project name="KerBeurreProject" default="compile" basedir="."> <!-- JBoss Seam location. Configure seam.home to the path where seam was extracted --> <property name="seam.home" location="/home/sylvain/jboss-seam-2.0.0.GA" /> <property name="seam.lib" location="${seam.home}/lib" /> <property file="${seam.home}/build.properties" /> <!-- Project configuration. --> <property name="project.name" value="KerBeurreProject" /> <property name="project.lib" location="./lib" /> <property name="project.src" location="./src" /> <property name="project.resources" location="./resources" /> <property name="project.view" location="./view" /> <property name="project.build" location="./build" /> <!-- Build path --> <property name="build.classes" location="${project.build}/classes" /> <property name="build.jars" location="${project.build}/jars" /> <path id="build.classpath"> <fileset dir="${seam.lib}" includes="*.jar" /> <fileset dir="${project.lib}" includes="*.jar" /> </path> <target name="clean"> <delete dir="${project.build}" /> </target> <target name="compile"> <mkdir dir="${build.classes}"/> <javac destdir="${build.classes}" srcdir="${project.src}" classpathref="build.classpath" debug="true" /> </target> <target name="war"> <mkdir dir="${build.jars}"/> <war destfile="${build.jars}/app.war" webxml="${project.resources}/WEB-INF/web.xml" > <webinf dir="${project.resources}/WEB-INF"> <include name="faces-config.xml"/> <include name="components.xml"/> <include name="navigation.xml"/> <!-- <include name="pages.xml"/> --> </webinf> <lib dir="${seam.lib}"> <include name="jboss-seam-ui.jar"/> <include name="jboss-seam-debug.jar" /> <include name="jsf-facelets.jar"/> </lib> <fileset dir="${project.view}" /> </war> </target> <target name="jar" depends="compile"> <mkdir dir="${build.jars}"/> <jar destfile="${build.jars}/app.jar" > <fileset dir="${build.classes}"> <include name="**/*.class" /> </fileset> <fileset dir="${project.resources}"> <include name="seam.properties" /> </fileset> <fileset dir="${project.lib}"> <include name="*.jar" /> </fileset> <metainf dir="${project.resources}/META-INF"> <include name="persistence.xml" /> <include name="ejb-jar.xml" /> </metainf> </jar> </target> <target name="ear" depends="war,jar"> <mkdir dir="${build.jars}"/> <ear destfile="${build.jars}/${project.name}.ear" appxml="${project.resources}/META-INF/application.xml"> <fileset dir="${build.jars}" includes="*.war,*.jar" /> <metainf dir="${project.resources}/META-INF"> <include name="jboss-app.xml"/> </metainf> <fileset dir="${seam.home}"> <include name="lib/jboss-seam.jar"/> <include name="lib/jboss-el.jar"/> </fileset> </ear> </target> </project>

Ant et les buildfiles?
Si vous n'êtes pas à l'aise avec Ant ou les buildfiles, n'hésitez pas à consulter l'article Premiers pas avec Ant ou les autres articles de la catégorie Ant.

Note:
Utilisateurs d'Eclipse, vous pouvez directement ouvrir ce projet en allant dans le menu "File > New Java Project" Dans la boîte de dialogue, cochez l'option "Create from existing path" et choisissez le répertoire de votre projet. Vous pourrez ensuite utiliser l'IDE pour éditer plus facilement vos fichiers.
Vous pouvez tester votre buildfile avec la commande suivante:
sh$ ant ear Buildfile: build.xml war: [mkdir] Created dir: /home/sylvain/dev/KerBeurreProject/build/jars [war] Building war: /home/sylvain/dev/KerBeurreProject/build/jars/app.war compile: [mkdir] Created dir: /home/sylvain/dev/KerBeurreProject/build/classes jar: [jar] Building jar: /home/sylvain/dev/KerBeurreProject/build/jars/app.jar ear: [ear] Building ear: /home/sylvain/dev/KerBeurreProject/build/jars/KerBeurreProject.ear BUILD SUCCESSFUL Total time: 0 seconds
Sous-scénario 1.1 - sans base de données
Voilà, les bases du projet sont jetées. Nous verrons le contenu des différents fichiers XML au fur et à mesure du développement de notre projet. Passons donc dès maintenant à la création du modèle de données.
Entity bean/Création du modèle de données
Même si nous n'avons pas décidé de coder d'entrée de jeu tout le scénario 1, il est relativement aisé de se rendre compte que notre application va manipuler 3 entités différentes: des clients, des lots et des paramètres de cuisson. Chacune de ces entités va être représentée par un entity bean (c'est à dire un objet Java persistant).

Note:
Utilisateurs d'Eclipse, pour utiliser votre IDE pour éditer vos classes, faites les manipulations suivantes:
- Dans les propriétés du projet, cherchez "Java Build Path / Source", cliquez sur "Add folder..." et choisissez le répertoire src de votre projet.
- Dans la même boite de dialogue, remplacez "Default output folder" par le répertoire build/classes de votre projet.
- Vous pouvez également configurer Ant comme builder pour votre application: Dans "Propriétés du projet > Builder" cliquez sur "New..." et sélectionnez "Ant builder". Configurez ensuite selon vos préférences.
Voici donc le fichier client.java:
package com.kerbeurre; import javax.persistence.*; @Entity public class Client { @Id @GeneratedValue private long IdClient; private String nom; private String adresse; @SuppressWarnings("unused") private Client() {} Client(String nom, String adresse) { this.nom = nom; this.adresse = adresse; } public String getAdresse() { return adresse; } public void setAdresse(String adresse) { this.adresse = adresse; } public String getNom() { return nom; } @Override public String toString() { return nom + "(" + adresse + ")"; } }

Note:
Utilisateurs d'Eclipse, pour pouvoir compiler vos entity bean il va vous falloir ajouter la bibliothèque persistence-api.jar dans le build path de votre projet ("Propriétés du projet > Java Build Path > Libraries > Add External JARs...").
Et maintenant, le fichier Lot.java:
package com.kerbeurre; import java.util.Date; import javax.persistence.*; @Entity public class Lot { @SuppressWarnings("unused") @Id @GeneratedValue private long IdLot; private String produit; private Date dateFabrication; @ManyToOne(cascade=CascadeType.ALL) private Client client; @OneToOne(cascade=CascadeType.ALL) private Cuisson cuisson; /* * Pour Hibernate */ @SuppressWarnings("unused") private Lot() {} public Lot(String produit, Client client) { this.produit = produit; this.client = client; this.dateFabrication = new Date(); } public String getProduit() { return produit; } public void setProduit(String produit) { this.produit = produit; } public Date getDateFabrication() { return (Date) dateFabrication.clone(); } public Client getClient() { return client; } public Cuisson getCuisson() { return cuisson; } public void setCuisson(Cuisson cuisson) { this.cuisson = cuisson; } @Override public String toString() { return produit + " pour " + client.toString(); } }
La cuisson, justement, est définie par deux propriétés: la durée et la température. La classe Cuisson se contente d'encapsuler ces données et est donc on ne peut plus simple à créer. En voici la définition:
package com.kerbeurre; import javax.persistence.*; @Entity public class Cuisson { @SuppressWarnings("unused") @Id @GeneratedValue private long IdCuisson; private int duree; private int temperature; /* Pour Hibernate */ @SuppressWarnings("unused") public Cuisson() {} public int getDuree() { return duree; } public void setDuree(int duree) { this.duree = duree; } public int getTemperature() { return temperature; } public void setTemperature(int temperature) { this.temperature = temperature; } @Override public String toString() { return temperature + " pendant " + duree + "s"; } }
Les vues

Remarque:
Toutes les vues (pages html) vont dans le répertoire view du projet.
Dans le sous-scénario 1.1, la seule page nécessaire est celle qui présente la liste des productions en cours. Voici le fichier encours.xhtml correspondant:
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:f="http://java.sun.com/jsf/core" xmlns:h="http://java.sun.com/jsf/html"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title>En cours</title> </head> <body> <h1>Productions en cours:</h1> <h:dataTable value="#{production.productionsEnCours()}" var="encours"> <h:column>#{encours.toString()}</h:column> </h:dataTable> </body> </html>
Comme vous le constatez, il n'est pas très long: il s'agit juste d'une table de données (dataTable) qui parcourt la liste des productions (obtenue par l'appel de la méthode productionEnCours d'un objet production), et qui affiche pour chacune la description de la production (Obtenue par un appel à la méthode toString()).

Seam ... déjà!
Nous utilisons déjà Seam sur ce simple fichier XHTML! En effet, c'est une extension de JSF fournie par Seam qui permet d'appeler une méthode à partir d'une expression #{...}. Pour obtenir la liste des productions ça ne change pas grand chose par rapport à l'utilisation d'un getter. Par contre, pour générer la description des productions, il est plus élégant d'utiliser un appel à toString...
Session bean/Le contrôleur
Vous l'avez remarqué, toutes les interactions de la page encours.xhtml se font au travers de l'objet production. Dans une application web JSF classique, il s'agirait d'un managed bean à configurer dans le fichier faces-config.xml.
Avec Seam, le travail du contrôleur peut être confié à un session bean EJB3. Ne vous laissez pas impressionner par les mots: il s'agit simplement d'un objet qui code la logique (d'une partie) de l'application.

session bean vs entity bean
Les entity bean représentent des objets du domaine d'application. Ainsi, par exemple une instance de la classe Cuisson représente des paramètres de cuisson. Par contre, ces objets ne connaissent pas la logique de l'application: Ainsi, ils ne savent pas quand ils doivent être enregistrés dans la base de données, ou encore dans quel ordre ils doivent être manipulés. Tout ce travail est de la responsabilité d'un autre objet: le contrôleur (manager ou session bean dans la terminologie EJB3). Généralement, chaque scénario possède son propre contrôler. En quelque sorte, c'est le session bean qui code le scénario.
Une manière d'appréhender le contrôleur est de penser en terme de service: quel(s) service(s) doit-il rendre au reste de l'application? Dans notre cas, et si l'on s'en tient toujours au sous-scénario 1.1, notre contrôleur n'offre pour l'instant qu'un seul service: donner la liste des productions en cours.

Services et méthodes
Vous pouvez associer services et méthodes dans votre esprit: un service est rendu par une (éventuellement plusieurs) méthode(s).
Dans notre exemple, pour savoir quel service doit être rendu par notre contrôleur, il suffit de chercher quelle méthode du contrôleur est appelée dans la page JSF. Ici, il n'y a que productionEnCours. C'est donc le seul service à implémenter pour l'instant.
Voici maintenant le code de la classe ProductionManagerAction:
package com.kerbeurre; import java.util.*; import javax.ejb.*; import org.jboss.seam.annotations.*; import static org.jboss.seam.ScopeType.*; @Stateful @Scope(SESSION) @Name("production") public class ProductionManagerAction implements ProductionManager { @Override public List<Lot> productionsEnCours() { List<Lot> result = new ArrayList<Lot>(); Client pbeurre = new Client("Au petit beurré", "5 rue du Cake, 35000 Rennes"); Client delices = new Client("Les délices sablés", "12 av. de la plage, 56170 Quiberon"); result.add(new Lot("palets", pbeurre)); result.add(new Lot("sablés", delices)); result.add(new Lot("palets", delices)); return result; } @Remove @Destroy public void destroy() { // nothing to do } }

Note:
Utilisateurs d'Eclipse, pour pouvoir compiler, il vous faudra ajouter les bibliothèques jboss-seam.jar et ejb-api.jar à votre build path.

Remarque:
Vous le constatez , dans la méthode productionsEnCours, nous ne faisons aucun accès à la base de données. Nous nous contentons pour l'instant de renvoyer des données codées en dur dans le programme. A nouveau, dans l'esprit d'un développement itératif, ce point est laissé pour plus tard. Mais ne vous inquiétez pas, nous allons y venir. Mais dans un premier temps, ce qui nous intéresse surtout c'est de pouvoir tester l'intégration entre JSF et EJB3, c'est pourquoi nous laissons Hibernate un peu de côté.
Dans le code de ProductionManagerAction les éléments notables sont les annotations dont voici le détail:
- @Stateful Indique que l'on est en train de définir un contrôleur qui pourra être utilisé pour plusieurs pages successives.
- @Scope Indique que la durée de vie du contrôleur s'étend sur toute la session en cours.
- @Name Indique le nom sous lequel le contrôleur courant sera connu dans les autres modules de l'application. C'est le nom indiqué ici qui permet de désigner le contrôleur dans les pages JSF.
- @Remove et @Destroy indiquent la méthode que le serveur d'application doit appeler quand la session se termine (@Remove) ou quand elle est détruite à la suite d'un time-out (@Destroy). Les spécifications EJB3 obligent à avoir une méthode avec ces annotations pour tout Stateful session bean, même si aucune opération n'est nécessaire.

Note:
Conjointement, les annotations @Stateful @Scope(SESSION) indiquent donc que le même bean sera utilisé par toutes les requêtes issues de la même session. Ainsi, cet objet pourra conserver en mémoire, sur le serveur, des données d'une requête à l'autre. Et le container garantit que pour deux sessions différentes, deux bean différents seront utilisés. Il n'y a donc pas de "mélange" possible entre les données de plusieurs clients.
EJB3 supporte d'autre types et portées pour les session bean: ainsi, un bean peut-être associé à d'autres contextes que celui de la session. De la même manière, un session bean peut aussi être @Stateless, ce qui signifie que le container d'application n'associe plus ce bean à un contexte particulier mais utilise n'importe quel objet disponible pour n'importe quelle requête. On ne peut alors plus utiliser ce bean pour conserver des informations d'une requête à l'autre. On dit alors que ce bean est sans état. Pour plus d'informations, vous pourrez vous reporter à la documentation de Sun: http://java.sun.com/j2ee/1.4/docs/tutorial-update2/doc/EJBConcepts3.html.
Vous remarquerez également que ProductionManagerAction réalise l'interface ProductionManager. A nouveau ceci nous est imposé par les spécifications EJB3: chaque contrôleur est associé à une interface qui publie les méthodes du contrôleur utilisables par les autres modules. Ici, deux méthodes sont publiées:
- productionsEnCours (appelée explicitement dans la page JSF)
- destroy (appelée automatiquement à la fin de la session)
L'interface contiendra donc ces deux méthodes:
package com.kerbeurre; import java.util.List; import javax.ejb.*; @Local public interface ProductionManager { public List<Lot> productionsEnCours(); public void destroy(); }
Ici la seule chose notable à l'annotation @Local qui précise que cette interface doit être utilisée pour communiquer avec le contrôleur par les modules qui tournent sur le même serveur d'application que lui. A l'inverse, il est possible d'utiliser une interface distante (@Remote) pour communiquer avec un contrôleur chargé sur un autre serveur d'application. Mais ceci sort du cadre de ce tutoriel.
Configuration
Bien, avant de pouvoir (enfin) tester, il nous faut faire encore un peu de configuration (vous vous souvenez, les fichiers XML que l'on avait laissé de côté au début). La mauvaise nouvelle est qu'il y a un total de 8 fichiers à modifier. La bonne, c'est qu'il n'y a quasiment rien d'essentiel à comprendre dans ces fichiers, et qu'un simple copier-coller fera l'affaire!
Tout d'abord, application.xml. Ce fichier indique principalement à quelle URL répond notre application:
<application> <display-name>KerBeurreProject</display-name> <module> <web> <web-uri>app.war</web-uri> <context-root>/KerBeurreProject</context-root> </web> </module> <module> <ejb>app.jar</ejb> </module> <module> <ejb>lib/jboss-seam.jar</ejb> </module> <module> <java>lib/jboss-el.jar</java> </module> </application>
Maintenant web.xml (que vous connaissez si vous avez déjà créé une Servlet ou une page JSF):
<?xml version="1.0" encoding="UTF-8"?> <web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"> <context-param> <param-name>javax.faces.CONFIG_FILES</param-name> <param-value>/WEB-INF/navigation.xml</param-value> </context-param> <!-- Seam --> <listener> <listener-class>org.jboss.seam.servlet.SeamListener</listener-class> </listener> <listener> <listener-class>com.sun.faces.config.ConfigureListener</listener-class> </listener> <servlet> <servlet-name>Seam Resource Servlet</servlet-name> <servlet-class>org.jboss.seam.servlet.ResourceServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>Seam Resource Servlet</servlet-name> <url-pattern>/seam/resource/*</url-pattern> </servlet-mapping> <filter> <filter-name>Seam Filter</filter-name> <filter-class>org.jboss.seam.servlet.SeamFilter</filter-class> </filter> <filter-mapping> <filter-name>Seam Filter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <context-param> <param-name>javax.faces.STATE_SAVING_METHOD</param-name> <param-value>server</param-value> </context-param> <context-param> <param-name>facelets.DEVELOPMENT</param-name> <param-value>true</param-value> </context-param> <context-param> <param-name>javax.faces.DEFAULT_SUFFIX</param-name> <param-value>.xhtml</param-value> </context-param> <servlet> <servlet-name>Faces Servlet</servlet-name> <servlet-class>javax.faces.webapp.FacesServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <!-- Faces Servlet Mapping --> <servlet-mapping> <servlet-name>Faces Servlet</servlet-name> <url-pattern>*.seam</url-pattern> </servlet-mapping> </web-app>
Pour JSF, le fichier faces-config.xml:
<faces-config version="1.2" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_1_2.xsd"> <application> <view-handler>com.sun.facelets.FaceletViewHandler</view-handler> </application> </faces-config>
jboss-app.xml:
<jboss-app> <loader-repository> KerBeurreProject:archive=KerBeurreProject.ear </loader-repository> </jboss-app>
Maintenant, pour Seam component.xml:
<?xml version="1.0" encoding="UTF-8"?> <components xmlns="http://jboss.com/products/seam/components" xmlns:core="http://jboss.com/products/seam/core" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation= "http://jboss.com/products/seam/core http://jboss.com/products/seam/core-1.1.xsd http://jboss.com/products/seam/components http://jboss.com/products/seam/components-1.1.xsd"> <core:init jndi-pattern="KerBeurreProject/#{ejbName}/local" debug="true"/> <core:manager conversation-timeout="120000"/> </components>
Et enfin ejb-jar.xml qui permet à Seam d'intercepter certains évènements sur les classes EJB3:
<?xml version="1.0" encoding="UTF-8"?> <ejb-jar xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/ejb-jar_3_0.xsd" version="3.0"> <interceptors> <interceptor> <interceptor-class>org.jboss.seam.ejb.SeamInterceptor</interceptor-class> </interceptor> </interceptors> <assembly-descriptor> <interceptor-binding> <ejb-name>*</ejb-name> <interceptor-class> org.jboss.seam.ejb.SeamInterceptor </interceptor-class> </interceptor-binding> </assembly-descriptor> </ejb-jar>
Restent persistence.xml et navigation.xml. Ceux-ci ne nous sont pas utiles pour l'intant (pour l'un, puisque nous n'utilisons pas encore Hibernate, et pour l'autre car notre application n'a pour l'instant qu'une page). Le fichier persistence.xml se réduit pour l'instant à sa plus simple expression:
<persistence></persistence>
Et de même concernant navigation.xml:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE faces-config PUBLIC "-//Sun Microsystems, Inc.//DTD JavaServer Faces Config 1.0//EN" "http://java.sun.com/dtd/web-facesconfig_1_0.dtd"> <faces-config> <!-- no navigation rules --> </faces-config>

Et il faut faire tout ça pour chaque projet?
Les nombreux fichiers de configuration ont de quoi impressionner. Néanmoins, sachez qu'ils sont quasiment identiques d'un projet Seam à l'autre! Et la plupart des modifications ne vont pas plus loin que changer le nom du projet ou modifier l'URL à laquelle il répond...
Déployer et tester
Voilà, maintenant que nous avons notre page XHTML, nos codes sources Java et la configuration en place, laissons Ant travailler:
sh$ ant ear Buildfile: build.xml war: [mkdir] Created dir: /home/sylvain/dev/KerBeurreProject/build/jars [war] Building war: /home/sylvain/dev/KerBeurreProject/build/jars/app.war compile: [mkdir] Created dir: /home/sylvain/dev/KerBeurreProject/build/classes [javac] Compiling 5 source files to /home/sylvain/dev/KerBeurreProject/build/classes jar: [jar] Building jar: /home/sylvain/dev/KerBeurreProject/build/jars/app.jar ear: [ear] Building ear: /home/sylvain/dev/KerBeurreProject/build/jars/KerBeurreProject.ear BUILD SUCCESSFUL Total time: 2 seconds
Vous devriez maintenant avoir dans le répertoire build/jars l'archive de votre application KerBeurreProject.ear. Il vous reste à la déployer et tester:
sh$ cp -f build/jars/KerBeurreProject.ear ${JBOSS_HOME}/server/default/deploy/ sh$ firefox 'http://localhost:8080/KerBeurreProject/encours.seam'
Et si les choses doivent mal se passer, c'est au cours d'une de ces deux dernières opérations! Soyez attentif aux informations fournies par JBoss pendant le déploiement et le chargement de l'application. Les erreurs classiques qui peuvent survenir sont
- Un fichier de configuration n'est pas dans le bon répertoire (WEB-INF au lieu de META-INF par ex.);
- Un fichier de configuration n'a pas le bon contenu ou vous avez fait un copier/coller dans le mauvais fichier;
- Un fichier n'est par orthographié correctement (par ex face-config.xml au lieu de faces-config.xml ou seam-properties au lieu de seam.properties);
- Votre serveur peut aussi ne plus avoir de mémoire. Penser à étendre la mémoire allouée à la machine virtuelle Java;
- etc.
Bien, en espérant que vous n'avez pas rencontré d'erreur - ou que vous les avez résolues - votre client http préféré devrait vous afficher les informations suivantes:
Productions en cours: palets pour Au petit beurré(5 rue du Cake, 35000 Rennes) sablés pour Les délices sablés(12 av. de la plage, 56170 Quiberon) palets pour Les délices sablés(12 av. de la plage, 56170 Quiberon)
Sous-scénario 1.1 - avec base de données
Préparation
Bien entendu, avant de commencer à travailler avec une base de données, il faut en avoir une! Si vous utiliser MySQL, vous pouvez la créer ainsi:
sh$ mysql -u root -p password: mysql> CREATE DATABASE KerBeurreDB DEFAULT CHARACTER SET "utf8"; mysql> GRANT ALL ON KerBeurreDB.* TO kb@localhost IDENTIFIED BY "kb-password";
Et maintenant, il faut enregistrer notre base auprès de JBoss pour qu'elle soit accessible depuis notre application. La procédure est détaillée dans le tutoriel Accéder à un pilote de base de données via JNDI. Tout d'abord, éditez le fichier kerbeurre-ds.xml:
<datasources> <local-tx-datasource> <jndi-name>KerBeurreDS</jndi-name> <connection-url>jdbc:mysql://localhost/KerBeurreDB</connection-url> <driver-class>com.mysql.jdbc.Driver</driver-class> <user-name>kb</user-name> <password>kb-password</password> </local-tx-datasource> </datasources>
Puis copiez-le dans le répertoire de déploiement de JBoss:
sh$ cp ressources/kerbeurre-ds.xml ${JBOSS_HOME}/server/default/deploy
Reste à modifier le fichier persistence.xml pour indiquer à Hibernate d'utiliser la source de données que nous venons de créer:
<persistence> <persistence-unit name="KerBeurreDatabase"> <provider>org.hibernate.ejb.HibernatePersistence</provider> <jta-data-source>java:KerBeurreDS</jta-data-source> <properties> <property name="hibernate.dialect" value="org.hibernate.dialect.MySQLDialect"/> <property name="hibernate.transaction.manager_lookup_class" value="org.hibernate.transaction.JBossTransactionManagerLookup"/> <property name="hibernate.transaction.flush_before_completion" value="true"/> <property name="hibernate.hbm2ddl.auto" value="create"/> <property name="hibernate.show_sql" value="true"/> </properties> </persistence-unit> </persistence>

Piège:
Attention à la propriété hibernate.hbm2ddl.auto: elle indique à Hibernate de (re)créer la base de données à chaque déploiement, ce qui peut conduire à des catastrophes si vous déployez avec cette configuration sur votre serveur de production...
Pensez à remettre cette propriété à "" dès que votre domain model est stable!

N'oubliez pas de copier le pilote de MySQL!
Pour accéder à la base de données, il faudra que le pilote de MySQL soit accessible à votre application.
Si lorsque vous testerez, vous obtenez une erreur comme:
javax.ejb.EJBTransactionRolledbackException: org.hibernate.exception.GenericJDBCException: Cannot open connection
C'est vraisemblablement que ce n'est pas le cas.
Le plus simple est de copier le pilote mysql-connector-java dans $JBOSS_HOME/server/default/lib. Les détails sont donnés dans le tutoriel Accéder à un pilote de base de données via JNDI.
Accéder à la base de données
Reste à accéder à notre base de données à partir de notre programme. Nous allons modifier la classe ProductionManagerAction:
package com.kerbeurre; import static javax.persistence.PersistenceContextType.EXTENDED; import java.util.*; import javax.ejb.*; import javax.persistence.*; import org.jboss.seam.annotations.*; import static org.jboss.seam.ScopeType.*; @Stateful @Scope(SESSION) @Name("production") public class ProductionManagerAction implements ProductionManager { @PersistenceContext(type=EXTENDED) private EntityManager em; @SuppressWarnings("unchecked") @Override public List<Lot> productionsEnCours() { List<Lot> result = em.createQuery("select l from Lot l where cuisson = null") .getResultList(); return result; } @Remove @Destroy public void destroy() { // nothing to do } }
Comme vous le constatez, le code est plus court qu'avant! Trois modifications ont été apportées:
- Importation du package javax.persistence.*
- Ajout de l'attribut private EntityManager em. C'est au travers de cet attribut que nous utiliserons Hibernate. Tout le travail de connexion est fait pour nous par le serveur d'application. Celui-ci injecte directement l'objet créé grâce à l'annotation @PersistenceContext.
- Remplacement du corps de la méthode productionsEnCours par une requête JPQL (Java Persistence Query Language).
Si vous recompilez et redéployez votre application, vous devriez constater qu'aucune production n'est actuellement en cours. C'est normal, la base de données est vide. Nous allons donc peupler un peu cette base de données à la main:
INSERT INTO `Client` VALUES (1,'Au petit beurré','5 rue du Cake, 35000 Rennes'), (2,'Les délices sablés','12 av. de la plage, 56170 Quiberon'); INSERT INTO `Lot` VALUES (4,'palets','2008-01-28 18:55:48',NULL,1), (5,'sablés','2008-01-28 18:55:48',NULL,2), (6,'palets','2008-01-28 18:55:48',NULL,2);

Note:
Il nous serait bien entendu possible de créer une interface pour saisir ces informations, mais il s'agirait là d'un autre scénario d'utilisation de notre application. Vraisemblablement à mettre en œuvre dès que possible...
Désormais, la base contient des informations. Néanmoins avant de poursuivre, il vous faut modifier le fichier persistence.xml pour qu'Hibernate n'écrase pas automatiquement la base au prochain déploiement:
<!-- ... --> <property name="hibernate.hbm2ddl.auto" value=""/> <!-- ... -->
Voilà, le scénario 1.1 est complet. Avant de poursuivre, n'hésitez pas à manipuler et/ou faire des modifications pour comprendre le points qui vous semblent obscurs.
Scénario 1 - navigation et création
Par rapport au sous-scénario 1.1, le scénario 1 utilise deux techniques supplémentaires: tout d'abord la navigation entre pages, puis la création d'un objet via une page JSF.
Commençons par créer les deux autres pages nécessaires à notre application. Tout d'abord cuisson.xhtml:
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:f="http://java.sun.com/jsf/core" xmlns:h="http://java.sun.com/jsf/html"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title>Cuisson/Fin de prod.</title> </head> <body> <h1>Cuisson/Fin de prod.</h1> <p>Vous éditez le lot #{lot.toString()}</p> <form jsfc="h:form" id="form_cuisson"> <p> <label for="duree">Durée:</label> <input type="text" jsfc="h:inputText" value="#{cuisson.duree}" id="duree"/> <h:message for="duree" showSummary="true" showDetail="false" /> </p> <p> <label for="temp">Temp.:</label> <input type="text" jsfc="h:inputText" value="#{cuisson.temperature}" id="temp"/> <h:message for="temp" showSummary="true" showDetail="false" /> </p> <p> <input type="submit" jsfc="h:commandButton" value="Valider" action="#{production.enregistrerCuisson}" /> </p> </form> </body> </html>
Et maintenant, confirm.xhtml:
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:f="http://java.sun.com/jsf/core" xmlns:h="http://java.sun.com/jsf/html"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title>Cuisson/Fin de prod.</title> </head> <body> <h1>Cuisson/Fin de prod.</h1> <p>La production du lot #{lot.toString()} est cloturée avec les paramètres de cuisson suivants:</p> <dl> <dt>Durée</dt><dd>#{lot.cuisson.duree}</dd> <dt>Température</dt><dd>#{lot.cuisson.temperature}</dd> </dl> <p><a href="encours.seam">Autres productions en cours</a></p> </body> </html>
Enfin, il nous faut également revenir sur la page encours.xhtml. En effet, dans le scénario 1 il faut que l'utilisateur puisse cliquer sur une production. Ce qui n'était pas prévu dans le sous-scénario 1.1:
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:f="http://java.sun.com/jsf/core" xmlns:h="http://java.sun.com/jsf/html"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title>En cours (v2)</title> </head> <body> <h1>Productions en cours:</h1> <h:form> <h:dataTable value="#{production.productionsEnCours()}" var="encours"> <h:column> <h:commandLink value="#{encours.toString()}" action="#{production.clore(encours)}" /> </h:column> </h:dataTable> </h:form> </body> </html>
Ces trois fichiers n'ont rien de vraiment notable. Mais si vous les regardez de plus près, vous remarquerez qu'il font référence à 3 objets: production (comme dans le scénario 1.1) mais aussi lot et cuisson. D'où viennent ces deux derniers objets? Ce sont en fait des propriétés de production outjectés par Seam. C'est ce que nous allons voir maintenant.
Contrôleur et outjection
Tout d'abord, le listing de ProductionManagerAction.java:
package com.kerbeurre; import java.util.*; import javax.ejb.*; import javax.persistence.*; import static javax.persistence.PersistenceContextType.EXTENDED; import org.jboss.seam.annotations.*; import static org.jboss.seam.ScopeType.*; @Stateful @Scope(SESSION) @Name("production") public class ProductionManagerAction implements ProductionManager { @PersistenceContext(type=EXTENDED) private EntityManager em; @Out(required=false) private Lot lot; @Out(required=false) private Cuisson cuisson; @SuppressWarnings("unchecked") @Override public List<Lot> productionsEnCours() { List<Lot> result = em.createQuery("select l from Lot l where cuisson = null") .getResultList(); return result; } // @Override public String clore(Lot lot) { this.lot = lot; this.cuisson = new Cuisson(); return "cuisson"; } // @Override public String enregistrerCuisson() { lot.setCuisson(cuisson); return "confirmation"; } @Remove @Destroy public void destroy() { // nothing to do } }
Comme vous le voyez, nous avons ajouté deux attributs (lot et cuisson), et codé les différentes méthodes liées à notre scénario (business methods): clore et enregistrerCuisson. A nouveau rien de complexe ici.
Toute la magie de Seam réside dans les deux annotations @Out(required=false): ces annotations indiquent que la propriété qui suit doit être rendue accessibles aux pages JSF. C'est grâce à elles que l'on peut accéder directement aux attributs lot et cuisson à partir des pages JSF. Dans le vocabulaire de Seam, on dit que les attributs sont outjectés (par analogie avec injectés).

Où est Hibernate?
Vous êtes peut-être surpris de ne pas voir de nouveau code lié à la persistance de la cuisson ou à la mise à jour du lot dans la base de données.
En fait, Hibernate gère tout cela en toute transparence lors de l'exécution de la ligne de code suivante:
/* ... */ lot.setCuisson(cuisson); /* ... */
Bien entendu, il faut aussi penser à modifier l'interface ProductionManager pour publier les nouvelles méthodes de notre business logic:
package com.kerbeurre; import java.util.List; import javax.ejb.*; @Local public interface ProductionManager { public List<Lot> productionsEnCours(); public String clore(Lot lot); public String enregistrerCuisson(); public void destroy(); }
Navigation
La navigation entre nos trois pages est des plus classiques. Pour plus de détails, vous vous reporterez au tutoriel Navigation dans des pages JSF. Ici, nous nous contenterons de donner le contenu du fichier navigation.xml:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE faces-config PUBLIC "-//Sun Microsystems, Inc.//DTD JavaServer Faces Config 1.0//EN" "http://java.sun.com/dtd/web-facesconfig_1_0.dtd"> <faces-config> <navigation-rule> <from-view-id>*</from-view-id> <navigation-case> <from-outcome>encours</from-outcome> <to-view-id>/encours.xhtml</to-view-id> <redirect /> </navigation-case> <navigation-case> <from-outcome>confirmation</from-outcome> <to-view-id>/confirm.xhtml</to-view-id> <redirect /> </navigation-case> <navigation-case> <from-outcome>cuisson</from-outcome> <to-view-id>/cuisson.xhtml</to-view-id> <redirect /> </navigation-case> </navigation-rule> </faces-config>
Vous avez désormais tout en main pour compiler et déployer votre application. Celle-ci implémente le scénario 1 défini au tout début de ce tutoriel. Lorsque vous aurez constaté qu'elle fonctionne, n'hésitez pas à expérimenter: Toutes les techniques abordées ici fournissent des solutions pratiques à bien des problèmes rencontrés dans les applications d'entreprise!
Un dernier mot
L'application développée ici offre de nombreuses possibilités d'évolution. Il y a aussi des améliorations possibles au scénario mis en oeuvre. Par exemple, il serait préférable que la portée du contrôleur soit CONVERSATION plutôt que SESSION. Hein? Qu'est-ce que ça veut dire? Et oui: il y a encore beaucoup de choses à dire sur Seam. Mais rassurez-vous, nous ne verrons pas ça maintenant!
Lorsque vous aurez digéré un peu ce tutoriel, n'hésitez pas à chercher de la documentation sur Seam sur Internet: c'est une technologie efficace, pratique et qui monte. Et ce serait un peu léger de la rejeter simplement parce qu'il y a une phase de configuration un peu lourde au début du projet: si vous comparez le temps mis pour réaliser le sous-scénario 1.1 avec le temps mis pour réaliser le scénario 1 complet vous remarquerez que ce surcoût est très vite amorti...
Ressources
- Le code complet de ce projet peut être téléchargé à l'adresse http://www.esicom-st-malo.fr/pub/seam/KerBeurreProject.zip
Ressources
- Le code complet de ce projet peut être téléchargé à l'adresse http://www.esicom-st-malo.fr/pub/seam/KerBeurreProject.zip
- Christian Bauer, Gavin King. Java persistence with Hibernate. Manning Publications, 2007. ISBN 1-932-39488-5.
(il y a en particulier un chapitre sur Seam à la fin)
- Debu Panda, Reza Rahman, Derek Lane. EJB3 in Action. Manning Publications, 2007. ISBN 1-933-98834-7.
- Michael Juntao Yuan, Thomas Heute. JBoss Seam: Simplicity and Power Beyond Java EE. Prentice Hall, 2007. ISBN 0-131-34796-9.
- http://labs.jboss.com/jbossseam/