Intégration de OSWorkflow avec les frameworks Spring IoC et Acegi

Publié dans: 

OSWorkflow se propose comme l’un des choix les plus importants de systèmes de workflow pour les développeurs Java. Offrant une flexibilité importante, le framework peut s’intégrer facilement aussi bien avec un code existant, qu’avec d’autres frameworks Java. Facilement…à condition d’en connaitre les rouages, parce que s’il y a un point faible à OSWorkflow, c’est bien la documentation ! L’objectif de cet article est de montrer l’integration de OSWorkflow avec Spring IoC et Acegi (Spring Security).

Cet article a été publié dans le numéro actuel (Octobre 2009) du magazine « PROgrammez ». La version PDF de l’article est mise à votre disposition en téléchargement gratuit.

La configuration basique de OSWorkflow

La configuration par défaut de OSWorkflow repose sur un fichier xml nommé osworkflow.xml (et qui devrait être présent sous la racine du projet ou sous le répertoire META-INF). La structure de celui-ci devra comprendre deux choses essentielles : la classe de persistance et la classe de la factory. La classe de persistance est la classe qui gère la persistance des instances de workflows, des actions, des étapes…alors que la factory gère les descripteurs qui modélisent le workflow. Ci-dessous un exemple de fichier de configuration où la persistance se fait en mémoire (qui n’est bien entendu utile qu’en cas de tests) et où la factory lit (et éventuellement écrit) les modèles à partir de descriptions dans des fichiers XML.

<osworkflow>
   <persistence class="com.opensymphony.workflow.spi.memory.MemoryWorkflowStore"/>
   <factory class="com.opensymphony.workflow.loader.XMLWorkflowFactory">
      <property key="resource" value="workflows.xml" />
   </factory>
</osworkflow>

En partant de ce type de configuration, le point d’entrée dont on dispose pour utiliser OSWorkflow depuis notre code, est d’instancier une classe qui implémente l’interface com.opensymphony.workflow.Workflow et qui, généralement, hérite de com.opensymphony.workflow.AbstractWorkflow. La classe fournie la plus basique (comme son nom l’indique) est BasicWorkflow, qu’on peut utiliser ainsi :

Workflow workflow = new BasicWorkflow("UtilisateurCourant");
DefaultConfiguration config = new DefaultConfiguration();
workflow.setConfiguration(config);

Inconvénients de la configuration basique

Les inconvénients d’une telle utilisation sont assez clairs. En effet, l’instance du workflow qu’on a créé est associé à l’utilisateur « UtilisateurCourant ». Et comme toute application de workflow « sérieuse » fait intervenir plusieurs utilisateurs, on aura au moins une instance par utilisateur. Le deuxième inconvénient se rapporte au fait qu’il sera « lourd » de gérer un point unique et global depuis lequel on invoque les instances du workflow pour lancer des requêtes ou effectuer des actions. C’est pour cela qu’on propose de définir un bean Spring unique dans l’application, qu’on pourra injecter dans les services qui en auront besoin et dont la récupération de l’utilisateur en cours (connecté) se fait de manière transparente à partir du contexte Acegi.

Implémentation Acegi du contexte workflow

Pour mieux appréhender le problème d’integration de Acegi, une compréhension du mécanisme interne de récupération de l’utilisateur (nommé “Caller” dans le vocabulaire de OSWorkflow) s’impose. En fait, la classe AbstractWorkflow récupère le caller à partir d’un service implémentant l’interface com.opensymphony.workflow.WorkflowContext en invoquant la méthode getCaller de celui-ci. Nous allons donc développer notre propre classe de contexte (qui implémente com.opensymphony.workflow.WorkflowContext) et qui renvoie le caller à partir d’un IUserService (un service utilisateur de base), et notre propre classe de workflow (qui hérite de AbstractWorkflow) qui supporte l’injection d’un contexte de façon dynamique.

public class IPTechWorkflow extends AbstractWorkflow {

public IPTechWorkflow() {
}

public void setContext(WorkflowContext wfctx)
{
   super.context = wfctx;
}

}
public class ConnectedUserWorkflowContext implements WorkflowContext{

private IUserService userService = null;

public ConnectedUserWorkflowContext() {
}
public String getCaller() {
   return userService.getConnectedUserInfo().getUsername();
}
public void setRollbackOnly() {
   // Gestion de Rollback non couverte dans cet article
}
}

Nous avons, dans ce code, supposé que l’interface IUserService dispose d’une méthode getConnectedUserInfo() et qui renvoie une instance d’une classe “User”.

