Tutoriel - Convertir une Application Métier en Portlet JSR 286

1. Introduction

L’objectif de ce tutoriel est donc de décrire les différentes étapes à réaliser pour obtenir une Portlet JSR 286 à partir d’une application web, et de l’intégrer ensuite dans le portail de JCMS.

L’exemple présenté dans ce tutoriel est une application web permettant d’afficher une liste de personnes, de créer de nouvelles personnes, d’éditer ou de supprimer des personnes.

Vous trouverez ci-dessous les exemples utilisés dans le tutoriel :

Il s’agit d’une application web de type CRUD (CREATE READ UPDATE and DELETE) utilisant les technologies suivantes :

  • Spring MVC avec Annotations
  • JSP
  • Tiles
  • JPA/Hibernate
  • Internationalisation (I18N)
  • Maven
  • Tomcat

2. Pré-requis

TOMCAT_HOME : répertoire racine de l'installation de Tomcat.

3. L'Application Web

3.1 Description

L'Application Web s'appelle simple-spring-webapp.

Voici une brève description du contenu de l’application web : 

webapp-structure

3.2 Déployer l’application web

Déployer l’Application Web dans Tomcat afin de vérifier le bon fonctionnement.

  • Créer le war avec Maven
    • Ouvrir une fenêtre de commande
    • Se mettre à la racine du répertoire du projet
    • Taper mvn clean package
  • Se rendre dans le répertoire target
  • Copier le fichier simple-spring-webapp.war
  • Coller le fichier dans le répertoire TOMCAT_HOME/webapps
  • Démarrer Tomcat (TOMCAT_HOME/bin/startup.bat)
  • Ouvrir un navigateur Internet et aller à l’adresse http://localhost:8080/simple-spring-webapp

portlet-overview

A gauche, le menu permet de naviguer dans l’application pour afficher, ajouter, modifier, supprimer des personnes.

Au centre, le contenu affiché selon l’action réalisée.

En bas à droite, un menu permet de choisir la langue du site.

4. Créer la Portlet JSR 286

Pour créer la Portlet JSR 286, dupliquer le projet de l’Application Web pour réaliser les modifications dans un projet séparé.

4.1 Le POM Maven

Modifier le fichier pom.xml afin de créer un fichier war avec nom différent, et ajouter les librairies nécessaires au bon fonctionnement de la Portlet.

  1. Modifier l’artifactId : 

    AVANT

    APRÈS

    <artifactId>simple-spring-webapp</artifactId>
    <artifactId>simple-spring-portlet</artifactId>
  2. Ajouter les dépendances suivantes : 
    <dependency>
    <groupId>javax.portlet</groupId>
    <artifactId>portlet-api</artifactId>
    <version>2.0</version>
    <scope>provided</scope>
    </dependency>

    JAR qui sera présent dans Tomcat et nécessaire pour le fonctionnement des Portlets

    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc-portlet</artifactId>
    <version>${spring.framework.version}</version>
    </dependency>

    JAR utilisé dans notre exemple car la webapp utilise le framework Spring MVC.

    Dans d’autres cas, il faut vérifier s’il n’existe pas des JARs jouant le rôle de pont (Bridge) limitant les modifications au sein de l’application web. Voir http://portals.apache.org/bridges/

    <dependency>
    <groupId>org.apache.tiles</groupId>
    <artifactId>tiles-portlet</artifactId>
    <version>2.2.2</version>
    </dependency>

    JAR nécessaire pour l’utilisation de Tiles dans les Portlets

4.2 Le fichier portlet.xml

Le fichier portlet.xml représente le descripteur de/des Portlet(s) contenues dans l'application.

Dans notre cas, nous allons définir qu’une seule Portlet, simple-spring-portlet.

Elle utilise la classe DispatcherPortlet qui envoie les requêtes aux Contrôleurs Spring, tout comme le DispatcherServlet fait dans le cadre d’une application web. Le DispatcherPortlet expose la localisation courante de la même façon que DispatcherServlet, ce qui permet de mettre en place l’internationalisation et d’ajouter le resource bundle utilisé.

<?xml version="1.0" encoding="UTF-8"?>
<portlet-app xmlns="http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.0"
xsi:schemaLocation="http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd">
<portlet>
<!-- The Portlet name -->
<portlet-name>simple-spring-portlet</portlet-name>
<!-- Custom DispatcherPortlet to add custom JS et CSS files to the Portlet -->
<portlet-class>org.springbyexample.web.portlet.MyDispatcherPortlet</portlet-class>
<supports>
<mime-type>text/html</mime-type>
<portlet-mode>view</portlet-mode>
</supports>
<resource-bundle>messages</resource-bundle>
<portlet-info>
<title>Simple Spring Portlet</title>
<portlet-info>
</portlet>
</portlet-app>

