I. Introduction

La gestion des layouts dans les applications web a toujours été pour moi une épine dans le pied jusqu'à ce que je découvre Sitemesh.
Une API de layouting, doit permettre d'éviter de copier/coller du code inutile pour décrire dans chaque page le header, la navigation, le footer, ... C'est ce que permet Sitemesh.

Avant Sitemesh, trois solutions :
  • Inclusion de JSP
  • Tiles
  • Cocoon

L'inclusion de JSP est basique et très limitée car on ne peut réellement tirer profit des framework MVC avec elle. Pour moi ce n'est pas une solution donc je ne vais pas en parler ici.

Je ne connais pas cocoon, bien que j'aie étudié son fonctionnement au moment de choisir le nouveau framework pour gérer le layout. Je pense ce framework très intéressant, en voici une petite introduction (pour plus de détail, le site web français officiel est ici) :
Cocoon est un framework fondé sur la séparation des domaines techniques et de l'assemblage de composants. Il implémente ces concepts par la notion de "pipelines de composants", chaque composant du pipeline étant dédié à une fonction particulière. Cela permet d'utiliser une approche du type "Lego" pour la construction d'applications web, en assemblant des chaînes de composants, sans nécessiter de programmation. Pour cela, on peut par exemple utiliser des transformations XML/XSL directement via Cocoon.

Sitemesh se base sur le pattern décorateur pour gérer le layout des applications web. Mais qu'est-ce donc que cela me direz-vous? (ceux qui connaissent peuvent passer au paragraphe suivant).
Le pattern Décorateur permet de modifier dynamiquement les comportements de certains objets en leur ajoutant de nouvelles fonctionnalités. Le pattern Décorateur fonctionne sur le principe des "Lego": on créé de nouveaux comportements en assemblant des modules, qui constituent les "briques" de notre application. On peut facilement chaîner les décorateurs pour pouvoir ajouter des fonctionnalités complexes grâce à des enchaînements de modules. Les décorateurs permettent aussi de coder des chaînes de traitement, l'idée essentielle est de pouvoir définir un traitement complexe comme un assemblage de traitements simples, offrant ainsi souplesse et modularité. Pour aller plus loin, l'article complet des design patterns de pcaboche est ici avec une bonne explication du design pattern décorateur (dont je me suis inspiré).

Personnellement, j'ai utilisé Sitemesh pour gérer le layout du site institutionnel d'une société de télécom. Voici un petit schéma du layout du site:

Exemple d'un layout de site utilisant Sitemesh
Exemple d'un layout de site utilisant Sitemesh

II. Quelques mots de Tiles

