Développement d'une Portlet avec Spring

Ce document présente dans ses grandes lignes le développement d'une Portlet à l'aide du framework Spring.


Mathieu  LARCHET 
Université Nancy 2

Dates de modification
Revision 2.4 24 août 2006 Passage en Spring 2.0-RC3
Revision 2.3 25 mai 2006 Couche d'accès aux données avec LdapTemplate
Revision 2.2 4 mai 2006 Corrections d'erreurs. Optimisations Ibatis (O. Ziller, C. Champmartin)
Revision 2.1 27 avril 2006 Ajout d'informations concernant le déploiement et la publication d'une portlet. Externalisation de la configuration.
Revision 2.0 23 mars 2006 Refonte de la documentation
Revision 1.2 22 mars 2006 Encore un peu de navigation + Couche d'accès aux données avec exemple Hiberbate 3 (Raymond BOURGES)
Revision 1.1 15 mars 2006 Formulaires
Revision 1.0 3 mars 2006 Portlet HelloWorld
1. Créer le projet sous Eclipse
2. Spring
2.1. Librairies Spring
2.2. Librairies tierces
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. HelloWorld avec Spring
4.1. Un début de configuration Spring
4.1.1. HandlerMode
4.1.2. Interceptor
4.1.3. ParameterHandler
4.1.4. Controller
4.2. Implémentation du contrôleur
4.3. Implémentation de la vue
4.4. Déploiement et publication
4.4.1. Déploiement
4.4.2. Publication dans uPortal
4.5. Un premier formulaire
5. Application de gestion d'un carnet d'adresses
5.1. Définition de la couche métier
5.1.1. Objects métiers
5.1.2. Service métier
5.1.3. Implémentation statique
5.1.4. Configuration Spring
5.2. Définition de la couche présentation
5.2.1. Page d'accueil
5.2.2. Suppression d'un enregistrement
5.2.3. Ajout d'un enregistrement
5.2.4. Modification d'un enregistrement
5.3. Définition de la couche d'accès aux données
5.3.1. Service de données
5.3.2. Service métier
5.3.3. Base de données
5.3.4. Ibatis
5.3.5. Hibernate
5.3.6. LDAP
5.4. Traitement des erreurs
5.4.1. Configuration Spring
5.4.2. Vues
5.5. Externalisation de la configuration
5.6. Internationalisation

Un corrigé de ce tutoriel est disponible ici.

Important

Cette correction ne peut fonctionner que si toutes les librairies correspondant aux choix d'implémentation sont présentes.

1. Créer le projet sous Eclipse

Commencez par créer un dossier dans lequel vous déposerez les fichiers build.xml et build.properties fournis dans la documentation sur le développement de Portlets. Configurez les correctement en n'oubliant pas de modifier le nom du projet dans le build.xml :

<project name="esup-portlet-spring" default="compile" basedir=".">
...

Créez un projet Java sous Eclipse pointant vers ce dossier, ne configurez rien pour le moment et validez. Ajoutez le fichier build.xml à la vue ANT et exécutez la tâche prepare. Rafraîchissez votre projet, vous remarquerez que tous les dossiers importants ont été créés.

Allez dans les propriétés du projet, configurez les sources afin qu'elles pointent vers le dossier source et le dossier de compilation vers build/WEB-INF/classes.

2. Spring

Nous allons maintenant ajouter à notre projet tout ce qui concerne Spring.

Commencez par télécharger la dernière version de Spring à l'adresse suivante : www.springframework.org.

Au moment où cette documentation est rédigée, il s'agit de la version 2.0-RC3 (les version 1.2.x n'intègrent pas la partie Portlets). Choisissez de préférence la version avec dépendances de façon à avoir sous la main toutes les librairies ne faisant pas partie de Spring mais indispensables par la suite.

2.1. Librairies Spring

Copiez dans le dossier lib de votre projet les fichiers suivants de la distribution Spring :

2.2. Librairies tierces

Copiez dans le dossier lib de votre projet les fichiers suivants :

2.3. Fichier de configuration global

Il est temps de créer notre premier fichier de configuration Spring. C'est celui destiné non pas à la Portlet mais à l'application web elle-même.

Créez un fichier properties/applicationContext.xml contenant les lignes suivantes :

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

</beans>

