wiki:EsupPortSpring

Développement de portlets pour ESUP avec Spring

Note : cette page reprend un tutoriel disponible en ligne, en corrigeant les erreurs que j'ai trouvées.

TOC?

Fichiers 'build' pour démarrer un projet

De façon à avoir des Portlets qui se compilent / déploient de façon identique, voici un fichier de configuration ANT pouvant servir de base commune à tous les développements, les télécharger :

*build.properties :  http://www.esup-portail.org/consortium/espace/Normes_1C/portlet-dev/media/build.properties

*build.xml :  http://www.esup-portail.org/consortium/espace/Normes_1C/portlet-dev/media/build.xml

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.

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.

Librairies Spring

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

  • spring.jar : tous les modules standards de Spring
  • spring-portlet.jar : le module externe dédié aux Portlets.

Librairies tierces

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

  • commons-logging.jar
  • commons-collections.jar
  • log4j-x.x.x.jar
  • standard.jar
  • jstl.jar

Vous pouvez ajouter dans les dépendances depuis Eclipse la librairie portlet-api.jar pour eviter que eclipse vous indique des erreurs, mais ne surtout pas la copier dans le dossier lib de votre portlet, cela fait planter le portail car elle est déjà présente dans esup.

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

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.

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 :

  • <display-name> : il s'agit du nom de la portlet, ce paramètre n'a pas de réelle importance.
  • <context-param> : on place un paramètre de contexte indiquant le fichier de configuration Spring à charger au démarrage.
  • <listener> : ce Listener Spring 'écoute' afin de savoir quand le contexte est initialisé, il se charge alors de charger le fichier de configuration déclaré dans le paramètre de contexte contextConfigLocation.
  • <servlet> : la seule Servlet que l'on déclare est la servlet de rendu, qu'on a mappée à une URL bien précise. C'est sur cette URL que notre 'ViewResolver?' défini dans la configuration Spring écoute par défaut afin de générer le rendu de notre Portlet.

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 :

  • <portlet-name> : ce paramètre est très important puisqu'il va servir d'identifiant unique de notre Portlet. Par exemple avec uPortal lors de la publication d'une Portlet ce paramètre est nécessaire.
  • <portlet-class> : on définit ici une classe Spring qui va réaliser l'aiguillage des requêtes vers des contrôleurs, ceux-ci étant expliqués plus loin.
  • <init-param> : comme dans le fichier web.xml, on déclare les fichiers de configuration Spring qui vont être traités à l'initialisation de la Portlet. Ici on a choisi de traiter tous les fichiers correspondants à l'expression régulière portlet-spring-*.xml (on va découper notre configuration en différents fichiers chacun traitant une couche de notre application).
  • <supports> : c'est ici qu'on va définir les modes supportés par notre Portlet (ici on n'utilise que le mode view) et le type MIME utilisé pour le rendu (HTML puisque c'est le type minimal supporté par tous les portails).

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

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 :

<web-app>
...
<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>
...
</web-app>

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

  • <servlet-name> : doit être identique au <portlet-name> du fichier portlet.xml.
  • <servlet-class> : la classe servant de point d'entrée pour Pluto.
  • <init-param><param-name>portlet-class : la classe wrappée par Pluto, autrement dit la classe définie dans le <portlet-class> du fichier portlet.xml.
  • <init-param><param-name>portlet-guid : l'identifiant unique de notre portlet, composé du nom de notre application (et donc le futur nom du contexte Tomcat) et du <portlet-name> de notre fichier portlet.xml.
  • <url-pattern> : déduit du <servlet-name> donc du <portlet-name>.

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 :

  • Ecrire le contrôleur Java qui traitera les requêtes arrivant sur la Portlet et qui choisira la vue.
  • Ecrire la feuille JSP traitant le rendu.
  • Ecrire la configuration Spring qui mettra ces différentes parties en relation.

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 :

  • Choix du controller : a chaque requête, Spring va devoir choisir quel contrôleur doit traiter celle-ci.
  • Choix de la vue : c'est le contrôleur qui va choisir quelle vue (logique) va réaliser le rendu graphique et qui va lui fournir le modèle dont elle a besoin.
  • Choix du viewResolver : il est chargé de faire correspondre au nom logique de la vue l'implémentation physique chargée de la réaliser. Nous avons choisi d'utiliser des JSP et notre ViewResolver? déjà défini dans le fichier applicationContext.xml se contente d'ajouter un préfixe et un suffixe au nom logique pour trouver la bonne page. On aurait pu utiliser un ViewResolver? qui utiliserait un autre type de MVC, comme Struts et Velocity.

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>

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.

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

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.

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.

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 :

  • Notre classe hérite de AbstractController? qui est le type de contrôleur le plus simple dans Spring. Il permet entre autres d'implémenter les traitements ayant lieu lors des deux phases principales d'une Portlet, l'action et le rendu.
  • Nous avons redéfini la méthode se chargeant du rendu afin qu'elle retourne un objet ModelAndView?.
  • Nous avons choisi de faire appelle à la vue logique 'home' pour réaliser notre rendu.
  • Nous avons défini un objet 'message' dans notre modèle et lui avons affecté la valeur 'Hello World !'

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.

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.

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

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>

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 :

  • Les requêtes de type render : affichage du formulaire suivi d'un 'Hello xxxx' avec xxxx ce que l'utilisateur aura saisi dans le formulaire précédemment.
  • Les requêtes de type action : traitement du formulaire et positionnement des paramètres pour le rendu.

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.