Dans notre exemple, nous utilisons la classe MyDispatcherPortlet qui étend DispatcherPortlet qui va nous permettre d'ajouter nos propres fichiers CSS et JS.

public class MyDispatcherPortlet extends DispatcherPortlet {

@Override
protected void doHeaders(RenderRequest request, RenderResponse response) {
super.doHeaders(request, response);
PortalContext portalContext = request.getPortalContext();
if (portalContext.getProperty(PortalContext.MARKUP_HEAD_ELEMENT_SUPPORT) != null) {
Element cssElement = response.createElement("link");
cssElement.setAttribute("href", response.encodeURL(request.getContextPath() + "/css/main.css"));
cssElement.setAttribute("rel", "stylesheet");
cssElement.setAttribute("type", "text/css");
response.addProperty(MimeResponse.MARKUP_HEAD_ELEMENT, cssElement);
}
}
}

4.3 Le fichier web.xml

Le fichier web.xml d’une Portlet représente le descripteur de déploiement d’une application web.

Il doit être modifié afin de fonctionner avec le portail de JCMS.

Nous devons ajouter :

  1. Une servlet utilisant la classe PortletServlet, et le mapping de cette Servlet. Ceci est obligatoire, quel que soit la Portlet créée, si la portlet doit fonctionner dans JCMS.
  2. Une servlet utilisant la classe ViewRendererServlet, et le mapping de cette Servlet. Nous ajoutons cette Servlet car notre exemple utilise le framework Spring. En utilisant cette servlet, la requête de portlet est convertie en une requête de servlet et la vue peut être rendue en utilisant la totalité de l'infrastructure de servlet normale. Cela signifie que toutes les fonctionnalités de rendu existantes, tels que JSP, Velocity, etc, peuvent être utilisés dans la portlet.

Dans notre exemple, nous ajoutons donc les servlets nommées simple-spring-portlet et view-servlet : 

<?xml version="1.0" encoding="UTF-8"?>
<web-app ...>
<display-name>simple-spring-portlet</display-name>
. . .
<servlet>
<servlet-name>view-servlet</servlet-name>
<servlet-class>org.springframework.web.servlet.ViewRendererServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>

<servlet>
<servlet-name>simple-spring-portlet</servlet-name>
<servlet-class>org.apache.pluto.container.driver.PortletServlet</servlet-class>
<init-param>
<param-name>portlet-name</param-name>
<param-value>simple-spring-portlet</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
. . .
<servlet-mapping>
<servlet-name>view-servlet</servlet-name>
<url-pattern>/WEB-INF/servlet/view</url-pattern>
</servlet-mapping>

<servlet-mapping>
<servlet-name>simple-spring-portlet</servlet-name>
<url-pattern>/PlutoInvoker/simple-spring-portlet</url-pattern>
</servlet-mapping>
. . .
</web-app>

4.4 Le fichier [portlet-name]-portlet.xml

Ce fichier est utilisé par le framework Spring qui, lors de l’initialisation du DispatcherPortlet, va chercher un fichier nommé [portlet-name]-portlet.xml dans le répertoire WEB-INF de l’application web et créer les beans définis.

Dans notre exemple le fichier se nommera simple-spring-portlet-portlet.xml.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util-3.0.xsd
">

<!-- Package scanné par Spring pour initialiser les composants (Controller, Services …) -->
<context:component-scan base-package="org.springbyexample.web.servlet.mvc" />

<!-- Initialise le Resource Bundle pour l’internationalisation -->
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basenames">
<list>
<value>messages</value>
</list>
</property>
</bean>

<!--
Tiles initializing..
We created DynamicTilesView.java and DynamicTilesViewProcessor.java to use the context of the portlet.
-->

<bean id="tilesConfigurer"
class="org.springframework.web.servlet.view.tiles2.TilesConfigurer"
p:definitions="/WEB-INF/tiles-defs/templates.xml" />

<bean id="tilesViewResolver"
class="org.springframework.web.servlet.view.InternalResourceViewResolver"
p:viewClass="org.springbyexample.web.servlet.view.DynamicTilesView"
p:prefix="/WEB-INF/jsp/"
p:suffix=".jsp" />

</beans>

Ca fichier définit le bean tilesViewResolver qui n'utilise pas les mêmes classes que le bean éponyme défini dans le fichier webmvc-context.xml.

Nous avons créé une classe DynamicTilesView qui s'inspire de la classe org.springbyexample.web.servlet.view.tiles2.DynamicTilesView définie dans le fichier webmvc-context.xml, afin d'utiliser le contexte de la portlet et ainsi pouvoir récupérer correctement l'objet TilesContainer.