Une des avantages de ce type d’integration est qu’il est complétement décorrélé du système d’authentification (remarquez qu’on n’a pas encore évoqué Acegi). Il nous suffit donc, à présent, de fournir une implémentation de l’interface IUserService et qui interroge le contexte Acegi pour récupérer l’utilisateur connecté :

public User getConnectedUserInfo() {

   Object objt = org.acegisecurity.context.SecurityContextHolder.
getContext().getAuthentication().getPrincipal();

   return (User)objt;

}

Définition des beans de configurations

Définissons à présent, les beans Spring qu’on injectera :

  • Le bean du service utilisateur qui récupère l’utilisateur à partir de Acegi (implémenté par une classe qu’on appellera AcegiUserServiceImpl)
<bean id="userService"
class="com.iptech.programmez.authentication.AcegiUserServiceImpl">

</bean>
  • Le bean du contexte workflow qui renvoie un caller à partir du service utilisateur
<bean id="workflowContext"
class="com.iptech.programmez.workflow.ConnectedUserWorkflowContext">
   <property name="userService" ref="userService"/>
</bean>
  • Le bean du workflow qui permettra l’injection d’un contexte
<bean id="workflow"
class="com.iptech.programmez.workflow.IPTechWorkflow">
   <property name="context" ref="workflowContext"/>
</bean>

Nous allons, maintenant, traiter la configuration de la factory et de la persistance avec Spring. Rien ne nous empêche, bien entendu, de laisser les choses comme elles le sont, et définir cette configuration dans notre osworkflow.xml, mais on aurait dans ce cas deux fichiers de configuration distincts. Le but étant de définir un bean pour la factory et un bean pour la persistance et de les injecter directement dans notre bean de workflow.

La distribution de OSWorkflow vient avec la classe com.opensymphony.workflow.config.SpringConfiguration qui est une classe de définition de configuration, qui ne lit pas à partir de osworkflow.xml (comme la configuration par défaut), mais qui nous permet d’injecter directement des beans (elle expose des setteurs pour ceux-ci). L’injection de la configuration devient alors directe.

<bean id="workflow"
class="com.iptech.programmez.workflow.IPTechWorkflow">
   <property name="context" ref="workflowContext"/>
   <property name="configuration">
      <ref local="osworkflowConfiguration"/>
   </property>
</bean>

<bean id="osworkflowConfiguration"
class="com.opensymphony.workflow.config.SpringConfiguration">
   <property name="store">
      <ref local="idBeanDePersistance"/>
   </property>
   <property name="factory">
      <ref local="idBeanDeFactory"/>
   </property>
</bean>

Déclaration du resolver permettant l’utilisation de beans Spring

Nous allons à présent aborder un autre aspect de l’integration de OSWorkflow avec Spring, et qui porte sur l’utilisation de beans Spring comme des pré-fonctions, des post-fonctions et des conditions sur les actions du workflow. En effet, lorsque nous modélisons les étapes et les actions de notre workflow, nous avons la possibilité d’indiquer au moteur de workflow qu’une condition, ou une fonction est implémenté dans un bean Spring plutôt que de lui indiquer directement le nom de la classe. Ceci se fait en injectant au bean du workflow ce qu’on appelle un « resolver ». Le resolver est utilisé par le moteur de workflow pour résoudre les types des conditions et fonctions. Dans notre cas, on s’intéresse à un resolver qui sait interpréter le type « bean spring » et qui récupère celui-ci pour l’invoquer. Un tel resolver est inclus dans la distribution de OSWorkflow, il s’agit de com.opensymphony.workflow.util.SpringTypeResolver qui reconnait les conditions et les fonctions de type « spring » (en minuscule) et invoque le bean dont l’id est indiqué comme valeur de l’argument « bean.name ». On pourra donc, dans notre modélisation, utiliser le bout suivant :

<restrict-to>
   <conditions>
      <condition type="spring">
         <arg name="bean.name">allowOwner</arg>
      </condition>
   </conditions>
</restrict-to>

Et il ne nous reste que d’indiquer au moteur d’utiliser le resolver approprié :

<bean id="workflow"
class="com.iptech.programmez.workflow.IPTechWorkflow">
   <property name="context" ref="workflowContext"/>
   <property name="configuration">
      <ref local="osworkflowConfiguration"/>
   </property>
   <property name="resolver" ref="workflowTypeResolver"/>
</bean>

<bean id="workflowTypeResolver"
class="com.opensymphony.workflow.util.SpringTypeResolver" />

Nous arrivons à la fin de cet article où on a pu mettre en place un bean Spring de workflow qu’on pourra injecter dans nos classes de services, où la récupération de l’utilisateur connecté se fait de manière transparente à partir du contexte Acegi.