Spring Web Flow

Introduction

Qu'est-ce que Spring Web Flow ?

Spring Web Flow est un sous-projet de Spring Framework. Il permet de définir et d'exécuter des enchaînements de pages dans une application web. Il est utilisable de façon autonome, mais on peut aussi l'intégrer avec un MVC web :

Voici un exemple de web flow concernant la saisie d'une commande :

SWF1.jpg

Dans la suite de cet article, nous verrons comment mettre en oeuvre SWF en version 2, d'une part avec Spring MVC et d'autre part avec JSF.

Description d'un Web Flow

Les web flows sont décrits avec un langage spécifique au format XML (Flow Definition Language, noté FDL par la suite). Comme nous pouvons le remarquer dans l'exemple ci-dessous, un enchaînement consiste en une série d'étapes appelées « états ».

<flow>
 
     <view-state id="saisirProduit">
     </view-state>
 
     <view-state id="saisirEmail">
     </view-state>
 
     <action-state id="enregistrerCommande">
     </action-state>
     ...
</flow>

Remarque : Spring IDE (plugin pour Eclipse) permet de composer visuellement les web flows SWF (cf image ci-dessous). Je n'ai cependant pas réussi à l'utiliser avec Eclipse Galileo. Cette fonctionnalité n'est pas non plus disponible avec Spring STS, bien qu'un assistant de création pour SWF 2 soit proposé.

SWF2.jpg

Principe de SWF

Le web flow représente une conversation avec un utilisateur unique. Il peut être représenté à l'aide d'un diagramme d'état. Voici un exemple :

SWF3.png

Ce qui correspond en syntaxe SWF au code XML suivant :

<flow>
   <view-state id="saisirProduit">
      ...
   </view-state>
   <view-state id="saisirEmail">
      ...
   </view-state>
   <action-state id="enregistrerCommande">
      ...
   </action-state>  
</flow>

Pour passer d'un état à un autre, on génère des évènements, qui permettent de franchir des transitions. Par exemple, un clic sur le bouton «Terminer » génère un évènement qui permet de passer à l'état « Enregistrer la commande ». En langage FLD, une transition est représentée avec l'élément <transition> :

<view-state id="saisirEmail">
   <transition on="terminer" to="enregistrerCommande" />
</view-state>


Notion de View State

Comme son nom le laisse penser, un view state correspond à une page que l'on souhaite afficher. Quand on entre dans un view state, SWF affiche la page correspondant à l'état. Le web flow est alors en pause. Après un certain temps, l'utilisateur génère un événement pour reprendre l'exécution du web flow (submit). En langage FLD, pour associer une page à un view state, on utilise l'attribut view :

<view-state id="saisirProduit" view="/saisirproduit.jsp">
  <transition on="suivant" to="saisirEmail" />
  <transition on="ajouter" to="saisirProduit" />
</view-state>

La plupart du temps, un view state est défini comme l'état de démarrage du web flow. Cet état de démarrage est spécifié avec l'attribut start-state. Si start-state n'est pas défini, l'état de démarrage est le premier état trouvé dans la liste.

<flow start-state="saisirProduit">
  <view-state id="saisirProduit" view="/saisirproduit.jsp">
    <transition on="suivant" to="saisirEmail" />
    <transition on="ajouter" to="saisirProduit" />
  </view-state>
  <view-state id="saisirEmail">
    ...
  </view-state>
  <action-state id="enregistrerCommande">
    ...
  </action-state>
</flow>

Notion d'Action State

L'objectif d'un action state est d'exécuter du code non visuel (i.e. un action state ne produit pas l'affichage d'une page). On peut comparer un action state à la partie contrôleur d'un MVC.

Quand on entre dans un action state, une méthode d'action est exécutée via une EL définie à l'aide de l'élément <evaluate> :

