Développement d'une Portlet avec Spring (Passage à JSF)

Ce document présente comment passer, sur la base du tutoriel "Portlet avec Spring", du MCV Spring à JSF.


Raymond  BOURGES 
Unuversité de Rennes 1

Dates de modification
Revision 1.0 26 mai 2006 Première version
Revision 1.0.1 26 juin 2006 Première relecture
1. Avertissements
1.1. id en int ou String ?
1.2. Qu'est-ce qui change ?
2. Configuration
2.1. Librairies tierces
2.2. Taglibs
2.3. Fichier de configuration global
3. Créer les descripteurs de déploiement
3.1. web.xml
3.2. portlet.xml
3.3. web-pluto.xml
4. Modification de la couche MVC
4.1. Suppression de la couche MCV Spring
4.2. Le fichier faces-config.xml
4.3. Lien JSF/Spring
4.4. Page Home
4.5. Page Add-entry

1. Avertissements

1.1. id en int ou String ?

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>

1.2. Qu'est-ce qui change ?

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.

2. Configuration

2.1. Librairies tierces

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 :

2.2. Taglibs

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".

2.3. Fichier de configuration global

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.

3. Créer les descripteurs de déploiement

3.1. web.xml

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 :

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" />

3.2. portlet.xml

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 :

3.3. web-pluto.xml

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>
...

4. Modification de la couche MVC

4.1. Suppression de la couche MCV Spring

Nous allons maintenant enlever tous les éléments de la couche MVC Spring :

4.2. Le fichier faces-config.xml

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 ?

4.3. Lien JSF/Spring

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);

4.4. Page Home

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 ?

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");
 }
}

4.5. Page Add-entry

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 :

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");
 }

}