C'est le seul bean à configurer à ce niveau, il s'agit d'un ViewResolver c'est à dire qu'il va être chargé de faire correspondre un nom logique de 'vue' à une pages JSP. Le modèle utilisé ici se contente d'ajouter un préfixe et un suffixe au nom de la vue pour trouver la JSP correspondante. Ce bean doit être déclaré ici car on va lui connecter une servlet de rendu (c'est pourquoi il doit être chargé lors du chargement du contexte et non pas au chargement de la Portlet).

3. Créer les descripteurs de déploiement

Il y a deux descripteurs de déploiement obligatoires, web.xml décrivant l'application web Java, et portlet.xml décrivant la ou les Portlet(s) se trouvant dans cette application. Pour des questions pratiques, nous verrons également comment créer un fichier web.xml construit spécifiquement pour le conteneur de Portlets Pluto. Pendant la phase de développement, ce fichier nous permettra de déployer directement notre Portlet dans Tomcat sans passer par un fichier WAR et une tâche spécifique de déploiement.

3.1. web.xml

Le fichier web.xml doit être placé à la racine du répertoire webpages. Voici une version standard de ce fichier indépendante du moteur de Portlet qui sera utilisé :

<?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>Portlet Spring</display-name>

  <!-- Fichier de configuration Spring pour l'application web -->
  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/classes/applicationContext.xml</param-value>
  </context-param>

  <!-- Listener pour le lancement de Spring au démarrage du contexte -->
  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>

  <!-- Servlet de rendu -->
  <servlet>
    <servlet-name>ViewRendererServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.ViewRendererServlet</servlet-class>
  </servlet>

  <!-- Mapping du rendu -->
  <servlet-mapping>
    <servlet-name>ViewRendererServlet</servlet-name>
    <url-pattern>/WEB-INF/servlet/view</url-pattern>
  </servlet-mapping>

</web-app>

Expliquons un peu les différentes parties de ce fichier :

3.2. portlet.xml

Dans ce fichier se trouve la description de notre Portlet. C'est également ici que nous allons configurer le démarrage de la partie Spring spécifique à la Portlet (par opposition à celle déclarée dans le web.xml qui s'occupe elle de l'application web en général). Voici un fichier portlet.xml minimal pour commencer :

<?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.springframework.web.portlet.DispatcherPortlet</portlet-class>
    <init-param>
      <name>contextConfigLocation</name>
      <value>/WEB-INF/classes/portlet-spring-*.xml</value>
    </init-param>
    <supports>
      <mime-type>text/html</mime-type>
      <portlet-mode>view</portlet-mode>
    </supports>
    <portlet-info>
      <title>Spring</title>
    </portlet-info>      
  </portlet>
    
</portlet-app>

Expliquons un peu les différentes parties de ce fichier :

Nous verrons plus tard comment déclarer dans ce fichier des attributs du portail qui doivent être récupérés pour chaque utilisateur.

3.3. web-pluto.xml

Lors du déploiement d'une Portlet via un fichier WAR et la tâche ANT d'uPortal (deployPortletApp), le fichier web.xml est modifié de façon spécifique Pluto. Durant la phase de développement, il est particulièrement pénible de faire un WAR à chaque changement et de déployer notre Portlet de cette façon. C'est pourquoi on peut faire un fichier web-pluto.xml qui intègre les modifications de Pluto afin de déployer directement dans Tomcat.

Concrètement, il faut ajouter une servlet 'wrapper' pour chaque Portlet définie dans notre fichier portlet.xml et de lui fournir le mapping associé. En repartant du fichier web.xml standard, voici ce qu'il faut ajouter :

...
<servlet>
  <servlet-name>spring</servlet-name>
  <display-name>spring 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.springframework.web.portlet.DispatcherPortlet</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>
...

Expliquons un peu les différentes parties de ce fichier :

4. HelloWorld avec Spring

Nous allons désormais commencer à développer réellement notre Portlet, en lui faisant afficher pour commencer un simple Hello World ! Pour cela il va falloir réaliser plusieurs étapes :

4.1. Un début de configuration Spring

Cette Portlet va utiliser le MVC de Spring pour son fonctionnement. Il va être nécessaire de déclarer un certains nombre de beans Spring dans notre configuration afin de réaliser des opérations automatiques :

Commencez par créer un fichier portlet-spring-web.xml dans le dossier properties. Ce fichier va contenir toute la configuration Spring relative à la couche web de notre application. Il sera automatiquement chargé au démarrage de la Portlet puisque son nom respecte l'expression régulière décrite plus haut :

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">

<beans>
  
  <!-- Ici vont se trouver les définitions de nos beans -->

</beans>

4.1.1. HandlerMode

Ce bean Spring se charge d'aiguiller les requêtes en fonction du mode d'exécution de la portlet. C'est le Handler qui doit décider en premier, donc il faut lui donner la priorité la plus élevée de tous les Handlers. Pour l'instant notre Portlet n'utilise que le mode View :

<bean id="portletModeHandlerMapping" class="org.springframework.web.portlet.handler.PortletModeHandlerMapping">
  <property name="order" value="20"/>
  <property name="portletModeMap">
    <map>
      <entry key="view" value-ref="homeController" />
    </map>
  </property>
</bean>

Toutes les requêtes arrivant en mode view sont transmises au contrôleur 'homeController' à moins qu'un Handler de priorité plus basse n'en décide autrement.

4.1.2. Interceptor

Un interceptor va pouvoir aider un Handler à aiguiller une requête en fonctions de certains paramètres. Dans notre cas, nous allons utiliser un Interceptor fondant ses décisions sur un paramètre spécifique GET ou POST qui s'appelle par défaut 'action' (ça rappelle un peu le MAG pour ceux qui connaissent) :

<bean id="parameterMappingInterceptor" class="org.springframework.web.portlet.handler.ParameterMappingInterceptor"/>

4.1.3. ParameterHandler

Ce bean Spring est un Handler de priorité inférieure au précédent. Il va utiliser l'Interceptor déclaré précédemment de façon à effectuer un mapping entre une requête et un contrôleur en fonction de la valeur du paramètre 'action'. Lorsqu'un utilisateur arrive pour la première fois sur une Portlet, ce paramètre n'est pas défini, donc le Handler ne fera rien et c'est le contrôleur 'homeController' qui va traiter la requête. Toutefois on aura probablement besoin par la suite de revenir à la page d'accueil c'est pourquoi on va déclarer quand même un mapping entre la valeur 'home' du paramètre 'action' et notre homeController :

<bean id="portletModeParameterHandlerMapping" class="org.springframework.web.portlet.handler.PortletModeParameterHandlerMapping">
  <property name="order" value="10"/>
  <property name="interceptors">
    <list><ref bean="parameterMappingInterceptor"/></list>
  </property>
  <property name="portletModeParameterMap">
    <map>
      <entry key="view">
        <map>
          <entry key="home" value-ref="homeController" />
        </map>
      </entry>
    </map>
  </property>
</bean>

Voici la logique de notre Handler : si on est en mode 'view' et que l'Interceptor nous retourne une valeur 'home' alors c'est 'homeController' qui doit traiter la requête.

4.1.4. Controller

Il faut maintenant déclarer note premier contrôleur que nous devrons après implémenter :

<bean id="homeController" class="org.esupportail.portlet.spring.web.HomeController" />

Notez le package utilisé, à savoir que toutes nos classes Java relatives à la couche web de notre application se trouveront au même endroit.

4.2. Implémentation du contrôleur

Nous allons désormais implémenter notre fameux HomeController.

Pour ce faire commencez par créer le fichier org.esupportail.portlet.spring.web.HomeController.java dans le dosser source :

package org.esupportail.portlet.spring.web;

import javax.portlet.RenderRequest;
import javax.portlet.RenderResponse;

import org.springframework.web.portlet.ModelAndView;
import org.springframework.web.portlet.mvc.AbstractController;

public class HomeController extends AbstractController {

 /**
  * Traitement 'Render'
  * @param request RenderRequest la requête
  * @param response RenderResponse la réponse
  */
  protected ModelAndView handleRenderRequestInternal(RenderRequest request, RenderResponse response) throws Exception {
    return new ModelAndView("home", "message", "Hello World !");
  }
}

Quelques remarques :

4.3. Implémentation de la vue

Notre ViewResolver va résoudre la vue 'home' en '/WEB-INF/jsp/home.jsp'. Il suffit donc de créer le fichier 'home.jsp' dans le répertoire 'webpages/stylesheets' de notre projet. Mais de façon à avoir une bonne base, nous allons commencer par créer un fichier include.jsp qui se chargera de déclarer les différentes Taglibs, puis nous l'importerons dans toutes nos pages JSP.

webpages/stylesheets/include.jsp :

<%@ page contentType="text/html; charset=ISO-8859-1" isELIgnored="false" %>

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

<%@ taglib prefix="portlet" uri="http://java.sun.com/portlet" %>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>

<%@ taglib prefix="html" tagdir="/WEB-INF/tags/html" %>

webpages/stylesheets/home.jsp :

<%@ include file="/WEB-INF/jsp/include.jsp" %>

<span class="portlet-font"><c:out value="${message}" /></span>

Notons l'utilisation de la Taglib standard pour récupérer une valeur contenue dans le modèle.

4.4. Déploiement et publication

Nous allons décrire ici comment dans un premier temps déployer notre Portlet dans Tomcat puis nous verrons un exemple de publication avec uPortal.

4.4.1. Déploiement

Le déploiement de notre Portlet dans Tomcat se fait en utilisant la tâche ANT deploy à condition d'utiliser Pluto comme moteur et d'avoir écrit un fichier web-pluto.xml. Si ce n'est pas le cas, il faut faire appel à la tâche ANT buildwar et utiliser l'outil de déploiement spécifique à votre portail.

Une fois les fichiers déposés physiquement au bon endroit, il reste à avertir Tomcat de la présence d'une nouvelle application web. Il n'est pas conseillé de déployer dans le répertoire webapps par défaut de Tomcat en misant sur une configuration automatique car la plupart de vos applications nécessiteront une configuration avancée (comme la déclaration d'un pool de connexions par exemple).

Tomcat/conf/server.xml :

...
<Context path="/esup-portlet-spring" docBase="/home/portail/portlets-apps/esup-portlet-spring" crossContext="true" reloadable="false" />
...

Notons le mapping qui doit correspondre au nom de notre Portlet et le paramètre crossContext qui doit obligatoirement être à true (il l'est par défaut si ce n'est pas précisé).

4.4.2. Publication dans uPortal

Dernière étape, avertir le portail qu'il dispose d'une nouvelle application. Nous verrons ici le cas d'uPortal car cette étape est spécifique au portail utilisé. La publication est similaire à celle d'un canal, on peut la réaliser à l'aide de l'interface graphique ou à l'aide d'un fichier pubchan dont voici un exemple type.

esup-spring-pubchan.xml :

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE channel-definition SYSTEM "channelDefinition.dtd">

<channel-definition>

    <title>Portlet Spring</title>
    <name>Portlet Spring</name>
    <fname>esup-portlet-spring</fname>
    <desc>Un carnet d'adresses développé sous forme d'une Portlet Spring</desc>
    <type>Portlet</type>
    <class>org.jasig.portal.channels.portlet.CPortletAdapter</class>
    <timeout>10000</timeout>
    
    <hasedit>N</hasedit>
    <hashelp>N</hashelp>
    <hasabout>N</hasabout>

    <secure>N</secure>
    <locale>en_US</locale>
    
    <categories>
        <category>Development</category>
    </categories>
    
    <groups>
        <group>Everyone</group>
    </groups>
    
    <parameters>

        <parameter>
            <name>portletDefinitionId</name>
            <value>esup-portlet-spring.spring</value>
            <description>Identifies the portlet deployed within the portlet container</description>
            <ovrd>N</ovrd>
        </parameter>

    </parameters>
    
</channel-definition>

Voici le résultat d'exécution de notre première Portlet Spring :

4.5. Un premier formulaire

Nous allons maintenant modifier notre application de façon à traiter un petit formulaire dans lequel l'utilisateur saisira son prénom.

Il va falloir modifier notre page home.jsp pour y inclure un formulaire ainsi que notre contrôleur qui devra gérer maintenant deux choses :

Voici la nouvelle version de notre fichier home.jsp :

<%@ include file="/WEB-INF/jsp/include.jsp" %>

<form method="post" action="<portlet:actionURL><portlet:param name="action" value="home" /></portlet:actionURL>">
  <span class="portlet-font">Entrez votre prénom : 
    <input class="portlet-form-input-field" type="text" name="name" />
    <input class="portlet-form-button" type="submit" value="OK" />
  </span>
</form>

<span class="portlet-font">Hello <c:out value="${message}" /> !</span>

On notera l'utilisation de la Taglib Spring pour générer l'URL utilisée pour le formulaire ainsi que le paramètre 'action' qui est ici positionné de façon à ce que le handler envoie la requête au HomeController.

Voici la nouvelle version de notre fichier HomeController.java :

package org.esupportail.portlet.spring.web;

import javax.portlet.ActionRequest;
import javax.portlet.ActionResponse;
import javax.portlet.RenderRequest;
import javax.portlet.RenderResponse;

import org.springframework.web.portlet.ModelAndView;
import org.springframework.web.portlet.mvc.AbstractController;

public class HomeController extends AbstractController {

  /**
   * Traitement 'Action'
   * @param request ActionRequest la requête
   * @param response ActionResponse la réponse
   */
  protected void handleActionRequestInternal(ActionRequest request, ActionResponse response) throws Exception {
    String name = request.getParameter("name");
    if(name != null && !"".equals(name)) {
      response.setRenderParameter("name", name);
    }
  }

  /**
   * Traitement 'Render'
   * @param request RenderRequest la requête
   * @param response RenderResponse la réponse
   */
  protected ModelAndView handleRenderRequestInternal(RenderRequest request, RenderResponse response) throws Exception {
    String name = request.getParameter("name");
    if(name == null) {
      name = "World";
    }
    return new ModelAndView("home", "message", name);
  }
}

Quelques explications sur le code que nous avons écrit :

La méthode handleActionRequestInternal réceptionne les requêtes de type 'action'. C'est donc elle qui traite notre formulaire. Elle se contente de récupérer le paramètre 'name' tout comme le ferait une servlet et de le passer en paramètre au traitement du rendu si il est différent de null et non vide.

La méthode handleRenderRequestInternal réceptionne les requêtes de type 'render'. Elle récupère un éventuel paramètre transmis par une requête 'action' et l'incorpore au modèle pour affichage.

Voici le résultat :

5. Application de gestion d'un carnet d'adresses

Cette section se propose de développer une application complète de gestion d'un carnet d'adresse. Elle couvrira tous les aspects du développement, conception des pages web, implémentation des objets métiers, implémentation de la couche d'accès aux données, utilisation d'outils tiers pour la persistance, externalisation de la configuration, gestion des erreurs.

5.1. Définition de la couche métier

La couche métier d'une application représente le coeur de celle-ci, à savoir la manipulation d'objets directement en relation avec la problématique traitée. Dans le cas d'un carnet d'adresse, la couche métier devra probablement manipuler des adresses. Elle offre ses services à la couche supérieure (la couche présentation) en masquant la complexité des mécanismes mis en oeuvre. Elle utilise souvent une couche d'accès aux données permettant de mettre en oeuvre un mécanisme de persistance, mais ne doit pas s'inquiéter de la façon dont celui-ci est mis en oeuvre.

5.1.1. Objects métiers

Les objets métiers sont des classes Java encapsulant des informations qui vont souvent transiter dans toute notre application. Ils sont créés par la couche d'accès aux données, manipulés par la couche métier et affichés par la couche présentation. Généralement il s'agit de POJO (Plain Old Java Object) c'est-à-dire de simples objets Java avec des propriétés accessibles en lecture/écriture. Dans notre application de gestion d'adresses, nous allons créer un objet qui renfermera les informations d'une entrée de ce carnet, que nous appellerons Entry. Ces objets sont souvent placés dans un package séparé.

org.esupportail.portlet.spring.beans.Entry :

package org.esupportail.portlet.spring.beans;

public class Entry {

  private String id;
  private String name;
  private String firstName;
  private String mail;

  /**
   * Constructeur
   */
  public Entry() {
  }

  /**
   * Retourne l'id
   * @return
   */
  public String getId() {
    return id;
  }

  /**
   * Positionne l'id
   * @param id
   */
  public void setId(String id) {
    this.id = id;
  }

  /**
   * Retourne le nom
   * @return
   */
  public String getName() {
    return name;
  }

  /**
   * Positionne le nom
   * @param name
   */
  public void setName(String name) {
    this.name = name;
  }

  /**
   * Retourne le prénom
   * @return
   */
  public String getFirstName() {
    return firstName;
  }

  /**
   * Positionne le prénom
   * @param firstName
   */
  public void setFirstName(String firstName) {
    this.firstName = firstName;
  }

  /**
   * Retourne l'email
   * @return
   */
  public String getMail() {
    return mail;
  }

  /**
   * Positionne l'email
   * @param mail
   */
  public void setMail(String mail) {
    this.mail = mail;
  }
}

Notre entrée de carnet d'adresse comprend quatre informations, le nom, le prénom, l'adresse mail et un identifiant unique. Il sera très souvent obligatoire d'avoir un identifiant pour chaque objet métier.

5.1.2. Service métier

Comme son nom l'indique, il s'agit d'un objet qui va proposer des services. Dans notre cas le service métier expose ses capacités à la couche supérieure, la couche présentation. Cependant, il va être possible d'implémenter ces services de différentes façons, sans que la couche présentation soit perturbée par d'éventuels changements. C'est pourquoi on va définir une interface qui contiendra la liste des services dont la couche présentation aura besoin. Les classes concernant la couche métier sont elles aussi placées dans un package particulier.

org.esupportail.portlet.spring.domain.DomainService :

package org.esupportail.portlet.spring.domain;

import java.util.List;

import org.esupportail.portlet.spring.beans.Entry;

public interface DomainService {

  public List<Entry> getAddresses();

  public Entry getAddress(String id);

  public void addAddress(Entry entry);

  public void updateAddress(Entry entry);

  public void deleteAddress(Entry entry);
}

5.1.3. Implémentation statique

Dans un premier temps, nous allons réaliser une implémentation de cette couche métier qui sera statique, c'est-à dire qui ne fera pas appel à une couche d'accès aux données. Cette implémentation servira de base pour développer notre couche présentation afin d'avoir quelque chose de fonctionnel très rapidement.

Important

Cette implémentation statique a comme particularité d'être vulnérable à des traitements multithreadés, elle n'est pas du tout destinée à un fonctionnement en production avec plusieurs utilisateurs concurrents accédant à l'application.

org.esupportail.portlet.spring.domain.DomainServiceStatic :

package org.esupportail.portlet.spring.domain;

import java.util.Iterator;
import java.util.List;

import org.esupportail.portlet.spring.beans.Entry;

public class DomainServiceStatic implements DomainService {

  private List<Entry> addresses;

  /**
   * Constructeur
   */
  public DomainServiceStatic() {
  }

  /**
   * Positionne la liste des adresses
   * @param addresses
   */
  public void setAddresses(List<Entry> addresses) {
    this.addresses = addresses;
  }

  /**
   * Retourne la liste des adresses
   * @return
   */
  public List<Entry> getAddresses() {
    return addresses;
  }

  /**
   * Retourne une adresse
   * @param id
   * @return
   */
  public Entry getAddress(String id) {
    for(Entry e : addresses) {
      if(e.getId().equals(id)) {
        return e;
      }
    }
    return null;
  }

  /**
   * Ajoute une adresse
   * @param entry
   */
  public void addAddress(Entry entry) {
    addresses.add(entry);
  }

  /**
   * Met à jour une adresse
   * @param entry
   */
  public void updateAddress(Entry entry) {
  }

  /**
   * Supprime une adresse
   * @param id
   */
  public void deleteAddress(Entry entry) {
    Iterator<Entry> iterator = addresses.iterator();
    while (iterator.hasNext()) {
      Entry element = iterator.next();
      if (element.getId() == entry.getId()) {
        iterator.remove();
      }
    }
  }
}

5.1.4. Configuration Spring

Nous allons maintenant déclarer notre service métier dans la configuration Spring, et lui injecter des informations statiques pour simuler son fonctionnement.

properties/portlet-spring-domain.xml :

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">

<beans>
    
    <bean id="address1" class="org.esupportail.portlet.spring.beans.Entry">
        <property name="id" value="1" />
        <property name="name" value="Larchet" />
        <property name="firstName" value="Mathieu" />
        <property name="mail" value="Mathieu.Larchet@univ.fr" />
    </bean>
    
    <bean id="address2" class="org.esupportail.portlet.spring.beans.Entry">
        <property name="id" value="2" />
        <property name="name" value="Dupont" />
        <property name="firstName" value="Jean" />
        <property name="mail" value="Jean.Dupont@univ.fr" />
    </bean>
    <bean id="address3" class="org.esupportail.portlet.spring.beans.Entry">
        <property name="id" value="3" />
        <property name="name" value="Martin" />
        <property name="firstName" value="Jacques" />
        <property name="mail" value="Jacques.Martin@univ.fr" />
    </bean>     
    
    <bean id="domainService" class="org.esupportail.portlet.spring.domain.DomainServiceStaticImpl">
        <property name="addresses">
            <list>
                <ref bean="address1" />
                <ref bean="address2" />
                <ref bean="address3" />
            </list>
        </property>
    </bean>
    
</beans>

5.2. Définition de la couche présentation

La couche présentation a pour vocation d'interagir avec les utilisateurs. Elle utilise les services de la couche métier et est spécifique au type d'application. Dans notre cas, il s'agit d'une application web implémentée sous la forme d'une Portlet.

5.2.1. Page d'accueil

Nous allons commencer par modifier le fonctionnement de notre contrôleur chargé d'afficher la page de démarrage. Trois fichiers sont à modifier, le contrôleur, la vue et le fichier de configuration Spring.

5.2.1.1. Contrôleur

Notre contrôleur va désormais utiliser les fonctionnalités de la couche métier pour récupérer la liste des adresses.

org.esupportail.portlet.spring.web.HomeController :

package org.esupportail.portlet.spring.web;

import javax.portlet.RenderRequest;
import javax.portlet.RenderResponse;

import org.esupportail.portlet.spring.domain.DomainService;
import org.springframework.web.portlet.ModelAndView;
import org.springframework.web.portlet.mvc.AbstractController;

public class HomeController extends AbstractController {

  private DomainService domainService;

  /**
   * Positionne le service métier 
   * @param domainService
   */
  public void setDomainService(DomainService domainService) {
    this.domainService = domainService;
  }

  /**
   * Traitement 'Render'
   * @param request RenderRequest la requête
   * @param response RenderResponse la réponse
   */
  protected ModelAndView handleRenderRequestInternal(RenderRequest request, RenderResponse response) throws Exception {
    return new ModelAndView("home", "addresses", domainService.getAddresses());
  }
}

5.2.1.2. Vue

Notre vue va désormais récupérer dans le modèle la liste des adresses, les parcourir et les afficher.

webpages/stylesheets/home.jsp :

<%@ include file="/WEB-INF/jsp/include.jsp" %>

<table border="0" cellpadding="2" cellspacing="0">
  <c:forEach items="${addresses}" var="entry">
    <tr>
      <td class="portlet-font">Nom :</td>
      <td class="portlet-font"><c:out value="${entry.name}" />
    </tr>
    <tr>
      <td class="portlet-font">Prénom :</td>
      <td class="portlet-font"><c:out value="${entry.firstName}" />
    </tr>
    <tr>
      <td class="portlet-font">Adresse :</td>
      <td class="portlet-font"><c:out value="${entry.mail}" />
    </tr>
  </c:forEach>
</table>

5.2.1.3. Configuration Spring

Nous allons modifier la définition de notre contrôleur afin de lui injecter le service de la couche métier.

properties/portlet-spring-web.xml :

...
<bean id="homeController" class="org.esupportail.portlet.spring.web.HomeController">
  <property name="domainService" ref="domainService" />
</bean>
...

Voici le résultat :

5.2.2. Suppression d'un enregistrement

Nous allons ajouter une nouvelle fonctionnalité à notre application, la possibilité de supprimer une adresse. Pour ce faire il va falloir réaliser trois étapes :

5.2.2.1. Vue

Nous allons simplement ajouter à notre vue courante un lien de type permettant de supprimer un enregistrement, en passant en paramètre son identifiant.

webpages/stylesheets/home.jsp :

<%@ include file="/WEB-INF/jsp/include.jsp" %>

<table border="0" cellpadding="2" cellspacing="0">
  <c:forEach items="${addresses}" var="entry">
    <tr>
      <td class="portlet-font">Nom :</td>
      <td class="portlet-font"><c:out value="${entry.name}" />
    </tr>
    <tr>
      <td class="portlet-font">Prénom :</td>
      <td class="portlet-font"><c:out value="${entry.firstName}" />
    </tr>
    <tr>
      <td class="portlet-font">Adresse :</td>
      <td class="portlet-font"><c:out value="${entry.mail}" />
    </tr>
    <tr>
      <td>
        <a href="<portlet:actionURL>
                   <portlet:param name="action" value="delEntry" />
                   <portlet:param name="id" value="${entry.id}" />
                 </portlet:actionURL>">Supprimer</a>
      </td>
      <td>&nbsp;</td>
    </tr>
  </c:forEach>
</table>

Notons que nous utilisons ici une URL de type action, nous verrons par la suite pourquoi.

5.2.2.2. Contrôleur

Il faut maintenant créer un nouveau contrôleur qui se chargera de la suppression de l'enregistrement. Aucun affichage ne sera effectué, il suffira de supprimer l'entrée et de laisser le soin au HomeController de mettre à jour l'affichage. D'où l'utilisation d'une requête de type action.

org.esupportail.portlet.spring.web.DelEntryController :

package org.esupportail.portlet.spring.web;

import javax.portlet.ActionRequest;
import javax.portlet.ActionResponse;

import org.esupportail.portlet.spring.beans.Entry;
import org.esupportail.portlet.spring.domain.DomainService;
import org.springframework.web.portlet.mvc.AbstractController;

public class DelEntryController extends AbstractController {

  private DomainService domainService;

  /**
   * Positionne le service métier 
   * @param domainService
   */
  public void setDomainService(DomainService domainService) {
    this.domainService = domainService;
  }

  /**
   * Traitement 'Action'
   * @param request ActionRequest la requête
   * @param response ActionResponse la réponse
   */
  protected void handleActionRequestInternal(ActionRequest request, ActionResponse response) throws Exception {
    String id = request.getParameter("id");

    Entry entry = domainService.getAddress(id);
    domainService.deleteAddress(entry);
    response.setRenderParameter("action", "home");
  }
}

On voit ici que le traitement d'une requête de type action ne génère aucun affichage, la méthode handleActionRequestInternal ne retourne pas d'objet ModelAndView. Par contre on positionne sur l'objet response un paramètre de rendu action à la valeur home. Ce paramètre sera incorporé à la requête de type render qui suivra le traitement ce qui permettra de l'aiguiller automatiquement vers notre HomeController.

5.2.2.3. Configuration Spring

Il ne nous reste plus qu'à déclarer notre contrôleur et à placer le bon mapping dans la configuration Spring.

properties/portlet-spring-web.xml :

...
<bean id="delEntryController" class="org.esupportail.portlet.spring.web.DelEntryController">
  <property name="domainService" ref="domainService" />
</bean>
...
<bean id="portletModeParameterHandlerMapping" class="org.springframework.web.portlet.handler.PortletModeParameterHandlerMapping">
  <property name="order" value="10"/>
  <property name="interceptors">
    <list><ref bean="parameterMappingInterceptor"/></list>
  </property>
  <property name="portletModeParameterMap">
    <map>
      <entry key="view">
        <map>
          <entry key="home" value-ref="homeController" />
          <entry key="delEntry" value-ref="delEntryController" />
        </map>
      </entry>
    </map>
  </property>
</bean>

5.2.3. Ajout d'un enregistrement

Nous allons ajouter une nouvelle fonctionnalité à notre application, la possibilité d'ajouter une adresse. Pour ce faire il va falloir réaliser plusieurs étapes :

5.2.3.1. Vue

Nous allons ajouter à notre page d'accueil un lien permettant d'accéder au formulaire de saisie. Ce sera un lien de type render vu qu'aucun traitement n'est associé à cette opération, il s'agit juste d'afficher une page.

webpages/stylesheets/home.jsp :

<%@ include file="/WEB-INF/jsp/include.jsp" %>

<table border="0" cellpadding="2" cellspacing="0">
  <c:forEach items="${addresses}" var="entry">
    <tr>
      <td class="portlet-font">Nom :</td>
      <td class="portlet-font"><c:out value="${entry.name}" />
    </tr>
    <tr>
      <td class="portlet-font">Prénom :</td>
      <td class="portlet-font"><c:out value="${entry.firstName}" />
    </tr>
    <tr>
      <td class="portlet-font">Adresse :</td>
      <td class="portlet-font"><c:out value="${entry.mail}" />
    </tr>
    <tr>
      <td>
        <a href="<portlet:actionURL>
                   <portlet:param name="action" value="delEntry" />
                   <portlet:param name="id" value="${entry.id}" />
                 </portlet:actionURL>">Supprimer</a>
      </td>
      <td>&nbsp;</td>
    </tr>
  </c:forEach>
</table>

<a href="<portlet:renderURL><portlet:param name="action" value="addEntry" /></portlet:renderURL>">Ajouter une personne</a>

5.2.3.2. Contrôleur

Nous allons utiliser un contrôleur un peu spécial pour la gestion du formulaire. En effet, si nous utilisions les méthodes classiques mises à notre disposition il faudrait réaliser de nombreuses opérations :

Heureusement, Spring propose des contrôleurs beaucoup plus évolués qui vont se charger d'une bonne partie de ce travail fastidieux.

org.esupportail.portlet.spring.web.AddEntryController :

package org.esupportail.portlet.spring.web;

import javax.portlet.ActionRequest;
import javax.portlet.ActionResponse;
import javax.portlet.PortletRequest;

import org.esupportail.portlet.spring.beans.Entry;
import org.esupportail.portlet.spring.domain.DomainService;
import org.springframework.validation.BindException;
import org.springframework.web.portlet.mvc.SimpleFormController;

public class AddEntryController extends SimpleFormController {

  private DomainService domainService;

  /**
   * Positionne le service métier
   * @param domainService
   */
  public void setDomainService(DomainService domainService) {
    this.domainService = domainService;
  }

  /**
   * Traite le formulaire une fois validé
   * @param request
   * @param response
   * @param o
   * @param e
   */
  protected void onSubmitAction(ActionRequest request, ActionResponse response, Object o, BindException e) throws Exception {
    Entry entry = (Entry)o;
    domainService.addAddress(entry);
    response.setRenderParameter("action", "home");
  }

  /**
   * Instancie l'objet sur lequel le formulaire sera mappé
   * @param request
   * @return
   */
  protected Object formBackingObject(PortletRequest request) throws Exception {
    return new Entry();
  }
}

Détaillons un peu le fonctionnement de ce contrôleur. A première vue, tout ça semble très léger, c'est parce que la complexité est masquée par Spring. Nous avons utilisé un SimpleFormController qui permet de gérer des formulaires simples. De façon habituelle, nous lui injecterons par configuration un objet DomainService. Un élément important est la notion d'objet de commande. Il s'agit d'un objet associé au formulaire qui est automatiquement rempli par mapping. La méthode formBackingObject est chargée de créer cet objet. Dans notre cas, il s'agit d'un objet de type Entry vide. La méthode onSubmitAction est appelée une fois que l'utilisateur a validé le formulaire et qu'une classe spécifique appelée validator a contrôlé les différentes valeurs et donné son accord. C'est pourquoi on se contente d'ajouter l'adresse à notre carnet et de retourner vers la page d'accueil. La définition de la vue servant à afficher le formulaire et du validator se configure directement dans la définition Spring de notre contrôleur.

5.2.3.3. Validator

Il s'agit d'une classe qui va contrôler la saisie de l'utilisateur. Si certains champs sont mal positionnés, elle aura pour charge de générer des messages d'erreur explicites. Ce sera au contrôleur de réafficher le formulaire en y incluant les différentes erreurs rencontrées.

org.esupportail.portlet.spring.web.AddEntryValidator :

package org.esupportail.portlet.spring.web;

import org.esupportail.portlet.spring.beans.Entry;
import org.esupportail.portlet.spring.domain.DomainService;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;

public class AddEntryValidator implements Validator {

  private DomainService domainService;

  /**
   * Positionne le service métier
   * @param domainService
   */
  public void setDomainService(DomainService domainService) {
    this.domainService = domainService;
  }

  /**
   * Indique le type d'objet supporté par ce validator
   * @param clazz
   * @return
   */
  public boolean supports(Class clazz) {
    return clazz.equals(Entry.class);
  }

  /**
   * Validation d'une entrée
   * @param 
   */
  public void validate(Object o, Errors errors) {
    Entry e = (Entry)o;
    validateId(e.getId(), errors);
    validateName(e.getName(), errors);
    validateFirstName(e.getFirstName(), errors);
    validateMail(e.getMail(), errors);
  }

  /**
   * Valide l'identifiant
   * @param id
   * @param errors
   */
  private void validateId(String id, Errors errors) {
    ValidationUtils.rejectIfEmpty(errors, "id", "id.required", "L'identifiant est obligatoire");
    if(!id.equals("")) {
      if(!id.matches("^[0-9]+$")) {
        errors.rejectValue("id", "id.invalid", "L'identifiant doit \u00EAtre num\u00E9rique");
      }
      else {
        if(domainService.getAddress(id) != null) {
          errors.rejectValue("id", "id.invalid", "Cet identifiant est d\u00E9j\u00E0 utilis\u00E9");
        }
      }
    }
  }

  /**
   * Valide le nom
   * @param name
   * @param errors
   */
  private void validateName(String name, Errors errors) {
    ValidationUtils.rejectIfEmpty(errors, "name", "name.required", "Le nom est obligatoire");
  }

  /**
   * Valide le prénom
   * @param firstName
   * @param errors
   */
  private void validateFirstName(String firstName, Errors errors) {
    ValidationUtils.rejectIfEmpty(errors, "firstName", "firstName.required", "Le prénom est obligatoire");
  }

  /**
   * Valide le mail
   * @param mail
   * @param errors
   */
  private void validateMail(String mail, Errors errors) {
    ValidationUtils.rejectIfEmpty(errors, "mail", "mail.required", "L'adresse est obligatoire");
    if(!mail.equals("")) {
      if(!mail.matches("^\\w[_\\-\\.\\w]*\\@\\w[_\\-\\.\\w]*\\.(\\w){2,3}$")) {
        errors.rejectValue("mail", "mail.invalid", "Adresse invalide");
      }
    }
  }
}

La méthode supports indique si ce validator s'applique au type d'objet rencontré. La méthode validate se charge de valider les différents champs. Dans notre cas, nous nous assurons que tous les paramètres ont bien été saisis, que l'identifiant est numérique et n'est pas encore utilisé et que l'adresse mail respecte un format habituel.

5.2.3.4. Vue du formulaire

Nous allons maintenant écrire la page JSP chargée d'afficher le formulaire.

webpages/stylesheets/add-entry.jsp :

<%@ include file="/WEB-INF/jsp/include.jsp" %>

<spring:hasBindErrors name="entry">
  <p>Merci de corriger les erreurs suivantes :</p>
  <ul>
    <spring:bind path="entry.*">
      <c:forEach items="${status.errorMessages}" var="error">
        <li><c:out value="${error}"/></li>
      </c:forEach>
    </spring:bind>
  </ul>
</spring:hasBindErrors>

<form method="post" action="<portlet:actionURL><portlet:param name="action" value="addEntry" /></portlet:actionURL>">
  <table border="0" cellspacing="0" cellpadding="1">
    <tr>
      <td class="portlet-font" nowrap="true">Identifiant :</td>
      <td nowrap="true">
        <spring:bind path="entry.id">
          <input class="portlet-form-input-field" type="text" name="${status.expression}" value="${status.value}" />
        </spring:bind>
      </td>
      <td class="portlet-section-text">(Obligatoire)</td>
    </tr>
    <tr>
      <td class="portlet-font" nowrap="true">Nom :</td>
      <td nowrap="true">
        <spring:bind path="entry.name">
          <input class="portlet-form-input-field" type="text" name="${status.expression}" value="${status.value}" />
        </spring:bind>
      <td class="portlet-section-text">(Obligatoire)</td>
    </tr>
    <tr>
      <td class="portlet-font" nowrap="true">Prénom :</td>
      <td nowrap="true">
        <spring:bind path="entry.firstName">
          <input class="portlet-form-input-field" type="text" name="${status.expression}" value="${status.value}" />
        </spring:bind>
      <td class="portlet-section-text">(Obligatoire)</td>
    </tr>
    <tr>
      <td class="portlet-font" nowrap="true">Mail :</td>
      <td nowrap="true">
        <spring:bind path="entry.mail">
          <input class="portlet-form-input-field" type="text" name="${status.expression}" value="${status.value}" />
        </spring:bind>
      <td class="portlet-section-text">(Obligatoire)</td>
    </tr>
  </table>
  <br/>
  <button class="portlet-form-button" type="submit">Ajouter</button>
</form>
<a href="<portlet:renderURL><portlet:param name="action" value="home" /></portlet:renderURL>">Revenir à la page d'accueil</a>

On remarquera l'utilisation de la TagLib Spring permettant de lier un élément du formulaire à une propriété de notre objet de commande. C'est également grâce à elle que l'on peut afficher à côté de chaque champ les erreurs qui lui sont associées.

5.2.3.5. Configuration Spring

Nous avons trois beans différents à configurer / modifier. Tout d'abord, déclarons notre validator.

properties/portlet-spring-web.xml :

...
<bean id="addEntryValidator" class="org.esupportail.portlet.spring.web.AddEntryValidator">
  <property name="domainService" ref="domainService" />
</bean>
...

Rien de bien nouveau dans ce bean tout ce qu'il y a de plus simple.

Définissons maintenant notre nouveau contrôleur un peu plus complexe que les précédents.

properties/portlet-spring-web.xml :

...
<bean id="addEntryController" class="org.esupportail.portlet.spring.web.AddEntryController">
  <property name="domainService" ref="domainService" />
  <property name="commandName" value="entry" />
  <property name="commandClass" value="org.esupportail.portlet.spring.beans.Entry" />
  <property name="formView" value="add-entry" />
  <property name="validator" ref="addEntryValidator" />
</bean>
...

On commence par lui injecter une référence au bean domainService. On définit ensuite le nom de notre objet de commande, ce qui permettra d'y accéder dans notre vue sous le nom entry. Ensuite, on définit la classe Java de notre objet de commande, afin que Spring puisse déterminer les mappings à réaliser avec les objets du formulaire. On donne le nom de notre vue chargée d'afficher le formulaire et enfin on indique une référence vers le validator chargé de la vérification de saisie.

Enfin on ajoute un nouveau mapping pour notre contrôleur.

properties/portlet-spring-web.xml :

...
<bean id="portletModeParameterHandlerMapping" class="org.springframework.web.portlet.handler.PortletModeParameterHandlerMapping">
  <property name="order" value="10"/>
  <property name="interceptors">
    <list><ref bean="parameterMappingInterceptor"/></list>
  </property>
  <property name="portletModeParameterMap">
    <map>
      <entry key="view">
        <map>
          <entry key="home" value-ref="homeController" />
          <entry key="addEntry" value-ref="addEntryController" />
          <entry key="delEntry" value-ref="delEntryController" />
        </map>
      </entry>
    </map>
  </property>
</bean>
...

Voici le résultat :

5.2.4. Modification d'un enregistrement

La modification d'un enregistrement suit exactement les mêmes règles que la création d'un nouvel enregistrement. La seule différence est qu'au lieu de récupérer un objet de commande vide pour notre formulaire, nous allons récupérer un objet existant auprès de notre service métier. Voici rapidement les différentes modifications à apporter à notre application.

5.2.4.1. Vue

Sur la page d'accueil, nous allons ajouter en plus du lien supprimer un lien modifier.

webpages/stylesheets/home.jsp :

<%@ include file="/WEB-INF/jsp/include.jsp" %>

<table border="0" cellpadding="2" cellspacing="0">
  <c:forEach items="${addresses}" var="entry">
    <tr>
      <td class="portlet-font">Nom :</td>
      <td class="portlet-font"><c:out value="${entry.name}" />
    </tr>
    <tr>
      <td class="portlet-font">Prénom :</td>
      <td class="portlet-font"><c:out value="${entry.firstName}" />
    </tr>
    <tr>
      <td class="portlet-font">Adresse :</td>
      <td class="portlet-font"><c:out value="${entry.mail}" />
    </tr>
    <tr>
      <td>
        <a href="<portlet:renderURL>
                   <portlet:param name="action" value="updateEntry" />
                   <portlet:param name="id" value="${entry.id}" />
                 </portlet:renderURL>">Modifier</a>
      </td>
      <td>
        <a href="<portlet:actionURL>
                   <portlet:param name="action" value="delEntry" />
                   <portlet:param name="id" value="${entry.id}" />
                 </portlet:actionURL>">Supprimer</a>
      </td>
    </tr>
  </c:forEach>
</table>

<a href="<portlet:renderURL><portlet:param name="action" value="addEntry" /></portlet:renderURL>">Ajouter une personne</a>

5.2.4.2. Validator

Nous allons utiliser un validator légèrement différent, notemment parce qu'il ne sera plus nécessaire de vérifier l'identifiant.

org.esupportail.portlet.spring.web.UpdateEntryValidator :

package org.esupportail.portlet.spring.web;

import org.esupportail.portlet.spring.beans.Entry;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;

public class UpdateEntryValidator implements Validator {

  /**
   * Indique le type d'objet supporté par ce validator
   * @param clazz
   * @return
   */
  public boolean supports(Class clazz) {
    return clazz.equals(Entry.class);
  }

  /**
   * Validation d'une entrée
   * @param 
   */
  public void validate(Object o, Errors errors) {
    Entry e = (Entry)o;
    validateName(e.getName(), errors);
    validateFirstName(e.getFirstName(), errors);
    validateMail(e.getMail(), errors);
  }

  /**
   * Valide le nom
   * @param name
   * @param errors
   */
  private void validateName(String name, Errors errors) {
    ValidationUtils.rejectIfEmpty(errors, "name", "name.required", "Le nom est obligatoire");
  }

  /**
   * Valide le prénom
   * @param firstName
   * @param errors
   */
  private void validateFirstName(String firstName, Errors errors) {
    ValidationUtils.rejectIfEmpty(errors, "firstName", "firstName.required", "Le prénom est obligatoire");
  }

  /**
   * Valide le mail
   * @param mail
   * @param errors
   */
  private void validateMail(String mail, Errors errors) {
    ValidationUtils.rejectIfEmpty(errors, "mail", "mail.required", "L'adresse est obligatoire");
    if(!mail.equals("")) {
      if(!mail.matches("^\\w[_\\-\\.\\w]*\\@\\w[_\\-\\.\\w]*\\.(\\w){2,3}$")) {
        errors.rejectValue("mail", "mail.invalid", "Adresse invalide");
      }
    }
  }
}

5.2.4.3. Contrôleur

Nous allons créer un nouveau contrôleur qui ressemble étrangement au précédent, notez les légères modifications.

org.esupportail.portlet.spring.web.UpdateEntryController :

package org.esupportail.portlet.spring.web;

import javax.portlet.ActionRequest;
import javax.portlet.ActionResponse;
import javax.portlet.PortletRequest;

import org.esupportail.portlet.spring.beans.Entry;
import org.esupportail.portlet.spring.domain.DomainService;
import org.springframework.validation.BindException;
import org.springframework.web.portlet.mvc.SimpleFormController;

public class UpdateEntryController extends SimpleFormController {

  private DomainService domainService;

  /**
   * Positionne le service métier
   * @param domainService
   */
  public void setDomainService(DomainService domainService) {
    this.domainService = domainService;
  }

  /**
   * Traite le formulaire une fois validé
   * @param request
   * @param response
   * @param o
   * @param e
   */
  protected void onSubmitAction(ActionRequest request, ActionResponse response, Object o, BindException e) throws Exception {
    Entry entry = (Entry)o;
    domainService.updateAddress(entry);
    response.setRenderParameter("action", "home");
  }

  /**
   * Instancie l'objet sur lequel le formulaire sera mappé
   * @param request
   * @return
   */
  protected Object formBackingObject(PortletRequest request) throws Exception {
    return domainService.getAddress(request.getParameter("id"));
  }
}

5.2.4.4. Vue du formulaire

C'est la réplique exacte de la vue précédente, la seule différence étant le verrouillage du champ id.

webpages/stylesheets/update-entry.jsp :

<%@ include file="/WEB-INF/jsp/include.jsp" %>

<spring:hasBindErrors name="entry">
  <p class="portlet-msg-error">Merci de corriger les erreurs suivantes :</p>
  <ul>
    <spring:bind path="entry.*">
      <c:forEach items="${status.errorMessages}" var="error">
        <li><c:out value="${error}"/></li>
      </c:forEach>
    </spring:bind>
  </ul>
</spring:hasBindErrors>

<form method="post" action="<portlet:actionURL><portlet:param name="action" value="updateEntry" /></portlet:actionURL>">
  <table border="0" cellspacing="0" cellpadding="1">
    <tr>
      <td class="portlet-font" nowrap="true">Identifiant :</td>
      <td class="portlet-font" nowrap="true">
        <input type="hidden" name="id" value="${entry.id}" />
        <c:out value="${entry.id}" /></td>
      <td>&nbsp;</td>
    </tr>
    <tr>
      <td class="portlet-font" nowrap="true">Nom :</td>
      <td nowrap="true">
        <spring:bind path="entry.name">
          <input class="portlet-form-input-field" type="text" name="${status.expression}" value="${status.value}" />
          <span class="portlet-msg-error">${status.errorMessage}</span>
        </spring:bind>
      </td>
      <td class="portlet-section-text">(Obligatoire)</td>
    </tr>
    <tr>
      <td class="portlet-font" nowrap="true">Prénom :</td>
      <td nowrap="true">
        <spring:bind path="entry.firstName">
          <input class="portlet-form-input-field" type="text" name="${status.expression}" value="${status.value}" />
          <span class="portlet-msg-error">${status.errorMessage}</span>
        </spring:bind>
      </td>
      <td class="portlet-section-text">(Obligatoire)</td>
    </tr>
    <tr>
      <td class="portlet-font" nowrap="true">Mail :</td>
      <td nowrap="true">
        <spring:bind path="entry.mail">
          <input class="portlet-form-input-field" type="text" name="${status.expression}" value="${status.value}" />
          <span class="portlet-msg-error">${status.errorMessage}</span>
        </spring:bind>
      </td>
      <td class="portlet-section-text">(Obligatoire)</td>
    </tr>
  </table>
  <br/>
  <button class="portlet-form-button" type="submit">Valider</button>
</form>
<a href="<portlet:renderURL><portlet:param name="action" value="home" /></portlet:renderURL>">Revenir à la page d'accueil</a>

5.2.4.5. Configuration Spring

Comme tout à l'heure, commençons par ajouter notre nouveau validator.

properties/portlet-spring-web.xml :

...
<bean id="updateEntryValidator" class="org.esupportail.portlet.spring.web.UpdateEntryValidator" />
...

On déclare notre contrôleur.

properties/portlet-spring-web.xml :

...
<bean id="updateEntryController" class="org.esupportail.portlet.spring.web.UpdateEntryController">
  <property name="domainService" ref="domainService" />
  <property name="commandName" value="entry" />
  <property name="commandClass" value="org.esupportail.portlet.spring.beans.Entry" />
  <property name="formView" value="update-entry" />
  <property name="validator" ref="updateEntryValidator" />
</bean>
...

Et on ajoute le nouveau mapping.

properties/portlet-spring-web.xml :

...
<bean id="portletModeParameterHandlerMapping" class="org.springframework.web.portlet.handler.PortletModeParameterHandlerMapping">
  <property name="order" value="10"/>
  <property name="interceptors">
    <list><ref bean="parameterMappingInterceptor"/></list>
  </property>
  <property name="portletModeParameterMap">
    <map>
      <entry key="view">
        <map>
          <entry key="home" ref="homeController" />
          <entry key="addEntry" ref="addEntryController" />
          <entry key="updateEntry" ref="updateEntryController" />
          <entry key="delEntry" ref="delEntryController" />
        </map>
      </entry>
    </map>
  </property>
</bean>
...

Voici le résultat :

5.3. Définition de la couche d'accès aux données

La couche d'accès aux données à pour objectif de masquer à la couche métier les méthodes utilisées pour assurer la persistance des informations. Elle utilisera souvent une base de données, dans ce cas il faudra s'abstraire du SGBD utilisé. On peut également imaginer des accès LDAP, ou à des fichiers XML ou bien d'autres choses encore. Cette couche a la particularité de ne pas obligatoirement être unique. On peut avoir plusieurs couches d'accès utilisées en parallèle par la couche métier afin d'accéder à des informations situées dans des endroits différents. Cette multiplicité ne doit toutefois jamais influencer la couche présentation.

Nous verrons principalement l'accès à des bases de données, et l'utilisation d'outils tiers pour le mapping objet / relationnel.

5.3.1. Service de données

Comme pour la couche métier nous allons commencer par définir une interface déclarant toutes les méthodes dont nous aurons besoin dans notre couche d'accès aux données, quelle que soit l'implémentation choisie. Elle va fortement ressembler à celle de la couche métier, principalement du fait de la simplicité de notre application, mais cette séparation est impérative.

org.esupportail.portlet.spring.dao.DataService :

package org.esupportail.portlet.spring.dao;

import java.util.List;

import org.esupportail.portlet.spring.beans.Entry;

public interface DataService {

  public List<Entry> getAddresses();

  public Entry getAddress(String id);

  public void addAddress(Entry entry);

  public void updateAddress(Entry entry);

  public void deleteAddress(Entry entry);
}

5.3.2. Service métier

Nous allons réaliser une nouvelle implémentation de notre service métier. Par opposition à notre première implémentation statique, celle-ci sera une implémentation reposant sur notre service de données.

org.esupportail.portlet.spring.domain.DomainServiceDao :

package org.esupportail.portlet.spring.domain;

import java.util.List;

import org.esupportail.portlet.spring.beans.Entry;
import org.esupportail.portlet.spring.dao.DataService;

public class DomainServiceDao implements DomainService {

private DataService dataService;

  /**
   * Positionne le service de données
   * @param dataService
   */
  public void setDataService(DataService dataService) {
    this.dataService = dataService;
  }

  /**
   * Retourne la liste des adresses
   * @return
   */
  public List<Entry> getAddresses() {
    return dataService.getAddresses();
  }

  /**
   * Retourne une adresse
   * @param id
   * @return
   */
  public Entry getAddress(String id) {
    return dataService.getAddress(id);
  }

  /**
   * Ajoute une adresse
   * @param entry
   */
  public void addAddress(Entry entry) {
    dataService.addAddress(entry);
  }

  /**
   * Met à jour une adresse
   * @param entry
   */
  public void updateAddress(Entry entry) {
    dataService.updateAddress(entry);
  }

  /**
   * Supprime une adresse
   * @param entry
   */
  public void deleteAddress(Entry entry) {
    dataService.deleteAddress(entry);
  }
}

Ce service est assez pauvre, il se contente de faire des callbacks aux services de la couche de données. Par contre il sera très utile pour venir entrelacer autour de ses méthodes de la programmation par aspects, par exemple pour mettre en place un mécanisme de logs, de transactions ou encore d'alertes.

Nous allons maintenant modifier notre configuration Spring de façon à utiliser cette nouvelle implémentation de la couche métier.

properties/portlet-spring-domain.xml :

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">

<beans>
  <bean id="domainService" class="org.esupportail.portlet.spring.domain.DomainServiceDao">
    <property name="dataService" ref="dataService" />
  </bean>  
</beans>

Pour l'instant, cette configuration n'est pas valide puisque le bean dataService n'a pas encore été déclaré. Mais on notera que ce changement de couche métier n'aura aucune influence sur le reste de notre application.

5.3.3. Base de données

Nous allons maintenant créer notre base de données. Voici un script SQL permettant de créer la table nécessaire.

db/database.sql :

CREATE TABLE `addressbook` (
id INT NOT NULL ,
name VARCHAR( 32 ) NOT NULL ,
firstname VARCHAR( 32 ) NOT NULL ,
mail VARCHAR( 128 ) NOT NULL ,
PRIMARY KEY ( id )
);

5.3.4. Ibatis

L'outil Ibatis propose un mapping objet / relationnel appelé SQL-MAPS. C'est un outil assez léger puisqu'il est toujours obligatoire d'écrire ses requêtes SQL mais celles-ci sont externalisées dans un fichier de configuration. Nous laisserons par contre à Spring la gestion des connexions. Comme nous sommes dans un environnement multi-threadé il est impératif d'utiliser un pool de connexions. Nous aurons l'occasion de voir les différentes méthodes proposées par Spring pour traiter ce problème.

5.3.4.1. Fichier de configuration sql-map-config.xml

C'est le fichier de configuration principal d'Ibatis. Dans notre cas, il ne contiendra qu'une référence au fichier de mapping entry.xml.

properties/sql-map-config.xml :

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMapConfig PUBLIC "-//iBATIS.com//DTD SQL Map Config 2.0//EN" "http://www.ibatis.com/dtd/sql-map-config-2.dtd">

<sqlMapConfig>
    <sqlMap resource="entry.xml"/>
</sqlMapConfig>

5.3.4.2. Fichier de configuration entry.xml

C'est dans ce fichier que nous allons écrire nos requêtes et notre mapping avec l'objet Java Entry.

properties/entry.xml :

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMap PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN" "http://www.ibatis.com/dtd/sql-map-2.dtd">

<sqlMap namespace="Entry">
    
    <resultMap id="entry" class="org.esupportail.portlet.spring.beans.Entry">
        <result property="id" column="id" />
        <result property="name" column="name" />
        <result property="firstName" column="firstname" />
        <result property="mail" column="mail" />
    </resultMap>
    
    <statement id="getEntries" resultMap="entry">
        SELECT * FROM addressbook
    </statement>
    
    <statement id="getEntry" resultMap="entry">
        SELECT * FROM addressbook WHERE id = #id#
    </statement>
    
    <statement id="addEntry">
        INSERT INTO addressbook(id, name, firstname, mail) VALUES(#id#, #name#, #firstName#, #mail#)
    </statement>
    
    <statement id="updateEntry">
        UPDATE addressbook SET name = #name#, firstname = #firstName#, mail = #mail# WHERE id = #id#
    </statement>
    
    <statement id="deleteEntry">
        DELETE FROM addressbook WHERE id = #id#
    </statement>
    
</sqlMap>

On a commencé par définir un mapping entre les champs de la table de notre base de données et les propriétés de l'objet Java Entry. Ensuite on a définit un statement pour chaque opération que l'on souhaite réaliser en y associant une requête SQL.

5.3.4.3. Implémentation

Nous allons maintenant implémenter notre couche de données à l'aide des outils Spring / Ibatis.

org.esupportail.portlet.spring.dao.DataServiceIbatis :

package org.esupportail.portlet.spring.dao;
            
import java.util.HashMap;
import java.util.List;
import java.util.Map;
           
import org.esupportail.portlet.spring.beans.Entry;
import org.springframework.orm.ibatis.SqlMapClientTemplate;
           
public class DataServiceIbatis implements DataService {
            
  private SqlMapClientTemplate sqlMapClientTemplate;
            
  /**
   * Positionne le service Ibatis
   * @param sqlMapClientTemplate
   */
  public void setSqlMapClientTemplate(SqlMapClientTemplate sqlMapClientTemplate) {
    this.sqlMapClientTemplate = sqlMapClientTemplate;
  }
            
  /**
   * Retourne la liste des adresses
   * @return
   */
  public List<Entry> getAddresses() {
    return (List<Entry>) sqlMapClientTemplate.queryForList("getEntries", null);
  }
            
  /**
   * Retourne une adresse
   * @param id
   * @return
   */
  public Entry getAddress(String id) {
    return (Entry)sqlMapClientTemplate.queryForObject("getEntry", id);
  }
            
  /**
   * Ajoute une adresse
   * @param entry
   */
  public void addAddress(Entry entry) {
    sqlMapClientTemplate.update("addEntry", entry);
  }
            
  /**
   * Met à jour une adresse
   * @param entry
   */
  public void updateAddress(Entry entry) {
    sqlMapClientTemplate.update("updateEntry", entry);
  }            
  /**
   * Supprime une adresse
   * @param entry
   */
  public void deleteAddress(Entry entry) {
    sqlMapClientTemplate.update("deleteEntry", entry);
  }
}

On s'est contenté de faire appel aux statements définis dans notre fichier de mapping Ibatis en lui fournissant les paramètres nécessaires. Nous verrons par la suite comment Spring définira l'objet SqlMapClientTemplate.

5.3.4.4. Configuration Spring

Nous allons commencer par déclarer notre implémentation de couche d'accès aux données. Tout ce qui concerne cette couche sera définit dans un fichier spécifique.

properties/portlet-spring-dao.xml :

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">

<beans>
  <bean id="dataService" class="org.esupportail.portlet.spring.dao.DataServiceIbatis"> 
    <property name="sqlMapClientTemplate" ref="sqlMapClientTemplate" />
  </bean>
</beans>

Nous définissons alors notre bean sqlMapClientTemplate.

properties/portlet-spring-dao.xml :

...
<bean id="sqlMapClientTemplate" class="org.springframework.orm.ibatis.SqlMapClientTemplate">
  <property name="sqlMapClient" ref="sqlMapClient" />
</bean>
...

Ce n'est pas encore finit, nous devons définir le bean sqlMapClient.

properties/portlet-spring-dao.xml :

...
<bean id="sqlMapClient" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
   <property name="configLocation" value="/WEB-INF/classes/sql-map-config.xml" />
   <property name="dataSource" ref="dataSource" />
</bean>
...

C'est maintenant au tour du bean dataSource. Celui-ci définit la façon dont seront obtenues les connexions à la base de données.

Voici un premier exemple où ces connexions sont obtenues via un pool de connexion JNDI (Tomcat).

properties/portlet-spring-dao.xml :

...
<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
  <property name="jndiName" value="java:comp/env/MonPoolTomcat" />
</bean>
...

Voici un autre exemple où on va laisser Spring monter un pool de connexion, par contre certaines librairies supplémentaires sont nécessaires : commons-pool et commons-dbcp.

properties/portlet-spring-dao.xml :

...
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
  <property name="driverClassName" value="com.mysql.jdbc.Driver" />
  <property name="url" value="jdbc:mysql://mysql.univ.fr/mabase" />
  <property name="username" value="username" />
  <property name="password" value="password" />
</bean>
...

Pour que notre application fonctionne, nous devons placer dans le dossier lib les fichiers suivants distribués avec Spring :

5.3.5. Hibernate

Hibernate est un autre outil tiers permettant de réaliser un mapping objet / relationnel. Il est plus puissant puisqu'il n'y a plus à écrire les requêtes SQL.

5.3.5.1. Fichier de configuration address.hbm

C'est le fichier de configuration d'Hibernate dans lequel nous allons déclarer le mapping entre la base de données et l'objet Java Entry.

properties/address.hbm :

<?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="addressbook">
    <id name="id" column="id" type="string" />
    <property name="name" column="name" length="45" not-null="true" type="string" />
    <property name="firstName" column="firstName" length="45" not-null="true" type="string" />
    <property name="mail" column="mail" length="250" not-null="true" type="string" />
 </class>
</hibernate-mapping>

5.3.5.2. Implémentation

Comme pour Ibatis, nous implémentons notre couche de données en utilisant cette fois les outils Hibernate.

org.esupportail.portlet.spring.dao.DataServiceHibernate :

package org.esupportail.portlet.spring.dao;

import java.util.List;

import org.esupportail.portlet.spring.beans.Entry;
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;

public class DataServiceHibernate extends HibernateDaoSupport implements DataService {

  /**
   * Retourne la liste des adresses
   * @return
   */
  public List<Entry> getAddresses() {
    return (List<Entry>)getHibernateTemplate().loadAll(Entry.class); 
  }

  /**
   * Retourne une adresse
   * @param id
   * @return
   */
  public Entry getAddress(String id) {
    return (Entry)getHibernateTemplate().load(Entry.class, id);
  }

  /**
   * Ajoute une adresse
   * @param entry
   */
  public void addAddress(Entry entry) {
    getHibernateTemplate().persist(entry);
  }

  /**
   * Met à jour une adresse
   * @param entry
   */
  public void updateAddress(Entry entry) {
    getHibernateTemplate().update(entry);
  }

  /**
   * Supprime une adresse
   * @param id
   */
  public void deleteAddress(Entry entry) { 
    getHibernateTemplate().delete(entry);
  }
}

5.3.5.3. Configuration Spring

Dans le fichier de configuration Spring, nous allons mettre en commentaire tous les beans spécifiques à Ibatis (sqlMapClientTemplate, sqlMapClient) et redéfinir notre bean dataService.

properties/portlet-spring-dao.xml :

...
<bean id="dataService" class="org.esupportail.portlet.spring.dao.DataServiceHibernate"> 
  <property name="sessionFactory" ref="sessionFactory" />
</bean>
...

Il faut maintenant définir le bean sessionFactory spécifique à Hibernate.

properties/portlet-spring-dao.xml :

...
<bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
  <property name="dataSource"><ref bean="dataSource"/></property>
  <property name="mappingResources">
    <list>
      <value>address.hbm</value>
    </list>
  </property>
  <property name="hibernateProperties">
    <props>
      <prop key="hibernate.dialect">org.hibernate.dialect.MySQLDialect</prop>
    </props>
  </property>
</bean>
...

On constate qu'Hibernate a également besoin d'un bean dataSource, il n'est pas besoin de le redéfinir puisqu'il est 100% compatible avec celui définit pour Ibatis.

Pour que notre application fonctionne correctement, certaines librairies doivent être ajoutées, elles sont pour la plupart fournies avec la distribution Spring :

5.3.6. LDAP

Les annuaires LDAP sont des sources de données particulières dans le sens où il ne s'agit pas de bases de données relationnelles accessibles via JDBC et interrogeables en SQL. Une tentative a bien été réalisée pour développer un driver LDAP / JDBC, mais il n'est pas possible d'exploiter au maximum les possibilités de LDAP en se limitant aux fonctions d'un SGBD relationnel. La seule solution pour un développeur est d'utiliser l'API JNDI. Celle-ci est prévue pour accéder à différents types de dépôts hiérarchiques et arborescents comme les annuaires LDAP, les annuaires de noms CORBA, les systèmes de fichiers etc. Toutefois, cette API a exactement le même type d'inconvénients que JDBC, un mécanisme d'exception très lourd à gérer, beaucoup de code pour ouvrir / fermer les connexions et enfin un très grosse dépendance entre les différents objets mis en oeuvre.

LdapTemplate est un framework destiné à simplifier l'accès à des annuaires LDAP en Java. Il a été développé de façon similaire au module JDBC de Spring, notemment au niveau de la hiérarchie d'exceptions et des principes de fonctionnement.

5.3.6.1. Implémentation

Comme pour Ibatis et Hibernate, nous implémentons notre couche de données en utilisant l'outil LdapTemplate.

org.esupportail.portlet.spring.dao.DataServiceLdap :

package org.esupportail.portlet.spring.dao;

import java.util.List;
import java.util.Map;

import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.BasicAttribute;
import javax.naming.directory.BasicAttributes;

import net.sf.ldaptemplate.AttributesMapper;
import net.sf.ldaptemplate.LdapTemplate;
import net.sf.ldaptemplate.support.DistinguishedName;

import org.esupportail.portlet.spring.beans.Entry;

public class DataServiceLdap implements DataService {

  private String context;
  private Map<String, Object> staticAttributes;
  private LdapTemplate ldapTemplate;

  /**
   * Positionne le contexte LDAP
   * @param context
   */
  public void setContext(String context) {
    this.context = context;
  }

  /**
   * Positionne les attributs statiques
   * @param staticAttributes
   */
  public void setStaticAttributes(Map<String, Object> staticAttributes) {
    this.staticAttributes = staticAttributes;
  }

  /**
   * Positionne le service LdapTemplate
   * @param ldapTemplate
   */
  public void setLdapTemplate(LdapTemplate ldapTemplate) {
    this.ldapTemplate = ldapTemplate;
  }

  /**
   * Retourne la liste des adresses
   * @return
   */
  @SuppressWarnings("unchecked")
  public List<Entry> getAddresses() {
    DistinguishedName dn = new DistinguishedName(context);
    return (List<Entry>)ldapTemplate.search(dn, "uid=*", new EntryAttributesMapper());
  }

  /**
   * Retourne une adresse
   * @param id
   * @return
   */
  public Entry getAddress(String id) {
    DistinguishedName dn = new DistinguishedName(context);
    return (Entry)ldapTemplate.search(dn, "uid=" + id, new EntryAttributesMapper());
  }

  /**
   * Ajoute une adresse
   * @param entry
   */
  public void addAddress(Entry entry) {
    DistinguishedName dn = new DistinguishedName(context);
    dn.add("uid", entry.getId());
    ldapTemplate.bind(dn, null, buildAttributes(entry));
  }

  /**
   * Met à jour une adresse
   * @param entry
   */
  public void updateAddress(Entry entry) {
    DistinguishedName dn = new DistinguishedName(context);
    dn.add("uid", entry.getId());
    ldapTemplate.rebind(dn, null, buildAttributes(entry));
  }

  /**
   * Supprime une adresse
   * @param entry
   */
  public void deleteAddress(Entry entry) {
    DistinguishedName dn = new DistinguishedName(context);
    dn.add("uid", entry.getId());
    ldapTemplate.unbind(dn);
  }

  /**
   * Mapping Objet Java / Attributs LDAP
   * @param entry
   * @return
   */
  private Attributes buildAttributes(Entry entry) {
    Attributes attrs = new BasicAttributes(true);
    // Attributs statiques
    for(String key : staticAttributes.keySet()) {
      Object o = staticAttributes.get(key);
      if(o instanceof List) {
        List values = (List)o;
        Attribute a = new BasicAttribute(key);
        for(Object value : values) {
          a.add(value);
        }
        attrs.put(a);
      }
      else {
        attrs.put(key, o);
      }
    }
    attrs.put("uid", entry.getId());
    attrs.put("sn", entry.getName());
    attrs.put("givenName", entry.getFirstName());
    attrs.put("mail", entry.getMail());
    return attrs;
  }

  private class EntryAttributesMapper implements AttributesMapper {

    /**
     * Mapping Attributs LDAP / Objet Java
     * @param attrs
     * @return
     */
    public Object mapFromAttributes(Attributes attrs) throws NamingException {
      Entry entry = new Entry();
      entry.setId(attrs.get("uid").get().toString());
      entry.setName(attrs.get("sn").get().toString());
      entry.setFirstName(attrs.get("givenName").get().toString());
      entry.setMail(attrs.get("mail").get().toString());
      return entry;
    }
  }
}

Quelques explications sur ce code. Spring va être chargé d'injecter dans notre classe ses trois propriétés :

Nous avons défini également une inner class chargée d'implémenter un mapping entre les attributs LDAP et notre objet Java Entry. Nous avons également réalisé l'opération inverse via la méthode privée buildAttributes. Le mapper LDAP / Java est utilisé par toutes les opérations de lecture de notre objet ldapTemplate comme les bind et les search.

5.3.6.2. Configuration Spring

Dans le fichier de configuration Spring, nous allons mettre en commentaire tous les beans spécifiques à Ibatis et Hibernate et redéfinir notre bean dataService.

properties/portlet-spring-dao.xml :

...
<bean id="dataService" class="org.esupportail.portlet.spring.dao.DataServiceLdap">
  <property name="context" value="ou=AddressBook" />
  <property name="staticAttributes">
    <map>
      <entry key="objectclass">
        <list>
          <value>top</value>
          <value>addressbookentry</value>
        </list>
      </entry>
      <entry key="addType" value="standard" />
    </map>
  </property>
  <property name="ldapTemplate" ref="ldapTemplate" />
</bean>
...

Nous en avons profité pour définir notre contexte LDAP ainsi que les attributs statiques qui seront systématiquement ajoutés. Il reste à définir la partie LdapTemplate qui nécessite la définition de deux beans supplémentaires.

properties/portlet-spring-dao.xml :

...
<bean id="contextSource" class="net.sf.ldaptemplate.support.LdapContextSource">
  <property name="url" value="ldap://ldap.univ.fr/dc=univ,dc=fr" />
  <property name="userName" value="" />
  <property name="password" value="" />
</bean>
  
<bean id="ldapTemplate" class="net.sf.ldaptemplate.LdapTemplate">
  <property name="contextSource" ref="contextSource" />
</bean>
...

On notera la similitude avec un JdbcTemplate et une DataSource.

Pour que notre application fonctionne, certaines librairies doivent être ajoutées :

5.4. Traitement des erreurs

Un aspect important de la programmation d'une application est la gestion des erreurs. Java propose un puissant mécanisme pour réaliser cette tâche, les exceptions. Toutefois, à trop vouloir bien faire, il devient rapidement très fastidieux d'écrire des blocs try / catch / finally garantissant qu'aucune erreur ne se produit. Spring va en partie nous faciliter les choses de ce côté. Vous aurez probablement remarqué au niveau de l'implémentation de la couche d'accès aux données qu'aucune exception n'est levée par les classes de Spring comme SqlMapClientTemplate ou DataServiceHibernate. En réalité, le package DAO de Spring fournit une hiérarchie d'exceptions unique quelle que soit l'implémentation choisie. Ces exceptions sont de type RuntimeException, il n'est donc pas obligatoire de les traiter et elles 'wrappent' les exceptions propriétaires de l'implémentation choisie. Il est dès lors possible de décider à quel niveau nous allons gérer ces erreurs. Supposons que nous souhaitons mettre en place un mécanisme de transactions, avec des rollbacks automatiques dès qu'une exception survient. Dans ce cas, c'est probablement dans la couche métier que nous le ferons. Les exceptions relatives à la couche d'accès aux données seront ainsi bloquées à ce niveau, et il sera alors possible de propager d'autres exceptions cette fois relatives à la couche métier. Il sera alors très intéressant de réaliser le même genre de chose, à savoir n'utiliser que des RuntimeException afin une fois de plus d'avoir la liberté de les traiter à l'endroit que l'on souhaite.

Si on poursuit ce raisonnement on se rend compte que tôt où tard, certaines exceptions remonteront toute la pile d'exécution jusqu'à la couche métier de notre application, et même au delà au niveau des couches Spring chargées du fonctionnement du MVC. Ce seront généralement des exceptions irrécupérables du genre NullPointerException, ClassCastException ou d'autres encore plus exotiques. Nous allons voir comment les traiter et surtout comment afficher un message d'erreur à l'utilisateur de façon à lui épargner la vue d'une stack d'exception Java.

5.4.1. Configuration Spring

Nous allons déclarer un nouvel Handler qui sera chargé de collecter toutes les exceptions qui pourraient survenir dans notre application.

properties/portlet-spring-web.xml :

...
<bean id="exceptionHandler" class="org.springframework.web.portlet.handler.SimpleMappingExceptionResolver">
  <property name="defaultErrorView" value="fatalerror"/>
  <property name="exceptionMappings">
    <props>
      <prop key="org.springframework.dao.DataAccessException">dberror</prop>
    </props>
  </property>  
</bean>
...

Ce nouveau bean se contente de faire des mappings entre certains types d'exceptions et des vues. Dans notre cas, toutes les exceptions de type DataAccessException (l'exception racine de la hiérarchie d'exceptions Spring pour les couches d'accès aux données) provoquent un affichage de la vue dberror. Toutes les autres exceptions provoquent l'affichage de la vue fatalerror, on a ainsi la garantie qu'aucune exception ne sera propagée jusqu'à la JVM évitant ainsi tout plantage violent de notre application.

5.4.2. Vues

Il ne nous reste plus qu'à écrire les pages JSP qui réaliseront l'affichage de nos deux nouvelles vues. Le choix de la feuille JSP est soumis au même processus que celui mis en oeuvre dans les contrôleurs, c'est le ViewResolver qui se charge de faire la correspondance.

webpages/stylesheets/dberror.jsp :

<%@ include file="/WEB-INF/jsp/include.jsp" %>

<table border="0" cellpadding="2" cellspacing="0">
  <tr>
    <td class="portlet-font" nowrap="true">Erreur base de données</td>
  </tr>
  <tr>
    <td>&nbsp;</td>
    <td class="portlet-msg-error">Message : <c:out value="${exception.message}" /></td>
  </tr>
</table>

<br/><br/>
<span class="portlet-font">
  <a href="<portlet:renderURL><portlet:param name="action" value="home" /></portlet:renderURL>">Continuer</a>
</span>

On a choisi ici de traiter ce type d'exception de façon non bloquante, un lien en bas de la vue permet de revenir à la page d'accueil de notre application.

webpages/stylesheets/fatalerror.jsp :

<%@ include file="/WEB-INF/jsp/include.jsp" %>

<table border="0" cellpadding="2" cellspacing="0">
  <tr>
    <td class="portlet-font" nowrap="true">Erreur fatale</td>
  </tr>
  <tr>
    <td>&nbsp;</td>
    <td class="portlet-msg-error">Message : <c:out value="${exception}" /></td>
  </tr>
</table>

Ici par contre on a considéré que l'erreur était irrécupérable et l'utilisateur n'a pas la possibilité de faire quoi que ce soit. Pour revenir à l'application, il sera obligé de quitter le portail et de s'y reconnecter afin de démarrer une nouvelle session. Le développeur est bien entendu libre de choisir ce qu'il affiche dans ces vues, dans cet exemple il est probable que la pile d'exception soit affichée ce qui ne lui apportera pas grand chose.

5.5. Externalisation de la configuration

Depuis le début de ce tutoriel, toute la configuration de notre application est réalisée dans les fichiers Spring. Normalement, les couches d'accès aux données ont des fichiers de configuration spécifiques mais ils ne devraient pas contenir d'informations susceptibles d'être modifiées par la personne chargée du déploiement. Cependant, un administrateur système n'aura pas forcément connaissance de Spring, il convient donc au maximum de lui éviter la pénible tâche d'éditer les fichiers XML de Spring, d'autant que rien ne permet de lui indiquer les paramètres qu'il doit modifier et ceux auxquels il ne doit absolument pas toucher. Nous allons donc utiliser une astuce afin d'externaliser certains aspects de notre configuration dans un (ou plusieur) fichier(s) de propriétés.

Commençons par déclarer un bean Spring qui se chargera de faire le travail à notre place. Peu importe le fichier où il est déclaré, l'essentiel est que cette déclaration soit unique. Comme nos seuls paramètres concernent la connexion à la base de données, nous allons le placer dans le fichier portlet-spring-dao.xml.

properties/portlet-spring-dao.xml :

...
<bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
  <property name="location" value="/WEB-INF/classes/config.properties" />
</bean>
...

Ecrivons maintenant le fichier config.properties que nous venons de déclarer et plaçons-y tout ce qui relève de la configuration de notre application dépendante de l'environnement d'installation.

properties/config.properties :

##
# Paramètres de connexion à la base de données
##

# Driver
db.driver=com.mysql.jdbc.Driver
# URL
db.url=jdbc:mysql://mysql.univ.fr/mabase
# User
db.user=root
# Password
db.password=admin

##
# Paramètres de connexion LDAP
##

# URL
ldap.url=ldap://ldap.univ.fr/dc=univ,dc=fr
# User
ldap.user=
# Password
ldap.password=

Il ne reste plus qu'à modifier nos fichiers de configuration pour faire appel aux propriétés que nous venons de définir, dans notre cas seul les beans dataSource et contextSource sont impactés.

properties/portlet-spring-dao.xml :

...
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
  <property name="driverClassName" value="${db.driver}" />
  <property name="url" value="${db.url}" />
  <property name="username" value="${db.user}" />
  <property name="password" value="${db.password}" />
</bean>
...
<bean id="contextSource" class="net.sf.ldaptemplate.support.LdapContextSource">
  <property name="url" value="${ldap.url}" />
  <property name="userName" value="${ldap.user}" />
  <property name="password" value="${ldap.password}" />
</bean>
...

Important

Il y a une limitation à cette façon de procéder, il n'est pas possible de définir de propriétés multi-valuées. Dans ce cas, on peut se rabattre sur un fichier de configuration Spring supplémentaire que nous appellerons portlet-spring-custom.xml et qui contiendra uniquement des beans de type List, Map, Set ou Properties.

5.6. Internationalisation

TODO