Très longtemps j'ai utilisé Tiles qui a de nombreux avantages :
  • Intégration à Struts et Spring
  • Peut être utilisé comme framework seul (ce n'est pas un MVC complet mais ça peut rendre pas mal de service pour une couche web basique)
  • Configuration XML permettant l'héritage
  • Possibilité de splitter le fichier de configuration en plusieurs parties
  • Orientation composant avec un contrôleur par composant
  • Bonne performance (un collègue à fait un test avec 1000 tiles imbriqués générés ... et ça n'a pris que quelques centaines de millisecondes à être affiché)
Mais qui, hélas, comprend aussi de nombreux inconvénients :
  • Fichier de configuration peu structuré vite complexe et brouillon
  • Pas de possibilité de partage de layouts entre différents sites
  • Pas de gestion spécifique des headers HTML (voir les possibilités de Sitemesh dans ce domaine)
  • Très vite, beaucoup d'héritage entre composants rendent le fichier de configuration très peu lisible
  • Très verbeux en configuration
  • Le principe de composant amène très vite à créer énormément de composants, donc énormément de JSP et le temps de première compilation d'une page en devient très long
  • La gestion des erreurs au niveau d'un composant Tiles n'est pas aisée (impossible de faire une redirection web depuis un contrôleur Tiles car à ce stade, la requête est déjà commité)
  • Utilisable uniquement avec des JSP

III. L'approche de Sitemesh

Sitemesh lui, a une approche fort différente: Sitemesh utilise le design pattern décorateur pour ajouter des briques à votre page. Concrètement, vous ne développez que l'intérieur des pages, puis vous définissez dans Sitemesh un layout à utiliser dans lequel vous donnez les différents décorateurs qui vont aller ajouter des parties d'HTML (donc vont aller décorer) vos pages. La page interne que vous allez développer va être calculée, puis en fin de requête le filtre de Sitemesh va décorer votre page selon ce que vous avez configuré. Votre page va être parsée par Sitemesh et va pouvoir ensuite être accessible aux décorateurs. Bien sûr, Sitemesh permet la composition, c'est-à-dire que vous pouvez décorer une page qui est elle-même décorée par une autre ... Mais attention aux effets de bord et aux risques de décoration "en boucle". (Page A, décoré par B, B est lui-même décoré par C, C est lui-même décoré par A ...).

La principale différence de Sitemesh se situe dans le fait que vos pages peuvent être créées avec n'importe quelle technologie. Par exemple, vous définissez une page /toto.do en Spring MVC (ou en Struts ou n'importe quel framework que vous aimez), vous utilisez ensuite le filtre Servlet de Sitemesh et vous lui dites d'intercepter les requêtes en *.do. Sitemesh, lorsque vous appelez /toto.do, va alors intercepter la requête et la décorer.

Dans Sitemesh, les décorateurs sont eux-mêmes des pages complètes (donc, /footer.do, /header.do, /navigation.do ou /toto.do peuvent être vues indépendamment, en HTML et peuvent toutes êtres des contrôleurs Spring MVC), ce qui permet de les développer et les tester indépendamment. Et en plus, Sitemesh peut décorer une page d'un site, par une page d'un autre site! Pour cela il suffit de définir la page du décorateur comme une page externe (donc commençant par http://), attention, cela peut engendrer des problèmes si vous utilisez de la réécriture d'URL car Sitemesh utilise un HttpClient pour accéder à ces pages (ouvre une session sur le serveur distant, demande la page, puis fait la décoration). Le fait que chaque page, décorée ou "décorante", ait un HTML complet peut ouvrir des horizons et des possibilités multiples, par exemple définir en XHTML une navigation qui pourra être réutilisable indépendamment depuis un site web en Spring MVC via une décoration de Sitemesh ou depuis un composant Flex ou Ajax qui utiliserait la même navigation que vous ... Vos composants de pages deviennent indépendant les uns des autres! Sitemesh n'opérant qu'en fin de requête, n'importe qu'elle technologie web peut être utilisé du côté JAVA. Sitemesh lui-même, pour les pages décorées, permet d'utiliser les technologies JSP, Velocity ou FreeMarker. Par contre, pour les décorateurs, aucune limite, on peut très bien, en plus des technologies JAVA, décorer ses pages par des HTML statiques, des pages PHP, des scripts CGI, ...

Dernière fonctionnalité évoquée : la gestion des headers. Comme les pages décorées et les décorateurs sont des pages HTML complètes, elles contiennent toutes un titre, des CSS,... Sitemesh permet donc, au niveau de la décoration, de spécifier des headers par défaut puis de les réécrire par les valeurs des headers de l'objet décoré. En fait, Sitemesh parse les différentes pages HTML et nous donne accès aux éléments head et body de la page pour pouvoir aller puiser dedans les informations qui nous intéressent.

En plus, le passage de paramètre par les header n'est pas limité aux header de la page finale. On peut très bien définir un paramètre dans les header de la page de contenue qui sera ensuite utilisé dans le body du décorateur. Voici un exemple :

Page décorée
Sélectionnez

<html>
    <meta name="author" content="test@example.com">
    <head>
        <title>Simple Document</title>
    </head>
    <body>
        Hello World! <br />
    </body>
</html>
			
Décorateur
Sélectionnez

 <%@ taglib uri="sitemesh-decorator" prefix="decorator" %>
 <decorator:usePage id="myPage" />
 <html>
 	<head>
 		<title>My Site - <decorator:title default="Welcome!" /></title>
 		<decorator:head />
 	</head>
 	<body>
 		<h1><decorator:title default="Welcome!" /></h1>
		 <a href="mailto:<decorator:getProperty property="meta.author" default="staff@example.com" />">
		 	<decorator:getProperty property="meta.author" default="staff@example.com" />
		 </a>
 		<decorator:body />
 	</body>
 </html>
			

Bien sûr, il existe encore plein d'autres fonctionnalités pour ce framework très simple d'utilisation et de principe, mais très performant. Allez voir sur le site pour plus de détails. Je précise quand même que j'utilise Tiles depuis 3 ans et que je viens de découvrir Sitemesh, que j'utilise pour un nouveau projet, et j'en suis très content alors pourquoi pas vous (ça sonne comme une pub à la télé :=).

Petit récapitulatif Sitemesh:
  • Pattern decorator qui permet la séparation entre le code et le layout, l'intégration du layout se faisant via un filtre en fin de requête
  • Possibilité d'avoir les éléments du layout dans un site externe (donc de les partager entre plusieurs applications)
  • Tous les éléments (éléments de layout et pages internes) sont des HTML complets et donc développables et testables indépendamment
  • Agnostique par rapport à la technologie web utilisée (Java - Struts, Spring, ... -, PHP, CGI, ...)
  • Agnostique par rapport à la technologie utilisée pour les pages décorées (JSP, Vélocity, Freemarker)
  • Gestion intégrée des différents header des pages et des décorateurs
  • Nombreuses fonctionnalités avancées (voir plus bas)

IV. Exemple

Rappelons tout d'abord, le schéma du site:

Exemple d'un layout de site utilisant Sitemesh
Exemple d'un layout de site utilisant Sitemesh
Définition du filtre de servlet
Sélectionnez

<filter>
	<filter-name>sitemesh</filter-name>
	<filter-class>com.opensymphony.module.sitemesh.filter.PageFilter</filter-class>
</filter>
			
Définition du fichier de configuration de Sitemesh
Sélectionnez

<?xml version="1.0" encoding="utf-8"?>
<decorators defaultdir="/view/decorator">
   <decorator name="main" page="main.jsp">
       <pattern>/*</pattern>
       <excludes>
       <pattern>/ErrorPage</pattern>
       </excludes>
   </decorator>
   <decorator name="empty" page="empty.jsp"/>
</decorators>
			

Ici, on définit la JSP main.jsp comme description de layout et on lui dit d'appliquer ce layout à toutes les pages à l'exclusion de la page /ErrorPage. Le décorateur 'empty' servant à pouvoir définir des décorateurs qui ne décorent pas!

La page de layout
Sélectionnez

< ?xml version="1.0" encoding="utf-8"?>
< %@ taglib uri="http://www.opensymphony.com/sitemesh/decorator" prefix="decorator" %>
< %@ taglib uri="http://www.opensymphony.com/sitemesh/page" prefix="page" %>
< !DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<decorator:usePage id="myPage" />
<html>
    <head>
        <title><decorator:title default="DEFAULT TITLE"/></decorator>
        <decorator:head/>
    </head>
    <body>
        <div><page:applyDecorator name="empty" page="/decorator/header"/></div>
        <div><decorator:body /></div>
        <div><page:applyDecorator name="empty" page="/decorator/footer"/></div>
    </body>
</html>
			

Ici, on définit donc la page HTML globale, on lui précise des éléments du header global qui pourront être redéfinis dans l'élément décoré puis on a ensuite la page HTML de layout elle même.
    - <decorator:title/> : renvoie le titre de la page décorée>
    - <decorator:head/> : renvoie le header de la page décorée
    - <decorator:body/> : renvoie le body de la page décorée
    - <decorator name="empty" page="/decorator/footer"/> : applique le layout donné dans l'attribut 'name' sur la page donnée dans l'attribut 'page' et met le résultat ici. Grâce à cette ligne on décore la page interne par des morceaux d'autres pages, et on remarque que l'on peut chaîner les décorateurs facilement.
    - <decorator:usePage id="myPage" /> : permet de faire d'obtenir une référence sur l'objet page (donc l'objet représentant la page décorée) pour qu'il soit accessible depuis les décorateurs, et donc la page de layout définie ici.

V. Fonctionalités avancées

V-A. Les DecoratorMapper

En plus du mappage standard par fichier de configuration pour définir quel décorateur utiliser pour quel page. Sitemesh permet d'utiliser d'autre stratégie de mapping. Elles sont bien sûres à définir au sein du fichier de configuration de Sitemesh (sitemesh.xml).

En voici un exemple de configuration :

Configuration d'un PageDecoratorMapper
Sélectionnez

<mapper class="com.opensymphony.module.sitemesh.mapper.PageDecoratorMapper">
	<param name="property.1" value="meta.decorator" />
</mapper>
				
Les différents mapper de Sitemesh
  • PrintableDecoratorMapper: permet de définir un décorateur spécifique pour la version print d'une page
  • PageDecoratorMapper: permet de spécifier le nom du décorateur en attribut meta de la page
  • ParameterDecoratorMapper: se base du des paramètres de requête pour savoir quel décorateur utiliser
  • FrameSetDecoratorMapper: mapper spécifique à l'utilisation des frames
  • CookieDecoratorMapper: se base sur la valeur d'un cookie pour choisir quel décorateur utiliser
  • RobotDecoratorMapper: permet de définir un décorateur spécifique pour les robots

V-B. Le tag <content>

Le tag content permet de passer des données d'une page JSP à un décorateur. Dans la page, il faut définir un tag content dans lequel on met les données, puis dans la page du décorateur, on utilise le tag decorator:getProperty pour récupérer les données. Exemple ci-dessous :

Définition du tag content dans la page décorée
Sélectionnez

<content tag="pageName">
    Login Page
</content>
Récupération des données dans le décorateur
Sélectionnez

<decorator:getProperty property="page.pageName"/>

On peut remarquer que Sitemesh utilise un scope pour accéder aux données de la page: meta.property (comme vu précédemment), permet d'accéder aux propriétés des meta tags, et page.property permet d'accéder aux données définies dans la page même.

V-C. Accès avancés aux données de la page décorée (accès à l'objet Page)

Il y a trois manières avancées d'accéder aux données d'une page décorée, ces trois manières fonctionnent aussi bien pour accéder aux données du scope head que page:

1. En utilisant les tags JSP et scriplet
Sélectionnez

<%@ taglib uri="http://www.opensymphony.com/sitemesh/decorator" prefix="decorator" %>
<decorator:usePage id="thePage" />
<% String author = thePage.getProperty("meta.author"); %>
2. Par Velocity
Sélectionnez

$page.getProperty("meta.author")
3. Accès en pure JAVA
Sélectionnez

import com.opensymphony.module.sitemesh.Page;
import com.opensymphony.module.sitemesh.RequestConstants;
...
Page thePage = request.getAttribute(RequestConstants.PAGE);
String author = thePage.getProperty("meta.author");

Je n'ai hélas pas réussit à faire fonctionner ce dernier sous WebSphere 5.1, l'objet Page obtenue est null. Mais normalement, cela est sensé fonctionner

D'un point de vue logique, si l'accès est possible via un scriplet, il doit aussi être possible via une expression EL, bien que non testé ça donnerait le code suivant

4. En utilisant les tags JSP et EL (non testé)
Sélectionnez

<%@ taglib uri="http://www.opensymphony.com/sitemesh/decorator" prefix="decorator" %>
<decorator:usePage id="thePage" />
${thePage.property["meta.author"]}

Tout cela étant équivalent bien sur à l'écriture utilisée à la section III :

Ecriture standard grâce à la taglib
Sélectionnez

<decorator:getProperty property="meta.author" />

V-D. Accéder à la requête de la page décorée depuis un décorateur

L'objet Page contient la requête, en y accédant, on a donc accès à l'objet requête de la page décorée. Sitemesh faisant l'assemblage de la page décorée et des éléments de décorations, dans un décorateur, la requête n'est pas celle de la page décorée. L'accès à la requête se fait en JAVA.

Accéder à la requête décorée
Sélectionnez

import com.opensymphony.module.sitemesh.Page;
import com.opensymphony.module.sitemesh.RequestConstants;
...
Page thePage = request.getAttribute(RequestConstants.PAGE);
HttpServletRequest decoratedRequest = thePage.getRequest()

VI. Bug de la décoration d'une page d'erreur

Un bug est connu au niveau de la décoration d'une page d'erreur. En effet, Sitemesh est basé sur un filtre J2EE, le principe de base d'un filtre J2EE est de faire, par requête HTTP une liste d'action puis de délégué aux autres filtres définie dans l'application (c'est une chaîne de filtre). En cas d'erreur dans les autres filtre, donc, en cas d'erreur dans l'execution de la requête, une exception est levée et on arrive à une page d'erreur qui doit avoir été préalablement définie dans l'application.

Le bug connue (voir le ticket JIRA: http://jira.opensymphony.com/browse/SIM-168) est que Sitemesh considère que la décoration est faites avant l'affichage de la page d'erreur et ne la décore donc pas. Ceci est due au fait que, pour éviter de décorer plusieurs fois la même page, Sitemesh met un attribut dans la requête, celui-ci, en cas d'erreur est mis dans la requête et la redirection vers la page d'erreur gardant la même requête le paramètre est mis dans la requête et empêche la décoration. Ce bug sera normalement corrigé dans la version 2.4 de Sitemesh dont la date de release prévue n'est pas encore communiquée.

Il existe trois solutions pour contourner le problème:

VI-A. Solution 1 : pour Tomcat uniquement

Sous Tomcat uniquement, on peut configurer le filtre Sitemesh de la sorte pour pallier à ce problème

 
Sélectionnez

<filter-mapping>
      <filter-name>sitemesh</filter-name>
      <url-pattern>/*</url-pattern>
      <dispatcher>REQUEST</dispatcher>
      <dispatcher>FORWARD</dispatcher>
      <dispatcher>ERROR</dispatcher>
</filter-mapping>

VI-B. Solution 2 : le filtre Sitemesh

Le bug se situant au niveau du filtre Sitemesh qui met le paramètre 'decoration done' dans la requête avant de passer dans la chaîne de filtre qui génère l'erreur. Il faut étendre le filtre Sitemesh en gérant les erreurs comme ceci:

 
Sélectionnez

public class ErrorHandlingSiteMeshPageFilter extends PageFilter {

    public void doFilter(ServletRequest request, ServletResponse rs, FilterChain chain) 
			throws IOException, ServletException {
        try {
            super.doFilter(request, rs, chain);
        } catch (RuntimeException e) {
            clearFilteredVariable(request);
            throw e;
        } catch(IOException e) {
            clearFilteredVariable(request);
            throw e;
        } catch(ServletException e) {
            clearFilteredVariable(request);
            throw e;
        }
    }

    private void clearFilteredVariable(ServletRequest request) {
        request.setAttribute(FILTER_APPLIED, null);
    }
}

VI-C. Solution 3 : la dernière chance

Si aucune des solutions ci-dessus ne marchent ou ne sont possible chez vous, il suffit de décorer manuellement votre page d'erreur. Dans le cas d'une JSP, ce peut être fait simplement comme cela:

 
Sélectionnez

<g:applyLayout name=?main?>
 // votre page d'erreur ici
</g:applyLayout>

VI. Liens

VIII. Remerciements

Je remercie tout les gens qui m'ont aidés dans la rédaction de cet article et spécialement :

  • Aspic pour sa correction orthographique (il a eu beaucoup de boulot)
  • Wichtounet pour sa correction orthographique et grammaticale (lui aussi a eu beaucoup de boulot)
  • Ricky81 pour ses conseils et sa relecture technique
  • ZedroS pour sa relecture technique très complète