Ce document présente comment passer, sur la base du tutoriel "Portlet avec Spring", du MCV Spring à JSF. |
Dates de modification | ||
---|---|---|
|
|
|
|
|
|
Entre les différentes versions du tutoriel l’id des objets Entry est passée de int à string.
Ce tutoriel a été testé avec hibernate. Dans cet exemple l'id des Entry est automatiquement "calculé" par hibernate et les vues ne proposent pas de le saisir.
Le fichier de mapping hibernate Address.hbm donné ici fonctionne.
<?xml version="1.0"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd" > <hibernate-mapping package="org.esupportail.portlet.spring.beans"> <class name="Entry" table="address"> <id name="id" column="id" type="string"> <generator class="native"/> </id> <property name="firstName" column="firstName" length="45" not-null="true" type="string" /> <property name="name" column="name" length="45" not-null="true" type="string" /> <property name="mail" column="mail" length="250" not-null="true" type="string" /> </class> </hibernate-mapping>
Comme précisé dans le résumé, ce tutoriel se limite à montrer comment mettre en œuvre JSF dans le tutoriel Spring/portlet existant.
Ainsi je m’attache ici à montrer seulement ce qui change. N’est donc concernée que la couche relative à la vue et seulement celle-ci.
Les différents packages dao, domain et bean ainsi que les fichiers de configuration de Spring (hors partie MVC) et de la couche de persistance ne sont pas modifiés. Ils ne sont donc pas repris dans ce tutoriel.
Ceci prouve, s’il en était encore besoin, que Spring permet de facilement passer d’une implémentation à une autre pour une couche de l’application sans avoir à modifier le reste de l’application.
L'implémentation de spécification JSF choisie pour ce portlet est MyFaces
Quand on prend le fichier myfaces-core-1.1.2-bin.zip, ce dernier contient un ensemble de librairies (dans le répertoire lib de l'archive) à positionner dans le répertoire lib de notre projet :
commons-beanutils-1.7.0.jar
commons-codec-1.3.jar
commons-collections-3.1.jar
commons-digester-1.6.jar
commons-el-1.0.jar
commons-lang-2.1.jar
commons-logging-1.0.4.jar
jstl-1.1.0.jar
myfaces-api-1.1.2.jar
myfaces-impl-1.1.2.jar
JSF fait aussi massivement appel aux taglibs.
Par contre, contrairement à ce qui est dit dans le tutoriel Spring/portlet qui sert de base ce tutoriel il n'est pas nécessaire de déposer les tagligs dans le répertoire webpages/tags et l'on peut donc le supprimer ! De même on n'a pas besoin de déclarer le taglib dans le fichier web.xml.
Voilà qui est beaucoup plus simple, mais comment est-ce possible ?
En fait, si on prend l'exemple d'un taglib comme http://java.sun.com/jsf/core qui est présent dans myfaces-impl-1.1.2.jar, on se rend compte que le fichier descripteur de ce taglib, myfaces_core.tld, est inclus dans le répertoire META-INF du fichier jar. C'est suffisant pour que la déclaration du tablib soit "automatique".
le properties/applicationContext.xml est modifié :
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd"> <beans> <!-- Default View Resolver --> <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/jsp/"/> <property name="suffix" value=".jsp"/> </bean> <import resource="portlet-spring-domain.xml"/> <import resource="portlet-spring-dao.xml"/> </beans>
On ajoute ici l'import des fichiers portlet-spring-domain.xml et portlet-spring-dao.xml.
En effet, ces fichiers étaient chargés via le fichier portlet.xml qui utilisait le composant Spring org.springframework.web.portlet.DispatcherPortlet.
Comme nous l'utilisons plus Spring pour le MVC, ce n'est plus ce composant qui est référencé dans le portlet.xml mais nous avons toujours besoin de référencer les fichiers de configuration Spring pour les couches domain et dao.
Note
Je n'ai pas enlevé la partie relative au ViewResolver. Je ne l'ai pas testé mais je pense que cela peut permettre de conserver le mécanisme de traitement des erreurs présenté dans le tutoriel spring/portlet.
Le fichier web.xml est modifié :
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd"> <web-app> <!-- Nom de notre Portlet --> <display-name>JSF-tut</display-name> <context-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/classes/applicationContext.xml</param-value> </context-param> <context-param> <param-name>javax.faces.CONFIG_FILES</param-name> <param-value> /WEB-INF/classes/faces-config.xml </param-value> <description> Comma separated list of URIs of (additional) faces config files. (e.g. /WEB-INF/my-config.xml) See JSF 1.0 PRD2, 10.3.2 </description> </context-param> <context-param> <param-name>javax.faces.STATE_SAVING_METHOD</param-name> <param-value>client</param-value> <description> State saving method: "client" or "server" (= default) See JSF Specification 2.5.2 </description> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <listener> <listener-class>org.apache.myfaces.webapp.StartupServletContextListener</listener-class> </listener> <servlet> <servlet-name>Faces Servlet</servlet-name> <servlet-class>javax.faces.webapp.FacesServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>Faces Servlet</servlet-name> <url-pattern>*.faces</url-pattern> </servlet-mapping> <!-- Welcome files --> <welcome-file-list> <welcome-file>index.html</welcome-file> </welcome-file-list> <security-constraint> <display-name> Prevent access to raw JSP pages that are for JSF pages. </display-name> <web-resource-collection> <web-resource-name>Raw-JSF-JSP-Pages</web-resource-name> <url-pattern>*.jsp</url-pattern> </web-resource-collection> <auth-constraint> <description>No roles, so no direct access</description> </auth-constraint> </security-constraint> </web-app>
Qu'est-ce qui change :
Un paramètre donnant le nom du fichier de configuration JSF et un autre donnant le mode de stockage des états des composants graphiques JSF.
Un nouveau listener spécifique à JSF.
Un paramétrage "interdisant" l'accès direct aux .jsp
Note
Je n'ai pas traité le welcome-file. De plus, les fichiers .jsp sont déployés à la racine de l’application et plus dans le répertoire WEB-INF/jsf comme dans le tutoriel spring/portlet. Ceci nécessite une adaptation du fichier build.xml qui doit contenir
<property name="build.jsp" value="${build.home}" />en remplacement de
<property name="build.jsp" value="${build.web}/jsp" />
Le fichier portlet.xml est modifié :
<?xml version="1.0" encoding="UTF-8"?> <portlet-app xmlns="http://java.sun.com/xml/ns/portlet/portlet-app_1_0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/portlet/portlet-app_1_0.xsd http://java.sun.com/xml/ns/portlet/portlet-app_1_0.xsd" version="1.0"> <portlet> <portlet-name>spring</portlet-name> <portlet-class>org.apache.myfaces.portlet.MyFacesGenericPortlet</portlet-class> <init-param> <name>default-view</name> <value>/home.jsp</value> </init-param> <supports> <mime-type>text/html</mime-type> </supports> <portlet-info> <title>jsf</title> <short-title>Sample JSF MyFaces Portlet</short-title> <keywords>Sample JSF MyFaces Portlet</keywords> </portlet-info> </portlet> </portlet-app>
Qu'est-ce qui change :
On référence maintenant org.apache.myfaces.portlet.MyFacesGenericPortlet
On lui donne en paramètre la vue à utiliser par défaut.
Comme pour le tutoriel Spring portlet on prépare un fichier web-pluto.xml qui simplifie le déploiement :
... <servlet> <servlet-name>spring</servlet-name> <display-name>jsf Wrapper</display-name> <description>Automated generated Portlet Wrapper</description> <servlet-class>org.apache.pluto.core.PortletServlet</servlet-class> <init-param> <param-name>portlet-class</param-name> <param-value>org.apache.myfaces.portlet.MyFacesGenericPortlet</param-value> </init-param> <init-param> <param-name>portlet-guid</param-name> <param-value>esup-portlet-spring.spring</param-value> </init-param> </servlet> ... <servlet-mapping> <servlet-name>spring</servlet-name> <url-pattern>/spring/*</url-pattern> </servlet-mapping> ...
Nous allons maintenant enlever tous les éléments de la couche MVC Spring :
Suppression du package org.esupportail.portlet.spring.web
Suppression du classpath de la librairie spring-portlet.jar
Suppression de portlet-spring-web.xml
Suppression des pages jsp existantes dans webpages/stylesheets
Le fichier faces-config.xml est le point central de configuration de JSF
<?xml version='1.0' encoding='UTF-8'?> <!DOCTYPE faces-config PUBLIC "-//Sun Microsystems, Inc.//DTD JavaServer Faces Config 1.1//EN" "http://java.sun.com/dtd/web-facesconfig_1_1.dtd"> <faces-config> <application> <variable-resolver> org.springframework.web.jsf.DelegatingVariableResolver </variable-resolver> </application> <!-- NAVIGATION --> <navigation-rule> <from-view-id>/add-entry.jsp</from-view-id> <navigation-case> <from-outcome>home</from-outcome> <to-view-id>/home.jsp</to-view-id> </navigation-case> </navigation-rule> <navigation-rule> <from-view-id>/home.jsp</from-view-id> <navigation-case> <from-outcome>add</from-outcome> <to-view-id>/add-entry.jsp</to-view-id> </navigation-case> </navigation-rule> <!-- FORM BEANS --> <managed-bean> <managed-bean-name>addEntryBean</managed-bean-name> <managed-bean-class> org.esupportail.JSFTut.web.AddEntryBean </managed-bean-class> <managed-bean-scope>request</managed-bean-scope> <managed-property> <property-name>facadeService</property-name> <property-class>org.esupportail.JSFTut.web.FacadeService</property-class> <value>#{facadeService}</value> </managed-property> </managed-bean> <managed-bean> <managed-bean-name>homeBean</managed-bean-name> <managed-bean-class> org.esupportail.JSFTut.web.HomeBean </managed-bean-class> <managed-bean-scope>request</managed-bean-scope> <managed-property> <property-name>facadeService</property-name> <property-class>org.esupportail.JSFTut.web.FacadeService</property-class> <value>#{facadeService}</value> </managed-property> </managed-bean> <managed-bean> <managed-bean-name>navigationBean</managed-bean-name> <managed-bean-class> org.esupportail.JSFTut.web.NavigationBean </managed-bean-class> <managed-bean-scope>request</managed-bean-scope> </managed-bean> <!-- OTHERS --> <managed-bean> <managed-bean-name>facadeService</managed-bean-name> <managed-bean-class> org.esupportail.JSFTut.web.FacadeService </managed-bean-class> <managed-bean-scope>application</managed-bean-scope> <managed-property> <property-name>domainService</property-name> <property-class> org.esupportail.portlet.spring.domain.DomainService </property-class> <value>#{domainService}</value> </managed-property> </managed-bean> </faces-config>
Que remarque-t-on ?
Une section application qui définit un variable-resolver Spring : org.springframework.web.jsf.DelegatingVariableResolver. C'est le moyen offert par Spring pour s'interfacer avec JSF. JSF prévoit en effet la possibilité de définir des variables dans son fichier de configuration. Spring implémente le mécanisme de résolution de variables de JSF. Ainsi, dans ce fichier de configuration, la variable #{domainService} sera définie par spring. Nous verrons ensuite comment cela fonctionne concrètement.
Une section navigation-rule. JSF prévoit que les contrôleurs soient implémentés sous forme de simples beans. Ces classes java n’ont pas besoin d’implémenter une interface particulière ou d’étendre une classe JSF donnée. Il est juste dit qu’une méthode doit retourner une string. C’est cette string qui est définie dans la balise from-outcome et qui permet ainsi de diriger l’utilisateur vers la vue définie dans la balise to-view-id
Une section managed-bean. Nous verrons qu'un bean est associé à chaque vue. Ce dernier sert aussi bien au contrôleur qu'à stocker les données saisies dans le formulaire. Il est possible de différencier ces deux activités dans des beans différents mais ce n'est pas le cas dans ce tutoriel. La section managed-bean sert donc à définir ces beans. Les vues utiliserons le managed-bean-name qui est plus facile à manipuler que le managed-bean-class. Ici ces beans utilisent parfois une propriété facadeService dont la valeur est la variable #{facadeService}. Cette propriété est décrite ci-après.
En fin de fichier on trouve un managed-bean de nom facadeService qui correspond à la variable #{facadeService}. Ce bean permet d'injecter dans les contrôleurs JSF une façade d’un objet service géré par spring. En effet, dans la définition de facadeService on trouve une propriété de type org.esupportail.portlet.spring.domain.DomainService qui sera renseignée par la variable #{domainService}. Cette variable sera résolue par le variable-resolver spring. Et en effet, dans la configuration Spring portlet-spring-domain.xml, on trouve la définition suivante :
<bean id="domainService" class="org.esupportail.portlet.spring.domain.DomainServiceDao"> <property name="dataService"> <ref bean="dataService"/> </property> </bean>
Comme nous l'avons vu au paragraphe précédent, le lien entre Spring et JSF est fait dans le fichier faces-config.xml grâce à l'utilisation d'un variable-resolver spring. Une façade est utilisée afin de permettre l'injection par Spring d'une classe de la couche métier.
Voici le code source de cette façade (org.esupportail.JSFTut.web.FacadeService.java) :
package org.esupportail.JSFTut.web; import org.esupportail.portlet.spring.domain.DomainService; public class FacadeService { private DomainService domainService; public DomainService getDomainService() { return domainService; } public void setDomainService(DomainService domainService) { this.domainService = domainService; } }
L'avantage de passer par une façade et de ne pas injecter directement l'objet géré par Spring (DomainService) dans les contrôleurs c'est que seule la façade a besoin d'importer une classe de la couche métier.
Cette façade sera utilisable dans les contrôleurs pour interagir avec la couche métier. Exemple :
Entry entry = facadeService.getDomainService().getAddress(id); facadeService.getDomainService().deleteAddress(entry);
la homepage est constituée d'une vue (home.jsp) :
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f"%> <%@ taglib uri="http://java.sun.com/jsf/html" prefix="h"%> <f:loadBundle basename="messages" var="messages" /> <f:view> <body> <h:dataTable value="#{homeBean.addresses}" var="address" border="0" rowClasses="uportal-background-content,uportal-background-light"> <h:column> <f:facet name="header"> <h:outputText value="#{messages.lastName}" /> </f:facet> <h:outputText value="#{address.name}" /> </h:column> <h:column> <f:facet name="header"> <h:outputText value="#{messages.fistName}" /> </f:facet> <h:outputText value="#{address.firstName}" /> </h:column> <h:column> <f:facet name="header"> <h:outputText value="#{messages.email}" /> </f:facet> <h:outputText value="#{address.mail}" /> </h:column> <h:column> <h:form id="home"> <h:commandLink action="#{homeBean.del}"> <h:outputText value="#{messages.del}" /> <f:param name="id" value="#{address.id}" /> </h:commandLink> </h:form> </h:column> </h:dataTable> <h:form id="home2"> <h:commandLink action="#{navigationBean.add}"> <h:outputText value="#{messages.addPage}" /> </h:commandLink> </h:form> </body> </f:view>
Que constate-t-on ?
La définition de taglib JSF standard
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f"%> <%@ taglib uri="http://java.sun.com/jsf/html" prefix="h"%>Un des points forts de JSF est de pouvoir utiliser des composants "graphiques" beaucoup plus riches que ceux présentés dans ce tutoriel. Exemple : un arbre représentant une arborescence de fichiers. Pour cela, il suffit de définit le taglib qui lui correspond et le contrôleur JSF (lié à la vue pourra) prendre en compte les évènements liés à la manipulation de ce composant. De plus, si le composant nécessite du JavaScript ou de l'Ajax pour fonctionner, c'est totalement transparent pour les développeurs que nous sommes. Nous avons juste besoin de correctement paramétrer le taglib et n'avons pas à nous poser la question de comment il est rendu en html.
On charge un fichier de message afin d'externaliser les chaînes de caractères et rendre l'application I18N via
<f:loadBundle basename="messages" var="messages" />
Le fichier messages.properties est présent dans le répertoire properties :
fistName=Fisrt Name*: invalidEmail=Invalid email ! lastName=Last Name*: email=Email*: add=Add addPage=Add an address home=home del=del
Sa version française messages_fr.properties est aussi dans le répertoire properties :
fistName=Prénom* : invalidEmail=Email non valide ! lastName=Nom* : email=Email* : add=Ajouter... addPage=Ajouter une adresse del=Supprimer home=Retour à la page d'accueil
Dans la vue ces messages sont accessibles par des balises du type :
<h:outputText value="#{messages.email}" />
Une table h:dataTable dont le contenu est #{homeBean.addresses}. Cette liste d'adresses est obtenue dans une méthode du bean homeBean lié à la vue. Cette méthode fait appel à la couche métier pour obtenir ces adresses en utilisant la façade décrite au paragraphe précédent :
public List<Entry> getAddresses() { this.addresses = facadeService.getDomainService().getAddresses(); return addresses; }
Chaque entrée de cette liste est ensuite disponible dans la variable address
Les balises <h:column> et <f:facet name="header"> permettent de définir les colonnes et leur entête.
Pour chaque ligne on va avoir un lien qui permet de supprimer l'enregistrement courant :
<h:form id="home"> <h:commandLink action="#{homeBean.del}"> <h:outputText value="#{messages.del}" /> <f:param name="id" value="#{address.id}" /> </h:commandLink> </h:form>
A noter ici que l'on utilise des balises de type h:commandLink et pas du code html afin de garantir une portabilité en mode servlet ou portlet.
Enfin on trouve un lien vers la page qui permet d'ajouter une nouvelle entrée :
<h:form id="home2"> <h:commandLink action="#{navigationBean.add}"> <h:outputText value="#{messages.addPage}" /> </h:commandLink> </h:form>
Ici, à titre d'exemple, j'ai choisi d'utiliser un contrôleur différent du reste de la vue.
Deux beans (contrôleurs) sont liés à cette vue. HomeBean.java :
package org.esupportail.JSFTut.web; import java.util.ArrayList; import java.util.List; import java.util.Map; import javax.faces.context.FacesContext; import org.esupportail.portlet.spring.beans.Entry; public class HomeBean { private FacadeService facadeService; private List<Entry> addresses; public void setFacadeService(FacadeService facadeService) { this.facadeService = facadeService; } public List<Entry> getAddresses() { this.addresses = facadeService.getDomainService().getAddresses(); return addresses; } public void setAddresses(ArrayList<Entry> addresses) { this.addresses = addresses; } public String del() { FacesContext context = FacesContext.getCurrentInstance(); Map map = context.getExternalContext().getRequestParameterMap(); String id = (String)map.get("id"); Entry entry = facadeService.getDomainService().getAddress(id); facadeService.getDomainService().deleteAddress(entry); return("home"); } }
et NavigationBean.java :
package org.esupportail.JSFTut.web; public class NavigationBean { public String home() { return("home"); } public String add() { return("add"); } }
La page Add-entry est constituée d'une vue (add-entry.jsp) :
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f"%> <%@ taglib uri="http://java.sun.com/jsf/html" prefix="h"%> <%@ taglib uri="http://myfaces.apache.org/tomahawk" prefix="t"%> <f:loadBundle basename="messages" var="messages"/> <f:view> <body> <h:form id="addEntry"> <table border="0" cellspacing="0" cellpadding="1"> <tr> <td nowrap="true"><h:outputText value="#{messages.fistName}"/></td> <td nowrap="true"><h:inputText value="#{addEntryBean.firstName}" required="true" id="firstName"> <f:validateLength minimum="3" /> </h:inputText> <h:message for="firstName" styleClass="RED" /></td> </tr> <tr> <td class="portlet-font" nowrap="true"><h:outputText value="#{messages.lastName}"/></td> <td nowrap="true"><h:inputText value="#{addEntryBean.lastName}" required="true" id="lastName"> <f:validateLength minimum="3" /> </h:inputText> <h:message for="lastName" styleClass="RED" /></td> </tr> <tr> <td class="portlet-font" nowrap="true"><h:outputText value="#{messages.email}"/></td> <td nowrap="true"><h:inputText value="#{addEntryBean.email}" required="true" id="email"> <t:validateEmail/> </h:inputText> <t:message for="email" detailFormat="#{messages.invalidEmail}" styleClass="RED" /></td> </tr> </table> <br /> <h:commandButton value="#{messages.add}" action="#{addEntryBean.validate}" /> </h:form> <h:form id="add-entry2"> <h:commandLink action="#{navigationBean.home}"> <h:outputText value="#{messages.home}" /> </h:commandLink> </h:form> </body> </f:view>
Que constate-t-on de nouveau :
JSF permet de définir des validateurs sous forme de balises. Exemple :
<f:validateLength minimum="3" />
Il est possible de paramétrer le message qui sera affiché dans la vue si cette contrainte n'est pas respectée. Ce message est affiché via une balise du type :
<h:message for="lastName" styleClass="RED" />
Dans la spécification de base JSF il n'est pas prévu de validateur pour les emails. Par contre, JSF est extensible et il existe, comme pour les composants graphiques, de nombreux autres validateurs. Ici, par exemple nous ajoutons un nouveau taglib :
<%@ taglib uri="http://myfaces.apache.org/tomahawk" prefix="t"%>
Pour qu'il fonctionne nous avons besoin d'ajouter au projet les librairies tomahawk-1.1.3-SNAPSHOT.jar, commons-validator-1.3.0.jar et jakarta-oro-2.0.8.jar
Le bean contrôleur lié à cette vue est AddEntryBean.java :
package org.esupportail.JSFTut.web; import java.util.ArrayList; import java.util.List; import org.esupportail.portlet.spring.beans.Entry; public class AddEntryBean { private String firstName; private String lastName; private String email; private FacadeService facadeService; public void setFacadeService(FacadeService facadeService) { this.facadeService = facadeService; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String validate() { Entry entry = new Entry(); entry.setFirstName(this.lastName); entry.setMail(this.email); entry.setName(this.firstName); facadeService.getDomainService().addAddress(entry); return("home"); } }