<action-state id="enregistrerCommande">
  <evaluate expression="commandeAction.enregistrerCommande()"/>
  <transition on="ok" to="afficherRecap" />
  <transition on="ko" to="afficherErreur" />
</action-state>

Dans l'exemple ci-dessus, l'EL indique qu'il faut invoquer la méthode enregistrerCommande() sur le bean nommé commandeAction. Ce bean est tout simplement un bean Spring :

@Component("commandeAction")
public class CommandeAction {
   public String enregistrerCommande() {
    ...
    commandeService.createCommande(commande);
    return "ok";
   }
}

Il est possible de passer des variables aux méthodes d'action. Dans l'exemple ci-dessous, on créé une variable nommée commandeForm qui sera passé en paramètre de la méthode enregistrerCommande().

<flow>            
   <var name="commandeForm" class="org.librairie.web.CommandeForm"/>
   ...
   <action-state id="enregistrerCommande">
     <evaluate expression="commandeAction.enregistrerCommande(commandeForm)"/>
     <transition on="ok" to="afficherRecap" />
     <transition on="ko" to="afficherErreur" />
   </action-state>
   ...
</flow>

Remarque : par défaut, la variable est placée dans la portée flow scope.

public String enregistrerCommande(CommandeForm form) {
 ...
}

Il existe aussi des variables implicites définies par SWF telle que flowRequestContext :

<evaluate expression="action.enregistrerCommande(form, flowRequestContext)"/>

Cette variable peut être utilisée par exemple dans le code de l'action Java pour placer un résultat dans le flow scope :

public String enregistrerCommande(CommandeForm form, RequestContext context) {
   ...
   commandeService.createCommande(commande);
   context.getFlowScope().put("commande", commande);
   ...
}

La page web affichée après l'exécution de l'action pourra alors accéder à la commande placée dans le flow scope.

Notion d'End State

Un end state permet de définir la page à afficher lorsque le web flow se termine. Lorsque le web flow passe dans un end state, les données liées à l'instance du web flow en cours ne sont plus accessibles.

En langage FLD, un end state se définit avec l'élément ... <end-state> :

<flow>
  ...	
  <end-state id="afficherRecap" view="/recap.jsp" />
</flow>

Exécuter une action "on start"

Une action "on start" est invoquée au démarrage du web flow, afin d'initialiser des variables utilisées par la suite dans le web flow. Dans notre exemple, la liste des produits est chargée au démarrage du web flow afin d'alimenter la liste déroulante de la page "saisirProduit" :

SWF4.png

Dans l'exemple suivant, l'action "on start" invoque une action qui retourne une liste de produits dont le résultat est placé dans le flow scope :

<flow start-state="saisirProduit">
   <on-start>
       <evaluate expression="commandeAction.findProduit()" result="flowScope.produits"/>
   </on-start>

   <view-state id="saisirProduit" view="/saisirproduit.jsp">
      ...
   </view-state>
   ...
 </flow>

Exécuter une action "on render"

Le principe d'une action "on render" consiste à exécuter du code Java avant l'affichage de la vue. Voici donc une autre façon d'initialiser la liste des produits pour notre formulaire :

<view-state id="saisirProduit" view="/saisirproduit.jsp">
   <on-render>
     <evaluate expression="commandeAction.findProduit()" result="viewScope.produits"/>
   </on-render>
   <transition on="ajouter" to="saisirEmail"/>
</view-state>

Remarque : le view scope utilisé dans le code ci-dessus n'a de sens qu'avec un view state

D'autres déclenchements d'action sont possibles dans un « view state » :

Configuration Spring

Pour utiliser SWF dans une application, il faut demander au conteneur Spring de démarrer le moteur SWF. Il suffit pour cela de configurer un fichier d'application context, en utilisant les balises SWF :

