Vous n'êtes pas connecté. Connexion
|
|
Design patterns Java EE 6 with JSF / EJB / JPADe $1Table des matières
IntroductionThis la exercice/tutorial shows an application that uses different Java EE frameworks. It shows the use of the MVC framework JSF 2, the use of Enterprise Java Beans, in particular as session facades or singleton objects, the use of JPA and entity classes as models shared all along the different layers of the application. Application architecture: Creation of a project of type "Enterprise Application"This is a special kind of project made of different modules : EJB modules, web modules, etc. All these sub-projects are grouped in a single project of type enterprise, that acts as a single container. The Java EE project is in a .ear file, (ear meas Enterprise ARchive) and makes the deployement easier. Use Netbeans menu File/New Project choose "Java EE" as a category, and choose the type of project as "Enterprise Application". Click "Next" then give a name to the project. Do not use strange characters in the project name please ! In the screenshots, we chose TP1CustomerApplication for the project name. In the next dialog, you will choose the application server, the Java EE version, etc. Keep all the default values. Do not forget to click the "Enable Context and Dependency Injection" checkbox ! Otherwise JSF will not work !
Click on finish now. Normally you will see 3 projects added to Netbeans:
Create an entity class from an existing databaseJava EE contains a framework named JPA for Java Persistence API. In that framework, special classes called "entity classes" correspond to tables in databases. The mapping between these classes and tables is done using external ORM tools like Hibernate or EclipseLink. These tools provides some means for generating automatically entity classes from the table models. This is what we are going to use. In the EJB project, the one with a java bean icon, do a right clic on the project name, and choose New/Entity class from database: You will be asked the JNDI name of the database. Choose "jdbc/sample". It's a database that comes bundled with the JavaDB database included in glassfish 3. So, no need to run a database server, create by hand some database, tables, etc. Normally after you chosed the name of the database, the left list will appear showing the available tables: Choose "CUSTOMER"and click the "add" button, this will add in fact two tables: CUSTOMER and also DISCOUNT_CODE as there is a relation between these two tables: Click "Next". Now you may change the names of the Java entity classes that will be generated. I recommend to let all the default values. One good practice is to put them in a package named "entities", like shown in this screenshot: Click "Next".Leave all default values...
Double click persistence.xml, you will see a designer that looks like that: Without entering into details, we recognize that
Creation of a Customer facadeWe will now add to the project a DAO / Facade that will take charge of all CRUD operations on the Customer entities. If one wants to insert, update, remove, search entities in the database jdbc/sample, he will have to use this facade ! We will implement this facade as a stateless session bean EJB: Do a right click New/Session Bean on the EJB project: Give a name to your facade. In general we would call it CustomerFacade or CustomerDAO or CustomerManager or CustomerHandler, and we will put it in a package named "sessions" in order to differentiate it Add some business methods in the facadeDouble click "CustomerManager.java" in the project so that the source code is shown in the editor. Click somewhere in the class (after the { for example) and right click / insert code. This will show a very interesting menu... Choose "add business méthod" and add a method named getAllCustomers() that will return a List of Customers: In the source, add the imports that are missing (click on the small yellow icon, or right click/fix imports, or Alt-Shift-I, or ⌘-Shift-I if you have a Mac). Add also a method update(Customer c) {...} that will be useful for updating a customer. You should get a code that looks like that: package session; import entities.Customer; import java.util.Collection; import javax.ejb.Stateless; import javax.ejb.LocalBean; @Stateless @LocalBean public class CustomerManager { public List<Customer> getAllCustomers() { return null; } public Customer update(Customer customer) { return null; } } Of course you could have typed these methods by hand, it was just cool to show you some netbeans goodies... Now we will implement these methods. Explanation are in the full JPA course but for now, just follow the instructions.
@PersistenceContext(unitName = "TP1CustomerApplication-ejbPU") // the name can be different in your case private EntityManager em; public void persist(Object object) { em.persist(object); } The variable em does not need to be initialized, as it will be "injected". The name just after the "unitName=" parameter, in the example "TP1CustomerManager-ejbPU", is also in the persistence.xml file. , New version of the methods: package session; import entities.Customer; import java.util.Collection; import javax.ejb.Stateless; import javax.ejb.LocalBean; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; @Stateless @LocalBean public class CustomerManager { @PersistenceContext private EntityManager em; public List<Customer> getAllCustomers() { Query query = em.createNamedQuery("Customer.findAll"); return query.getResultList(); } public Customer update(Customer customer) { return em.merge(customer); } public void persist(Object object) { em.persist(object); } } Here we are, we implemented these two methods. The first one executes a "named query" whose name is "Customer.findall". Look at the beginning of the file Customer.java, you will find this query. It's equivalent to a select * from CUSTOMER. But as we are working with objects, in that case we will return a List of Customers, not tuples. The second method, update(Customer customer)... updates the database content with the value of the Customer passed as a parameter. The em.merge(customer); call does exactly that: "take this object in memory, look if it exists in the database, then update the columns in the database if they do not correspond to the customer object property values. Objects have a primary key (in that case, look at the id property in the Customer.java class). Normally you should fix the imports, but beware at the Query class, you must import it from the javax.persistence package, not from the javax.management package ! Ok, we are fone with the business part of the project. Then we will look at the web part. Create a JSF managed bean as a controllRight click on the project and add/java server faces / jsf managed bean Give a name to this JSF bean. As it will manage the GUI for Customers, we call it CustomerMBean and add it to a package named managedbeans. We choose that its lifetime will be the HTTP session, so we set its scope to be "session". You should get a CustomerMBean.java that looks like that: package managedbeans; import javax.inject.Named; import javax.enterprise.context.SessionScoped; import java.io.Serializable; @Named(value = "customerMBean") @SessionScoped public class CustomerMBean implements Serializable { public CustomerMBean() { } } Note the annotation @SessionScoped. This annotation says that 1) the bean will last the duration of the http session and 2) its attributes/properties will be stored in the HTTP session. We will use the properties of this class as Models for display/modification in the JSF page (they are views in our approach). Now we will add some functionalities to this JSF bean:
To call an EJB, right click / insert code / call enterprise bean. This will just add these two lines: @EJB private CustomerManager customerManager; Here again, the @EJB annotation injects the customerManager variable. No need for a new()... This annotation acts both as a service locator and as a factory. But this time, control is reversed, (we call it "inversion of control" or IOC: the right object is built by a "third person", in this case, the EJB container). Let's complete the JSF bean code: package managedbeans; import entities.Customer; import javax.ejb.EJB; import javax.inject.Named; import javax.enterprise.context.SessionScoped; import java.io.Serializable; import java.util.List; import session.CustomerManager; @Named(value = "customerMBean") @SessionScoped public class CustomerMBean implements Serializable { @EJB private CustomerManager customerManager; /* models/properties */ private Customer customer; // for display/update/insert in a form private List<Customer> customers; // for display in a datatable public CustomerMBean() { } /** * returns customer list for display in a datatable DataTable * @return */ public List<Customer>getCustomers() { if((customers == null) || (customers.isEmpty())) refresh(); return customers; } public void refresh() { tousLesComptes = compteBancaireFacade.findAll(); } /** * returns details of a customer. Useful for displaying in a form a customer's details * @return */ public Customer getDetails() { return customer; } /** * Action handler - Called when a line in the table is clicked * @param customer * @return */ public String showDetails(Customer customer) { this.customer = customer; return "CustomerDetails"; // will display CustomerDetails.xml JSF page } /** * Action handler - update the customer model in the database. * called when one press the update button in the form * @return */ public String update() { System.out.println("###UPDATE###"); customer = customerManager.update(customer); return "CustomerList"; // will display the customer list in a table } /** * Action handler - returns to the list of customers in the table */ public String list() { System.out.println("###LIST###"); return "CustomerList"; } } Add a JSF page for displaying customers in a tableLet's add a JSF page to the project. Right click / new Java Server Faces / JSF page:
Click "next", then give a name to the JSF page: This will add a CustomerList.xhtml file in the project. Double click to see the source. You may add some text or html code there... Add a datatable JSF widget in the JSF page, link it to the "customers" modelShaw the palette tool in netbeans (menu Window/Palette or shortcut ctrl-shift-8). Open the JSF part and drag'n'drop "JSF Data Table from Entity", just after the body: Une fenêtre de dialogue va apparaitre demandant de préciser pour quelle classe entité vous voulez une Data Table, indiquez la classe entité Customer, puis comme property indiquez le nom du managed bean pour les clients suivi du nom de la propriété correspondant à une liste de clients. Dans notre cas, il faut mettre customerMbean.customers. Pourquoi ? Car lorsque dans une JSP ou dans une page JSF on référencera une "propriété" de lecture, cela appellera la méthode get correspondante. Par exemple, si on référence la propriété "customers" du managed bean customerMBean, cela ne fonctionnera que si il existe une méthode getCustomers() qui renvoie une Collection de Customer (en effet, la Data Table gère des Collection). Il n'est pas nécessaire qu'une variable "customers" existe dans le bean, il suffit que la méthode Collection<Customer> getCustomers() existe. La preuve :
@Named(value = "customerMBean") @SessionScoped public class CustomerMBean implements Serializable { ... public Collection getCustomers() { return customerManager.getAllCustomers(); } Voici donc la fenêtre avec les bonnes valeurs :
Ceci devrait insérer de nombreuses lignes dans la page JSF. Avec netbeans 7.0.1 il faut rajouter aussi le namespace xmlns:f="http://java.sun.com/jsf/core" au tag html, vous pouvez faire ceci semi-automatiquement en utilisant l'autocomplétion dans NetBeans: placez-vous à l'intérieur d'un des tags du namespace f, puis faites ctrl+espace Exécution du projet et premier testPour exécuter le projet, faites clic droit sur le projet en forme de triangle (le projet "enterprise") et Run. Une page va s'afficher, modifiez l'URL pour afficher la bonne page :
Vous devriez obtenir le résultat suivant : Pour le moment ce tableau n'est pas très bien présenté car il n'y a aucune fioriture de mise en page. Les données proviennent de la base jdbc/sample. 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 la vue sur les tables de la base jdbc/sample et faites clic droit/view data : Vous pouvez vérifier que ce sont bien les mêmes données qui ont été affichées dans la page JSF. Maintenant on va modifier l'affichage du discountCode qui est une relation, en effet, voir "entities.DiscountCode[discountCode=M]" n'est pas très satisfaisant. Si vous avez compris le concept de "propriétés", vous pouvez modifier la ligne qui affiche le code. remplacez : <h:outputText value="#{item.discountCode}"/> par : <h:outputText value="#{item.discountCode.discountCode} : #{item.discountCode.rate}%" /> Ce qui aura pour effet de remplacer la première expression par le résultat de l'appel de la méthode getDiscountCode() de la classe DiscountCode.java, et la seconde par la valeur renvoyée par getRate() de cette même classe. L'affichage sera bien meilleur : Etudiez maintenant le code de la page JSF, du managed bean, du session bean de l'entity bean, et essayez de retrouver les morceaux que vous avez développés dans le schéma présenté dans la toute première illustration de cette page. Regardez également comment s'articulent les différentes parties et dans quel ordre elles sont exécutées. Remplacement de la DataTable par une provenant de la librairie de composants JSF PrimeFacesAjout de la librairie PrimeFaces dans le projetPrimeFaces propose des composants évolués/complémentaires pour JSF2, le site web de référence est : http://www.primefaces.org Cette librairie est déjà présente dans netbeans 7.2, dans sa dernière version 3.2. Il suffit de l'ajouter au projet pour pouvoir l'utiliser : faites sur le projet web (celui précédé d'une icône en forme de globe terrestre) clic droit/properties, puis allez dans "Librairies", cliquez sur "Add library", selectionnez Primefaces 3.2. Modification de la page JSFPour pouvoir utiliser des tags provenant de PrimeFaces dans une page JSF il faut ajouter le namespace suivant : (cf http://primefaces.org/gettingStarted.html) xmlns:p=xmlns:p="http://primefaces.org/ui" A partir de là on pourra utiliser des tags PrimeFaces avec le préfixe p:
Déployez le projet (clic droit sur le projet avec le triangle/deploy) puis exécutez à nouveau la page qui affiche la liste des clients. Il est nécessaire de déployer car il faut que la librairie que nous venons d'ajouter au projet soit déployée dans le serveur. Il suffit de le faire une fois pour le projet, ensuite le déploiement incrémental (avec save ou ctrl-s) suffira. Vous devriez obtenir le résultat suivant : C'est déjà mieux présenté non ? Bon, ce qui est intéressant, c'est que le composant dataTable de PrimeFaces possède de nombreuses options pour la pagination, rendre les colonnes triables, éditables, etc. Allez donc voir les démos/sources sur : http://www.primefaces.org/showcase-labs/ui/home.jsf
Affichage des détails d'un client lorsqu'on clique sur une ligneMaintenant nous allons voir comment afficher dans une autre page les détails d'un client lorsqu'on clique sur une ligne. Ajout d'un lien dans le tableau pour déclencher l'affichage des détails d'un clientModifiez la page CustomerList.xhtml de manière à ce que lorsqu'on clique sur la colonne Id on affiche le détail d'un client, comme à la ligne 18 du listing ci-dessous. <p:dataTable value="#{customerMBean.customers}" var="item" emptyMessage="No customer found with given criteria" widgetVar="customerTable" paginator="true" rows="10"> <f:facet name="header"> <p:outputPanel> <h:outputText value="Search all fields:" /> <p:inputText id="globalFilter" onkeyup="customerTable.filter()" style="width:150px" /> </p:outputPanel> </f:facet> <p:column headerText="CustomerId" sortBy="#{item.customerId}" filterBy="#{item.customerId}" filterMatchMode="contains"> <h:commandLink action="#{customerMBean.showDetails(item)}" value="#{item.customerId}"/> </p:column> ... la ligne ajoutée indique que lorsqu'on clique sur la colonne, on appelle la méthode showDetails du managedBean (n'oubliez pas : le managed bean est dans la session), et que l'on affiche dans le texte du lien l'id du client (attribut "value=..."). Dans ce cas, lorqu'on clique sur la colonne, on appelle la méthode d'instance showDetails avec en paramètre l'objet item qui correspond au client de la ligne courante. Si vous regardez le source de la méthode showDetails de CustomerMBean.java : /** * Action handler - appelé lorsque l'utilisateur sélectionne une ligne dans * la DataTable pour voir les détails * @param customer * @return */ public String showDetails(Customer customer) { this.customer = customer; return "CustomerDetails"; } vous voyez que le paramètre attendu est un client, et que cette méthode fait deux choses :
Si vous sauvegardez la page JSF et l'exécutez, vous verrez que la colonne id contient maintenant un lien hypertexte. Pour le moment il ne mène à rien car la page qui affiche les détails n'existe pas encore. Ajout d'une page JSF pour afficher les détails d'un client
Indiquez ensuite le nom de la classe entité dont vous voulez afficher les informations dans un formulaire, ainsi que le nom de la méthode du managedBean qui renvoie les détails d'un client : Ici on a mis customerMBean.details qui signifie que c'est la méthode getDetails() qui sera appelée. On aurait pu aussi mettre customerMBean.customer qui aurait appelé getCustomer(), qui fait la même chose. Une fois qu'on a validé, la page JSF se remplit avec des lignes qui sont en fait un formulaire qui sera pré-rempli dès l'affichage si il existe un objet "customer" dans la session (ce qui sera le cas, voir ce que nous avons expliqué dans la section précédente). Il est encore un peu tôt pour que tout fonctionne. Il reste maintenant à spécifier la navigation entre la page qui affiche la liste des clients et la page qui affiche le détail d'un client.
Executez le projet et testez que les détails s'affichent bienMaintenant vous pouvez exécuter le projet et voir si lorsque vous cliquez sur le lien Id dans la liste ça affiche bien le détail d'un client. Vous devriez obtenir ceci pour le client d'id=1 : Voilà, les détails du client numéro 1 s'affichent bien, cependant le code de discount pour ce client n'est pas correctement affiché. Nous allons voir maintenant comment arranger ce petit problème, comment ajouter des boutons de navigation et comment tant qu'à faire permettre une modification des données. Régler l'affichage des DiscountCodes dans le détail d'un clientDans la base de données la table des DISCOUNT_CODE contient juste 4 valeurs correspondants aux quatre types de réductions possibles. Nous allons utiliser un Converter, qui permet de transformer un objet de type entities.DiscountCode en String, et vice versa. On va commencer par rajouter au projet EJB un session bean stateless dans lequel on ajoutera une méthode List<DiscountCode> getAllDiscountCodes(). Inspirez-vous du gestionnaire de client et de la méthode qui renvoie tous les clients. Plutôt que copier/coller le code ci-dessous, essayez de refaire les étapes que nous avions faites lors de l'écriture du gestionnaire de clients (avec insert code/call enterprise bean, insert code/user Entity Manager etc...) Vous devriez avoir un code comme ceci : package session; import entities.DiscountCode; import java.util.List; import javax.ejb.Stateless; import javax.ejb.LocalBean; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.persistence.Query; @Stateless @LocalBean public class DiscountCodeManager { @PersistenceContext(unitName = "TP1CustomerApplication-ejbPU") private EntityManager em; public List<DiscountCode> getDiscountCodes() { Query query = em.createNamedQuery("DiscountCode.findAll"); return query.getResultList(); } public void persist(Object object) { em.persist(object); } } Donc, maintenant on a un EJB qui renvoie toutes les valeurs possibles de DiscountCode. On va ajouter dans le managed bean du projet web un objet de type javax.faces.convert.Converter, qui nous servira à transformer un objet de type entities.DiscountCode en String, et vice versa. On utilisera une représentation en string de la forme "L : 7.00%", comme dans la colonne du tableau de détail des clients. Pour celà, commencez par indiquer au bean qu'il va appeler un EJB : DiscountCodeManager, celui que nous venons d'écrire. Ensuite, pour utiliser ce converter dans la page JSF .xhtml, on a besoin de définir une méthode get. Voici le code à ajouter dans le managed bean CustomerMBean: @EJB private DiscountCodeManager discountCodeManager; public Converter getDiscountCodeConverter() { return discountCodeConverter; } private Converter discountCodeConverter = new Converter() { @Override public Object getAsObject(FacesContext context, UIComponent component, String value) { return new ConverterException("On verra la conversion String->Objet plus tard..."); } @Override public String getAsString(FacesContext context, UIComponent component, Object value) { DiscountCode dc = (DiscountCode) value; return dc.getDiscountCode()+" : "+dc.getRate()+"%"; } }; Finalement, on peut définir une méthode utilitaire qui servira à remplir le menu déroulant de la page JSF CustomerDetails.xhtml. Procédez comme dans le début du TP (insert code/call enterprise bean), puis insérez dans le bean la méthode suivante : /** * renvoie un tableau de SelectItem pour affichage dans le menu déroulant * de la page des détails d'un client * @return */ public List<SelectItem> getAllDiscountCodes() { List<SelectItem> options = new ArrayList<SelectItem>(); List<DiscountCode> ldc = discountCodeManager.getDiscountCodes(); for(DiscountCode dc : ldc) { options.add(new SelectItem(dc,discountCodeConverter.getAsString(null, null, dc))); } return options; } Si vous regardez ce code, il se contente de récupérer la liste des DiscountCodes et de la transformer en liste de SelectItems qui est une classe JSF utilisée par le composant <f:selecteditems ... /> qui indique le contenu du menu <h:selectOneMenu.../> situé dans la page CustomerDetail.xhtml. Chaque SelectItem prends en paramètre lors de la construction un objet (l'objet de type DiscountCode) et une string qui le décrit (formatée par le Converter, qui sera visible par l'utilisateur). On va pouvoir maintenant modifier la partie "menu" de la page CustomerDetails.xhtml afin que la méthode que nous venons d'écrire soit appelée. Voici le code qu'il faut modifier dans CustomerDetails.xhtml : <h:selectOneMenu id="discountCode" value="#{customerMBean.details.discountCode}" title="DiscountCode" required="true" requiredMessage="The DiscountCode field is required." converter="#{customerMBean.discountCodeConverter}"> <f:selectItems value="#{customerMBean.allDiscountCodes}"/> </h:selectOneMenu> Remarquons plusieurs choses :
On peut utiliser le Converter dans la page JSF CustomerList.xhtml également : <p:column headerText="DiscountCode" sortBy="#{item.discountCode.discountCode}" filterBy="#{item.discountCode.rate}%" filterMatchMode="contains"> <h:outputText value="#{item.discountCode}" converter="#{customerMBean.discountCodeConverter}"/> </p:column> Voilà, sauvegardez, testez, ça doit fonctionner : Voilà ! Maintenant il ne reste plus qu'à ajouter des boutons de navigation pour mettre à jour les données ou revenir à la liste des clients. Ajout de boutons pour la mise à jour et retour à la listeIl suffit de rajouter deux lignes dans le fichier CustomerDetails.xhtml ! Ajoutez ces deux lignes : <h:commandButton id="back" value="Back" action="#{customerMBean.list}"/> <h:commandButton id="update" value="Update" action="#{customerMBean.update}"/> juste après le menu déroulant pour les discountCodes... Vous comprenez maintenant ce que cela signifie :
Regardons la version actuelle de la méthode update() : public String update() { System.out.println("###UPDATE###"); customer = customerManager.update(customer); return "CustomerList"; } Cette méthode prend la propriété "customer", de type Customer et appelle une méthode métier dans le gestionnaire de clients qui va mettre à jour le client en base. Les attributs de cette propriété (nom, etc...) ont pour valeurs celles du formulaire de la page de détails. Si on a modifié ces valeurs avant de cliquer sur le bouton "update", les attributs de la propriété ont été modifiés. Donc on a bien dans "customer" un client mis à jour (en mémoire pour le moment, regardez ce que fait customerManager.update() pour voir. On verra en cours que c'est là que se fait la modification en base de données). Il existe en effet une relation entre les clients et les codes de remise. Regardez attentivement la classe entité Customer.java, vous verrez que le code de remise est un attribut discountCode de type DiscountCode. Or le menu déroulant dans la page de détails, lorsqu'on modifie le code de remise, nous renvoie une String. C'est le travail du Converter de transformer cette string en objet de type DiscountCode !
Observez la requête nommée DiscountCode.findByDiscountCode définie dans un attribut de la classe bean entité DiscountCode. Cette requète possède un paramètre noté :discountCode. Quand on donne la valeur d'un code de réduction à ce paramètre, on peut récupérer l'objet de type DiscountCode correspondant. On propose d'utiliser une nouvelle méthode public DiscountCode getDiscountCodeByDiscountCode(char code) dans le bean session session.DiscountCodeManager. Voici cette méthode: public DiscountCode getDiscountCodeByDiscountCode(char code) { Query query = em.createNamedQuery("DiscountCode.findByDiscountCode"); query.setParameter("discountCode", code); return (DiscountCode) query.getSingleResult(); } Observez: on associe le caractère code au paramètre de requête "discountCode", et on récupère un unique résultat de type DiscountCode: query.getSingleResult(). Voici finalement ce qu'on vous propose comme transformation String->DiscountCode par le Converter: private Converter discountCodeConverter = new Converter() { @Override public Object getAsObject(FacesContext context, UIComponent component, String value) { char code = value.charAt(0); DiscountCode dc = discountCodeManager.getDiscountCodeByDiscountCode(code); return dc; } @Override public String getAsString(FacesContext context, UIComponent component, Object value) { DiscountCode dc = (DiscountCode) value; return dc.getDiscountCode()+" : "+dc.getRate()+"%"; } }; Exécutez, testez, amusez-vous à modifier les valeurs des clients, vérifiez dans le gestionnaire de base de données de netbeans que les modifications ont été prises en compte... Problèmes devant encore être réglés :
Nous verrons comment corriger ces problèmes dans un autre TP. Pour la semaine prochaine:Inspirez-vous de l'article d'introduction à JSF 2.0 du site de Netbeans http://netbeans.org/kb/docs/web/jsf20-intro.html (Cet article contient plein de trucs pour développer plus rapidement et plus efficacement sur NetBeans !!!) pour utiliser une facelet pour rendre le site ergonomique (un en-tête avec le titre du TP (statique) et le titre de chaque page mis en forme, et une colonne de gauche qu'on utilisera plus-tard pour mettre les liens de navigation).
|
Powered by MindTouch Deki Open Source Edition v.8.08 |