Iniciación a OSWorkflow con Spring

0
14253

Iniciación a OSWorkflow con Spring

Índice de contenido

Introducción.

En prácticamente todos los proyectos siempre nos encontramos con alguna entidad que pasa por una serie de estados, y dependiendo del estado en que se encuentre será posible realizar una serie de acciones u otras. Normalmente esto lo modelamos con una máquina de estados, obteniendo una representación del workflow para dicha entidad. Una vez tenemos la máquina de estados es común que sea el programador el que tenga que ir preguntando en qué estado se encuentra, controlando de esta forma las acciones que ofrece, y siendo también el responsable de cambiar de estado; lo que puede provocar en casos olvidados no ofreciendo todas las acciones o no actualizando el estado de la entidad al ejecutar alguna acción.

Esta responsabilidad la podemos delegar en los motores de workflow, que básicamente son los encargados de controlar las transiciones y cambios de estado de una máquina de estados. De esta forma, una vez tengamos definida nuestra máquina de estados sería conveniente delegar la responsabilidad del workflow a uno de estos motores sin necesidad de tener que ir controlando todo desde nuestra aplicación. En este tutorial vamos a presentar uno de estos motores OSWorkflow y su integración con Spring.

Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portatil Samsung R70 ( Intel(R) Core(TM)2 Duo 2,5Ghz, 2046 MB RAM, 232 Gb HD)
  • Sistema Operativo: Windows Vista Home Premium
  • Máquina Virtual Java: JDK 1.5.0_14 de Sun Microsystems (http://java.sun.com/javase/downloads/index_jdk5.jsp)
  • IDE Eclipse 3.3 (Europa) (http://www.eclipse.org/downloads/)

Dependencias.

Para utilizar OSWorkflow y Spring en nuestro proyecto debemos tener registradas todas las dependencias necesarias; ya sabéis que crear un proyecto de Maven nos ayuda a gestionar todas estas dependencias de una forma sencilla; por lo que simplemente configurando el fichero «pom.xml» ya estaremos listos para empezar a trabajar.

 

OSWorkflow.

Como hemos adelantado, OSWorkflow es un motor de workflow en el que podemos delegar la responsabilidad de todo el workflow. OSWorkflow nos permite definir los distintos pasos por los que pasar, restricciones para la ejecución de acciones, acciones con alternativas del estado al que pasar dependiendo del resultado de la acción, acciones que bifurcan o unen el flujo, funciones que se ejecutan antes o después de realizar una acción, etc.

Para ver una parte de las posibilidades que nos ofrece nos vamos a basar en un ejemplo de envío de pedidos con la siguiente máquina de estados, que simplifica la gestión de pedidos en un almacén.

OSWorkflow nos permite definir el flujo mediante un fichero XML, donde los elementos principales son:

  • pasos (<step>): Son los distintos estados posibles del flujo. En nuestro ejemplo lo son «inDemand», «made», «outOfStock», etc.
  • acciones (<action>): Serían las transiciones entre un estado y otro. En nuestro ejemplo lo son «make», «order», «package», etc. Los elementos <action> se definen dentro de los elementos <step>.
  • estado (atributos status y old-status): OSWorkflow nos permite definir el estado en que se encuentra una instancia del workflow dentro de un paso. Vendría a ser como un subestado dentro de los estados principales. Es un string al que le podemos asignar el valor que nos convenga, normalmente suele ser suficiente con :
    • «Underway»: Suele indicar que el workflow se encuentra en dicho paso y está disponible para seguir su flujo.
    • «Queued»: Suele indicar que el workflow se encuentra en dicho paso y está esperando algún evento para poder seguir su flujo.
    • «Finished»: Suele indicar que el paso ya ha sido abandonado.

    Nos puede servir para restringir las acciones a realizar dentro de un paso dependiendo del subestado de dicho paso. Estos atributos se definen en los resultados de las acciones.

  • resultados (<result> y <unconditional-result>): Se definen dentro de las acciones, siendo obligatorio definir un elemento <uncoditional-result>. En el resultado se indicará el subestado en el que se queda el paso en el que se ejecuta la acción (mediante el atributo «old-status») y el estado del paso al que se dirige el flujo (mediante el atributo «status»). Para indicar a que paso se salta al ejecutar la acción se define por medio del atributo «step». El hecho de tener la posibilidad de elementos <result> y <unconditional-result> nos sirve para incluir condiones que dirijan el flujo. Estas condiciones se definirán dentro de los elementos <result>. En caso de no cumplirse ninguna, el resultado de la acción será el definido por el elemento <unconditional-result>

Además de estos elementos, en la definición del workflow se pueden definir otros adicionales, como permisos, condiciones, bifurcaciones y uniones, etc. algunos de los cuales iremos viendo en el ejemplo. Para más información podéis ver la documentación de OSWorkflow.

Sabiendo los elementos de definición del workflow creamos un fichero XML que define la máquina de estados del ejemplo.

 

En este fichero de ejemplo se puede ver como se han definido todos los pasos por los que puede pasar el flujo y las acciones disponibles a realizar en cada uno de ellos, tal y como están definidos en la máquina de estados.

Al principio del fichero se deben definir las posibles acciones iniciales. Estas acciones iniciales son las que crean una nueva instancia del workflow. En nuestro caso de ejemplo tenemos que la acción inicial es «create», en la que se define como resultado de la acción que la instacia del workflow va al paso con id=1 (estado «inDemand»).

 

En el primer paso (step con id=1) se puede ver como hay dos posibles resultados para la acción «make» dependiendo de la comprobación del stock; para lo cual nos hemos tenido que definir una condición que devuelve «true» o «false». Podemos ver como esta condición va definida en un elemento <conditions> con un atributo «type» con valor «AND», como podemos suponer dentro del elemento <conditions> podríamos definir múltiples condiciones, que de cumplirse todas, al tener el operador «AND», este resultado sería el resultado de ejecutar la acción.

 

Definir condiciones en OSWorkflow es bastante sencillo, sólo hay que crear una clase que implemente el interfaz «com.opensymphony.workflow.Condition» implementando el método «public boolean passesCondition(Map transientVars, Map args, PropertySet propertySet)». Donde los parámetros son:

  • transientVars: Conjunto de variables no persistentes donde podemos encontrar;
    • store: La instancia del WorkflowStore.
    • descriptor: El descritor del workflow, donde podemos acceder a su definición.
    • currentSteps: Un array con los pasos en los que se encuentra la instancia del workflow.
    • entry: La representación del flujo recorrido, donde podremos acceder a los pasos actuales de la instancia del workflow y a los pasos que se han recorrido.
    • context: El contexto de la instancia del gestor de workflows.
    • actionId: El identificador de la acción que se está ejecutando.
    • configuration: La configuración del gestor de workflow.
    • createdStep: El nuevo paso que se ha creado al ejecuta la acción.
  • args: Argumentos que pueden ser pasados desde la definición en el xml con elementos <arg name=»argumento»>valor</arg>. (Se ve con un ejemplo de Roles un poco más abajo)
  • propertySet: Conjunto de propiedades que se desea sean persistentes. No se explica en este tutorial. (Utiliza la librería PropertySet).

En nuestro caso hemos definido la condición «CheckStock» que devuelve true si el identificador de la instancia del workflow es impar:

 

A modo de ejemplo, en la acción con id=4, se ha creado una restricción de ejecución. Esto sirve para que sólamente se pueda ejecutar dicha acción si se cumplen las condiciones definidas en la restricción. En nuestro caso de ejemplo sólo se permite ejecutar dicha acción si el paso se encuentra en estado «Underway».

Por poner otro ejemplo de uso, se podría definir una condición que sólo permitiese la ejecución de la acción si el usuario pertenece a unos posibles roles.

 

Ya tenemos definido nuestro workflow (máquina de estados) en el fichero «commissioning.xml», que colocaremos en el classpath de nuestro proyecto para que sea accesible, pero faltaría definir la configuración propia de OSWorkflow:

  • WorkflowStore: Responsable de persistir las distintas instancias de los posibles workflows. Por ejemplo en un sistema de pedidos, cada pedido en particular, tendrá su propia instancia de workflow (mantiene el estado en el que se encuentra el pedido, acciones que puede ejecutar, etc.) aunque todas ellas son del mismo tipo.
  • WorkflowFactory: Responsable de crear los distintos tipos de workflows (gestores de workflows) partiendo de su definición.

Nota: Lo que hemos llamado «gestor de workflow» será una instancia de una clase que implemente el interfaz «com.opensymphony.workflow.Workflow», donde se definen los servicios para trabajar con las instancias concretas de los workflows.

Vamos a aclarar lo que es un WorkflowStore, WorkflowsFactory, gestor de workflows, instancias de workflow y tipos de workflow un poco más. Si tenemos ficheros XML’s que definen workflows de forma lógica; con el WrokflowFactory crearemos un gestor de workflows por cada tipo. Con el «gestor» que se ha creado podrémos crear instancias concretas de ese workflow, y a través del gestor iremos realizando las distintas acciones sobre cada instancia. Por último, tenemos el WorkflowStore, que es el responsable de la persistencia de las instancias concretas de workflow que hemos creado con el «gestor de workflows». En el gráfico siguiente vemos una representación de este funcionamiento:

Al integrar OSWorkflow con Spring, esta configuración la haremos directamente en los fichero de configuración de Spring. Si no estamos integrando con Spring, hay que definirse el fichero de configuración «osworkflow.xml» que también debería estar en el classpath. Un ejemplo de este fichero sería:

 

Se puede ver que guarda las distintas instancias de los workflows en memoria, no utiliza ninguna base de datos. En cuanto al WorkflowFactory, al haber definido el workflow en XML (podría estar en una BD), utilizamos la clase «XMLWorkflowFactory» que necesita una propiedad (el fichero «workflows.xml») que le indica donde están definidos los distintos workflows. El fichero «workflows.xml», también lo ubicaremos en nuestro classpath, y tendrá una referencia por cada workflow; en nuestro caso:

 

Integrando OSWorkflow con Spring

Una de las ventajas que tenemos con OSWorkflow es que lo podemos integrar con Spring añadiendo las referencias necesarias en el «applicationContext.xml». Siendo en este fichero donde definiremos la configuración que antes se hacía en el fichero «osworkflow.xml». El «applicationContext.xml» será:

 

Además de la configuración tipica de «dataSource», «transactionManager», etc. tenemos la configuración propia de OSWorkflow, donde configuramos:

  • workflowStore: En este caso, sí queremos que se guarde en la base de datos, por lo que utilizamos «SpringHibernateWorkflowStore» indicando que utilice el «sessionFactory» definido en el fichero.
  • workflowFactory: Al seguir teniendo nuestra defiición en XML sigue siendo «XMLWorkflowFactory». Toma por defecto el fichero «workflows.xml» que se encuentra en el classpath.
  • osworkflowConfiguration: Utilizamos «SpringConfiguration» indicando el «WorkflowStore» y «WorkflowFactory». Lo pasaremos como referencia al workflow en concreto.
  • workflowTypeResolver: Utilizando «SpringTypeResolver» indicamos que podemos hacer referencias a «beans» de Spring, directamente desde los ficheros XML que definen los workflows (lo veremos un poco más adelante).
  • workflow: Al estar utilizando como persistencia Hibernate, utilizamos «TransactionProxyFactoryBean» para asegurarnos que el acceso a los métodos de la clase «BasicWorkflow» (declarada como «target») se hacen en una transacción. De esta forma nos aseguramos no tener problemas de sessiones al ir recuperando las entidades que gestiona (HibernateWorkflowEntry, HibernateCurrentStep e HibernateHistoryStep). En caso de no acceder mediante este proxy, tendríamos excepciones del tipo «LazyInicializationException» al acceder a los métodos de la clase que va a gestionar los workflows.
  • workflowTarget: Utilizado como objetivo del bean «workflow» y sera el tipo concreto de workflow a crear. En nuestro ejemplo utilizamos «BasicWorkflow», pero podríamos crear nuestra propia clase de workflow.

Al decidir que las instancias concretas de los workflows se almacenen de forma persistente, hemos tenido que incluir el bean «TransactionProxyFactoryBean» que permita acceder a todas las entidades relacionadas con la instancia del workflow en concreto dentro de una transacción, al encontrase su representación relacionando varias entidades. A continuación ponemos la relación de estas entidades en base de datos (y su mapeo de clases) para comprender mejor como es la representación persistente de las instancias de los workflows.

Ahora ya podemos empezar a desarrollar el resto de nuestras clases, DAO’s, entidades, managers, etc.

Vamos a definir la entidad que va a estar sometida al workflow:

 

Como el control del flujo va a ser a través de OSWorkflow, es conveniente que en nuestra entidad tengamos una referencia a la entrada del workflow para no perder trazabilidad; por eso tenemos el atributo «workflowId», que será asignado al hacer persintente una nueva instancia de la clase «Commissioning».

De igual forma, aunque el estado está controlado por el motor de OSWorkflow, es conveniente que en nuestra entidad tengamos reflejado el estado en el que se encuentra, de esta forma simplificamos posibles informes o cambios del motor de workflow. En el ejemplo, guardaremos en el atributo «state» el paso actual del flujo en el que se encuentra la entidad. Al ser el responsable de un paso a otro el propio workflow, lo debemos delegar en él, creando una función encargada de actualizar el estado de la entidad al cambiar de un paso a otro. Ahora es donde vamos a ver como nos aprovechamos de la integración con Spring, referenciando beans directamente desde la definición del workflow, añadiendo la referencia a la siguiente función en el fichero «commissioning.xml»:

 

Esta referencia la crearemos para todas las acciones que tenemos definidas en el fichero, excepto para la acción inicial, ya que todavía no hay entidad creada.

Como vemos, la función la hemos definido dentro de un elemento <post-functions> que indica las funciones que se deben ejecutar al terminar la acción (transición). De forma similar podremos definir funciones que se ejecuten antes de ejecutar la acción (al empezar la transición) dentro de un elemento <pre-functions>.

La función la implementaremos en la clase «SetWorkflowState» y será un bean de Spring al que podremos inyectar cualquier otro recurso del contexto de Spring.

 

El manager responsable de gestionar los pedidos, también ofrecerá los servicios necesarios para el workflow, por ejemplo, será el responsable de asociar cada instancia del workflow con su entidad correspondiente, devolver las acciones disponibles, etc.

 

Como caso a destacar es el método «executeAction(final Commissioning commissioning, final int actionId)», donde podemos ver que se realiza un «refresh» después de haber ejecutado la acción sobre el workflow. Esto es debido a que como es posible que alguna función del workflow modifique el estado de la entidad (en nuestro caso es así) tenemos que refrescarla para que los cambios se vean reflejados en la propia entidad.

También podemos ver cómo para las acciones propias del workflow nos apoyamos en la clase «WorkflowUtils», que es donde hemos delegado la gestión del workflow a más bajo nivel. Es la clase responsbles de inicializar el workflow, ejecutar las acciones sobre el mismo y recuperar las acciones disponibles.

 

En esta clase se puede ver como se trabaja con el «gestor de workflows» siendo éste el recurso «workflow».

En el método «newWorkflow()» se crear una nueva instancia del workflow «commissioning» (WF_COMMISSIONING)) ejecutando la acción inicial aquella con id=100 (WF_INITIAL_ACTION) al ejecuta el método «initialize(….)» del «gestor de workflows».

Para obtener las acciones disponibles de una instancia concreta, llamamos al método «getAvalaibleActions(…)» del «gestor de workflows» pasando como parámetro el identificador de la instancia del workflow.

Y para poder ejecutar, método «executeAction(…)» del «gestor de workflows», una determinada acción sobre una instancia de workflow concreta, se pasa como parámetro el identificador de la instancia del workflow sobre la que ejecutar la acción, y el identificador de la acción a ejecutar.

Probando el ejemplo

Habiendo creado ya todas las clases de nuestro ejemplo, pasamos a probarlo recorriendo los caminos posibles del workflow. Para ello nos creamos una clase de «Test» con dos casos de prueba. En el primero hacemos un recorrido completo suponiedo que hay stock. En el segundo caso de prueba suponemos que no tenemos stock, así llevamos el flujo por la otra rama, haciendo que vuelva de nuevo al estado «inDemand» (primer paso), y volviendo de nuevo por la rama de «sin stock» para terminar cancelando.

 

Conclusiones

Hemos visto como podemos delegar los flujos en un motor de workflow donde configuraremos los distintos workflows que tengamos; de esta forma queda de una forma más clara el seguimiento del flujo de una entidad; cosa que antes teníamos que ir recorriendo el código fuente, de la misma forma que era muy probable cometer errores (dejando casos sin contemplar); algo que ahora es mucho más fácil de detectar.

También se puede apreciar como utilizando un motor de workflow, es fácilmente extensible la casuística, ya simplemente hay que definir los cambios en el fichero de definición del workflow.

Si queréis, aquí podéis conseguir todo el código fuente de este ejemplo
OSWorkflow con Spring.

Un saludo.
Borja Lázaro de Rafael.

Dejar respuesta

Please enter your comment!
Please enter your name here