<beans>

    <webflow:flow-executor id="flowExecutor"/>

    <webflow:flow-registry id="flowRegistry" flow-builder-services="flowBuilderServices">
       <webflow:flow-location id="flowCommande" path="/WEB-INF/webflow-commande.xml"/>
       <webflow:flow-location id="flow2" path="/WEB-INF/flow2.xml"/>
    </webflow:flow-registry>
    ...
</beans>

Intégration avec Spring MVC

Configuration Spring

Afin de pouvoir utiliser SWF avec Spring MVC, il faut compléter le fichier d'application context de Spring pour notre application. La configuration minimale impose la déclaration des beans suivants : « flow builder services », « flow handler adapter » et « flow handler mapping ».

<beans>
   ...  
   <webflow:flow-builder-services id="flowBuilderServices" />	

   <bean class="org.springframework.webflow.mvc.servlet.FlowHandlerAdapter">
      <property name="flowExecutor" ref="flowExecutor" />
   </bean>
   <bean class="org.springframework.webflow.mvc.servlet.FlowHandlerMapping">
      <property name="flowRegistry" ref="flowRegistry"/>
      <property name="order" value="0"/>
   </bean>
	
</beans>

Associer un view state et une JSP

Dans le fichier de description du web flow, l'attribut « view » permet de spécifier la JSP à laquelle un view state correspond :

<view-state id="saisirProduit" view="/saisirproduit.jsp">
   ...
</view-state>

On peut aussi exploiter le "view resolver" de Spring MVC (ce qui permet de ne pas mettre le nom des fichiers JSP dans le document de définition du web flow). A cette fin, il faut déclarer un "view factory creator" dans la configuration Spring :

<webflow:flow-builder-services id="flowBuilderServices" view-factory-creator="viewFactoryCreator"/>
<bean id="viewFactoryCreator" class="org.springframework.webflow.mvc.builder.MvcViewFactoryCreator">
   <property name="viewResolvers" ref="viewResolver"/>
</bean>
<view-state id="saisirProduit" view="saisirproduit">
   ...
</view-state>

Ecriture des JSP : invoquer un web flow

L'url à utiliser dépend du flow id et de l'url pattern de la dispatcher servlet de Spring MVC.

<servlet-mapping>
   <servlet-name>librairie</servlet-name>
   <url-pattern>*.do</url-pattern>
</servlet-mapping>
<webflow:flow-location id="flowCommande" path="/WEB-INF/webflow-commande.xml"/>
<a href="flowCommande.do">Passer une Commande</a>

<form action="flowCommande.do">
   <input type="submit" value="Passer une Commande"/>
</form>

Ecriture des JSP : émettre un évènement

<form>
   <input type="submit" name="_eventId_suivant" value="Suivant"/>
</form>

<form>
   <input type="submit" value="Suivant" />
   <input type="hidden" name="_eventId" value="suivant" />
</form>

<a href="${flowExecutionUrl}&_eventId=suivant">Suivant</a>

Ecriture des JSP : formulaires

Pour écrire le formulaire, on utilise les tags de Spring MVC. Ce formulaire est lié à un objet "model" instancié par le webflow, dont la classe contient les champs des différents formulaires du web flow.

<form:form modelAttribute="commandeForm">
   Produit  : <form:select path="idProduit">...</form:select>
   Quantité : <form:input path="quantite" />
   <input type="submit" name="_eventId_ajouter" value="Ajouter"/>
</form:form>
<flow>                          
   <var name="commandeForm" class="org.librairie.web.CommandeForm"/>
   ...
</flow>
public class CommandeForm implements Serializable {
     private String idProduit;
     private int quantite;
     private String email;

     // getters et setters
     ...
}

Traitement des formulaires

Les view states assurent le traitement des formulaires après le submit. Ils assurent en effet le transfert des données saisies par l'utilisateur dans l'objet "model".

<form:form modelAttribute="commandeForm">
   ...