public class DynamicTilesView extends InternalResourceView {
...
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
PortletConfig portletConfig = (PortletConfig) request.getAttribute("javax.portlet.config");
PortletContext portletContext = portletConfig.getPortletContext();
RenderRequest renderRequest = (RenderRequest) request.getAttribute("javax.portlet.request");
RenderResponse renderResponse = (RenderResponse) request.getAttribute("javax.portlet.response");
TilesContainer container = PortletUtil.getCurrentContainer(renderRequest, portletContext);
if (container == null) { throw new ServletException("Tiles container is not initialized. " + "Have you added a TilesConfigurer to your web application context?");
}
exposeModelAsRequestAttributes(model, request);

dynamicTilesViewProcessor.renderMergedOutputModel(getBeanName(), getUrl(), portletContext, renderRequest, renderResponse, container);
}
}

4.5 Le Contrôleur

La classe servant de Contrôleur dans notre exemple est la classe nommée PersonController.java.

Il est nécessaire de la modifier car le fonctionnement du mapping des URLs est différent entre une Application Web et une Portlet.

  • L’Application Web gère des URLs selon la méthode http envoyé par la requête (GET, POST, DELETE …)
  • La portlet gère des URLs de type render ou action en conjonction avec un mode (VIEW, EDIT …)

Il est donc nécessaire de modifier les mappings. Pour cela, nous sommes obligés d’utiliser des annotations apportées par la librairie spring-webmvc-portlet.jar.

  1. @RenderMapping pour l’affichage d’une page
  2. @ActionMapping dans le cas d’une soumission d’un formulaire

Note : Ces modifications sont spécifiques aux frameworks utilisés. Il est possible que pour certains frameworks, il ne soit pas nécessaire de modifier les Contrôleurs car des librairies servant de pont (Bridge) peuvent éviter ces changements.

@Controller
@RequestMapping(value = "VIEW")
public class PersonController {

private static final String SEARCH_VIEW_KEY = "/person/search";
private static final String SEARCH_MODEL_KEY = "persons";

private final PersonRepository repository;

@Autowired
public PersonController(PersonRepository repository) {
this.repository = repository;
}

/**
* For every request for this controller, this will
* create a person instance for the form.
*/
@ModelAttribute
public Person newRequest(@RequestParam(required = false) Integer id) {
return (id != null ? repository.findOne(id) : new Person());
}

/**
* <p>Person form request.</p>
* <p>Expected HTTP GET and request '/person/form'.</p>
*/
@RenderMapping
public String showHome(RenderResponse response) {
return "index";
}

/**
* <p>Saves a person.</p>
* <p>Expected HTTP POST and request '/person/form'.</p>
*/
@ActionMapping(params = "myaction=save")
public void form(Person person, Model model) {
if (person.getCreated() == null) {
person.setCreated(new Date());
}

Person result = repository.saveAndFlush(person);

model.addAttribute("statusMessageKey", "person.form.msg.success");

response.setRenderParameter("myaction", "save");
}

@RenderMapping(params = "myaction=save")
public String showForm(Person person, Model model) {
return "person/form";
}

/**
* <p>Deletes a person.</p>
* <p>Expected HTTP POST and request '/person/delete'.</p>
*/
@ActionMapping(params = "myaction=delete")
public void delete(Person person, ActionResponse response) {
repository.delete(person);

// clear ModelMap to prevent it from being stored as an ImplicitModel.
modelMap.clear();

response.setRenderParameter("myaction", "search");
}

/**
* <p>Searches for all persons and returns them in a
* <code>Collection</code>.</p>
* <p>Expected HTTP GET and request '/person/search'.</p>
*/
@ModelAttribute(value = SEARCH_MODEL_KEY)
public Collection<Person> search() {
return repository.findAll();
}

@RenderMapping(params = "myaction=search")
public String showSearch() {
return SEARCH_VIEW_KEY;
}
}

4.6 LES JSPs

La Portlet étant intégrée dans un portail, plusieurs modifications sont à prévoir au niveau des JSPs.

Suppression de balises

La Portlet va être ajoutée au sein d'une page HTML. Il est donc nécessaire de supprimer les balises <html>, <head><body>.

Dans notre exemple, ces balises se trouvent seulement dans le fichier de template main.jsp utilisé par tiles.

Réécriture des URLs

A la différence d’une Servlet, une Portlet n'a pas d’URL attachée à elle de sorte qu'elle ne peut pas y accéder directement. L'accès se fait uniquement par la page du portail qui détient la Portlet.