</form:form>
<view-state id="saisirProduit" model="commandeForm" view="saisirproduit.jsp">
   <binder>
      <binding property="idProduit"/>
      <binding property="quantite"/>
   </binder>
   <transition on="ajouter" to="saisirEmail"/>
</view-state>

Intégration avec JSF

SWF permet d'intégrer JSF uniquement dans le cas où l'on utilise Facelets (i.e. pages .xhtml). Les backing beans et les règles de navigation sont pris en charge par SWF. Il n'y a donc pas de configuration dans faces-config.xml

Configuration web.xml

En plus de la configuration standard JSF + Facelets, il faut déclarer la servlet de Spring :

<servlet>
   <servlet-name>SpringServlet</servlet-name>
   <servlet-class>
      org.springframework.web.servlet.DispatcherServlet
   </servlet-class>
   <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value></param-value>
   </init-param>
   <load-on-startup>2</load-on-startup>
</servlet>

<servlet-mapping>
   <servlet-name>SpringServlet</servlet-name>
   <url-pattern>/spring/*</url-pattern>
</servlet-mapping>

Configuration Spring

La configuration minimale impose la déclaration des beans suivants : "flow builder services", "flow handler adapter" et "flow handler mapping".

<beans>
   ...
   <faces:flow-builder-services id="flowBuilderServices"/>	
   <bean class="org.springframework.webflow.mvc.servlet.FlowHandlerAdapter">
      <property name="flowExecutor" ref="flowExecutor" />
   </bean>
   <bean class="org.springframework.webflow.mvc.servlet.FlowHandlerMapping">
      <property name="flowRegistry" ref="flowRegistry" />
      <property name="defaultHandler">
	      <bean class="org.springframework.web.servlet.mvc.UrlFilenameViewController"/>
	   </property>
   </bean>
</beans>

Associer un view state et une page JSF

Dans le fichier de descrption du web flow, l'attribut « view » spécifie la page .xhtml à laquelle le view state correspond.

<view-state id="saisirProduit" view="/saisirproduit.xhtml">
   ...
</view-state>

Remarque : Contrairement à Spring MVC, il n'existe pas de "view resolver" simple pour s'affranchir de l'extension .xhtml dans l'attribut view

Pages XHTML : invoquer un web flow

L'url à utiliser dépend du flow id et de l'url pattern de la dispatcher servlet de Spring MVC.

<webflow:flow-location id="flowCommande" path="/WEB-INF/webflow-commande.xml"/>
<servlet-mapping>
   <servlet-name>SpringServlet</servlet-name>
   <url-pattern>/spring/*</url-pattern>
</servlet-mapping>
<a href="${request.contextPath}/spring/flowCommande">Passer une Commande</a>

<form action="${request.contextPath}/spring/flowCommande">
   <input type="submit" value="Passer une Commande"/>
</form>

Pages XHTML : émettre un évènement

L'attribut "action" des composants de commande permet de spécifier l'évènement à générer.

<h:commandButton id="btnSuivant" value="Suivant" action="suivant"/>

<h:commandLink id="btnSuivant" value="Suivant" action="suivant"/>
<view-state id="saisirProduit" view="/saisirproduit.xhtml">
   ...
   <transition on="suivant" to="saisirEmail"/>
</view-state>

Pages XHTML : formulaires

<h:form>
   <h:outputLabel for="cboProduit" value="Produit : "/>
   <h:selectOneMenu id="cboProduit" value="#{flowScope.commandeForm.idProduit}">
      <f:selectItems value="#{produits}"/>
   </h:selectOneMenu>
   <h:outputLabel for="txtQuantite" value="Quantité : "/>
   <h:inputText id="txtQuantite" value="#{flowScope.commandeForm.quantite}"/>
   <h:commandButton value="Suivant" action="suivant"/>
</h:form>

Remarque : Les EL JSF ont accès au "flowScope" de SWF

<flow>                          
   <var name="commandeForm" class="org.librairie.web.CommandeForm"/>
   ...
</flow>