L’API de Portlet propose des tags qui permettent de construire des URLs de Portlet.

  • <portlet:actionURL> : crée une URL qui pointe vers le portlet courant et déclenche une demande d'action avec les paramètres fournis. Nous utilisons ce tag pour les URLs provoquant des modifications (Ajout, Suppression, Modification…)
  • <portlet:renderURL> : crée une URL qui pointe vers le portlet courant et déclenche une requête de rendu avec les paramètres fournis. Nous utilisons ce tag pour les URLs ne servant qu’à afficher des pages sans provoquer de modifications

Dans le Contrôleur, les méthodes avec @ActionMapping seront appelés par les URLs utilisant le tag <portlet:actionURL>, et les méthodes avec @RenderMapping seront appelés par les URLs utilisant le tag <portlet:renderURL>.

Par exemple, si on regarde la JSP /src/main/webapp/WEB-INF/jsp/person/search.jsp, nous avons les modifications des URLs de la manière suivante : 

AVANT

APRÈS

<c:url var="editUrl" value="/person/form.html">
<c:param name="id" value="${person.id}" />
</c:url>
<portlet:renderURLvar="editUrl">
<portlet:paramname="myaction"value="save"/>
<portlet:paramname="id"value="${person.id}"/>
</portlet:renderURL>
<c:url var="deleteUrl" value="/person/delete.html"/>
<portlet:actionURLvar="deleteUrl">
<portlet:paramname="myaction"value="delete"/>
<portlet:paramname="id"value="${person.id}"/>
</portlet:actionURL>

 

Il faut donc modifier les URLs des autres pages de la même manière.

Note : Pour certains Framework, Il existe des librairies de pont (Bridge) qui permettent d’éviter cette réécriture.

Javascript / CSS

L’application web a une feuille de style, /css/main.css. Pour pouvoir conserver le style, il faut pouvoir indiquer au portail JCMS de charger cette feuille de style.

Pour cela, nous devons étendre la classe org.springframework.web.portlet.DispatcherPortlet pour implémenter la méthode doHeaders dans laquelle on déclare les fichiers Javascript et CSS à charger.

public class MyDispatcherPortlet extends DispatcherPortlet {

@Override
protected void doHeaders(RenderRequest request, RenderResponse response) {
super.doHeaders(request, response);
PortalContext portalContext = request.getPortalContext();
if (portalContext.getProperty(PortalContext.MARKUP_HEAD_ELEMENT_SUPPORT) != null) {
Element cssElement = response.createElement("link");
cssElement.setAttribute("href", response.encodeURL(request.getContextPath() + "/css/main.css"));
cssElement.setAttribute("rel", "stylesheet");
cssElement.setAttribute("type", "text/css");
response.addProperty(MimeResponse.MARKUP_HEAD_ELEMENT, cssElement);
}
}
}

Il faut ensuite déclarer cette classe dans le fichier portlet.xml.

Note : Il est préférable de minimiser l’utilisation du Javascript étant donné que la Portlet va être intégrée dans un portail dont elle ne connait pas forcément le Javascript déjà présent. Pour le CSS, il en est de même, surtout si on souhaite avoir une Portlet qui utilise le CSS du portail afin d’être homogène avec les autres éléments de la page.

4.7 Déployer la Portlet dans Tomcat

Veuillez suivre la section Déployer une Portlet JSR 286 sur le serveur d’application de la documentation du module JSR 286.

4.8 Ajouter la Portlet dans JCMS

Veuillez suivre la section Ajouter une Portlet Conteneur JSR 286 dans JCMS de la documentation du module JSR 286.

5. Conclusion

Nous avons vu dans ce tutoriel un exemple concret, basé sur Spring MVC, pour convertir une Application Web en Portlet.

Dans le cas d’un autre Framework, il est nécessaire de se documenter car il existe des spécificités selon le Framework choisi, et il peut exister des librairies facilitant la conversion, tel que les librairies de pont (Bridge, http://portals.apache.org/bridges/), ou des libraires de tags (ex : Struts tags)

Vous pourrez trouver ci-dessous des documentations expliquant comment modifier une application web basé sur d’autres Frameworks en Portlet :

Références

En résumé...

Dans le cadre de la sortie du module JSR 286, un tutoriel a été réalisé afin de décrire les étapes nécessaires pour convertir une Application Web en Portlet JSR 286. Vous trouverez ci-dessous les exemples utilisés dans le tutoriel : * [[jc_80554][simple-spring-webapp.zip]] : Le ZIP du projet de l'application métier * [[jc_80553][simple-spring-portlet.zip]] : le ZIP du projet de la Portlet obtenue à partir de l'application métier * [[jc_131104][simple-spring-webapp.war]] le WAR de l'application métier * [[jc_131103][simple-spring-portlet.war]] : le WAR de la Portlet

Sujet
Produits
Publié

29/01/14

Rédacteur
  • Nicolas Dos Santos