Iniciación a OSWorkflow con Spring

0
14348

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.

<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>workflow</groupId>
	<artifactId>workflow</artifactId>
	<packaging>jar</packaging>
	<version>1.0-SNAPSHOT</version>
	<name>workflow</name>
	<url>http://maven.apache.org</url>
	<dependencies>
		<!-- junit -->
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>4.4</version>
			<scope>test</scope>
		</dependency>
		
		<!-- librerías para osworkflow -->
		<dependency>
			<groupId>opensymphony</groupId>
			<artifactId>osworkflow</artifactId>
			<version>2.8.0</version>
		</dependency>
		<dependency>
			<groupId>opensymphony</groupId>
			<artifactId>oscore</artifactId>
			<version>2.2.5</version>
		</dependency>
		<dependency>
			<groupId>opensymphony</groupId>
			<artifactId>propertyset</artifactId>
			<version>1.4</version>
		</dependency>


		<!-- anotaciones de Hibernate -->
		<dependency>
			<groupId>org.hibernate</groupId>
			<artifactId>hibernate-annotations</artifactId>
			<version>3.4.0.GA</version>
		</dependency>
		<dependency>
			<groupId>org.hibernate</groupId>
			<artifactId>hibernate-commons-annotations</artifactId>
			<version>3.3.0.ga</version>
		</dependency>

		<!-- springframework -->
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-aop</artifactId>
			<version>2.5.6</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-tx</artifactId>
			<version>2.5.6</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-hibernate3</artifactId>
			<version>2.0.8</version>
		</dependency>
		<dependency>
			<groupId>org.slf4j.slf4j</groupId>
			<artifactId>slf4j-simple</artifactId>
			<version>1.5.2</version>
		</dependency>
		
		<!-- driver de mysql -->
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>5.1.6</version>
		</dependency>
		
		<!-- anotaciones de java -->
		<dependency>
			<groupId>javax.annotation</groupId>
			<artifactId>jsr250-api</artifactId>
			<version>1.0</version>
		</dependency>
	</dependencies>
	<build>
		<finalName>workflow</finalName>
		<plugins>
			<plugin>
				<artifactId>maven-compiler-plugin</artifactId>
				<configuration>
					<source>1.5</source>
					<target>1.5</target>
					<encoding>UTF-8</encoding>
				</configuration>
			</plugin>
		</plugins>
	</build>
</project>

 

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.

<!DOCTYPE workflow PUBLIC "-//OpenSymphony Group//DTD OSWorkflow 2.6//EN" "http://www.opensymphony.com/osworkflow/workflow_2_8.dtd">
<workflow>
	<initial-actions>
	  <!-- Es la acción inicial que se ejecuta al crear una instancia del workflow -->
		<action id="100" name="create">
			<results>
				<unconditional-result old-status="Finished"
					status="Underway" step="1" />
			</results>
		</action>
	</initial-actions>
	<steps>
		<step id="1" name="inDemand">
			<actions>
				<action id="1" name="make">
					<results>
						<result old-status="Finished" status="Underway"
							step="5">
							<!-- conjunto de condiciones que si se cumplen este resultado será el resultado al ejecutar la acción -->
							<conditions type="AND">
								<condition type="class">
									<arg name="class.name">
										com.autentia.workflow.conditions.CheckStock
									</arg>
								</condition>
							</conditions>
						</result>
						<!-- El "unconditional-result" será el resultado si no se cumple la condición del resultado definido arriba -->
						<unconditional-result old-status="Finished"
							status="Underway" step="2" />
					</results>
				</action>
			</actions>
		</step>
		<step id="2" name="outOfStock">
			<actions>
				<action id="2" name="order">
					<results>
						<unconditional-result old-status="Finished"
							status="Underway" step="3" />
					</results>
				</action>
			</actions>
		</step>
		<step id="3" name="backOrder">
			<actions>
				<action id="3" name="receive">
					<results>
						<unconditional-result old-status="Finished"
							status="Underway" step="1" />
					</results>
				</action>
				<action id="4" name="cancel">
				  <!-- Ejemplo para incluir una condición que deberá cumplirse para tener permisos de ejecución de la acción -->
					<restrict-to>
						<conditions type="AND">
							<condition type="class">
								<arg name="class.name">
									com.opensymphony.workflow.util.StatusCondition
								</arg>
								<arg name="status">Underway</arg>
							</condition>
						</conditions>
					</restrict-to>
					<results>
						<unconditional-result old-status="Finished"
							status="Finished" step="4" />
					</results>
				</action>
			</actions>
		</step>
		<step id="4" name="cancelled"></step>
		<step id="5" name="made">
			<actions>
				<action id="5" name="package">
					<results>
						<unconditional-result old-status="Finished"
							status="Underway" step="6" />
					</results>
				</action>
			</actions>
		</step>
		<step id="6" name="packaged">
			<actions>
				<action id="6" name="send">
					<results>
						<unconditional-result old-status="Finished"
							status="Finished" step="7" />
					</results>
				</action>
			</actions>
		</step>
		<step id="7" name="sent"></step>
	</steps>
</workflow>

 

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»).

.....
<initial-actions>
  <!-- Es la acción inicial que se ejecuta al crear una instancia del workflow -->
	<action id="100" name="create">
		<results>
			<unconditional-result old-status="Finished"
				status="Underway" step="1" />
		</results>
	</action>
</initial-actions>
.....

 

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.

.....
<conditions type="AND">
	<condition type="class">
		<arg name="class.name">
			com.autentia.workflow.conditions.CheckStock
		</arg>
	</condition>
</conditions>
.....

 

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:

public class CheckStock implements Condition {

	public boolean passesCondition(final Map transientVars, final Map args, final PropertySet propertySet)
			throws WorkflowException {
		final WorkflowEntry entry = (WorkflowEntry)transientVars.get("entry");
		if (entry.getId() % 2 == 0) {
			return false;
		}
		return true;
	}

}

 

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.

.....
<!--- restricción de ejemplo -->
<restrict-to>  
   <conditions type="AND">  
       <condition type="class">  
           <arg name="class.name">  
               com.autentia.workflow.conditions.UserInRole
           </arg>  
           <arg name="roles">Rol1, Rol2, Rol3, etc.</arg>  
       </condition>  
   </conditions>  
</restrict-to>
.....

 

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:

<osworkflow>
    <persistence class="com.opensymphony.workflow.spi.memory.MemoryWorkflowStore"/>

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

 

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:

<workflows>
    <workflow name="commissioning" type="resource" location="commissioning.xml"/> 
</workflows>  

 

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á:

<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:util="http://www.springframework.org/schema/util"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xmlns:tx="http://www.springframework.org/schema/tx"
	xsi:schemaLocation="
		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
		http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-2.5.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd
		http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd
		http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd">

	<context:annotation-config />
	<context:component-scan base-package="com.autentia.workflow" />

	<!-- ···························· -->
	<!-- Configuracion del datasource -->
	<!-- ···························· -->
	<bean id="dataSource"
		class="org.springframework.jdbc.datasource.DriverManagerDataSource">
		<property name="driverClassName" value="com.mysql.jdbc.Driver" />
		<property name="url"
			value="jdbc:mysql://localhost:3306/osworkflow" />
		<property name="username" value="osworkflow" />
		<property name="password" value="osworkflow" />
	</bean>

	<!-- ·························· -->
	<!-- Configuracion de hibernate -->
	<!-- ·························· -->
	<bean id="sessionFactory"
		class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
		<property name="dataSource" ref="dataSource" />
		<property name="configurationClass"
			value="org.hibernate.cfg.AnnotationConfiguration" />
		<property name="configLocation">
			<value>classpath:hibernate.cfg.xml</value>
		</property>
	</bean>
	<bean id="transactionManager"
		class="org.springframework.orm.hibernate3.HibernateTransactionManager">
		<property name="sessionFactory" ref="sessionFactory" />
	</bean>

	<!-- ······················································ -->
	<!-- Definicion de los beans necesarios para el workflow     -->
	<!-- ······················································ -->
	<bean id="workflowStore"
		class="com.opensymphony.workflow.spi.hibernate3.SpringHibernateWorkflowStore">
		<property name="sessionFactory">
			<ref bean="sessionFactory" />
		</property>
		<property name="propertySetDelegate">
			<bean id="propertySetDelegate"
				class="com.opensymphony.workflow.util.PropertySetDelegateImpl" />
		</property>
	</bean>
	<bean id="workflowFactory"
		class="com.opensymphony.workflow.loader.XMLWorkflowFactory"
		init-method="initDone" />
	<bean id="osworkflowConfiguration"
		class="com.opensymphony.workflow.config.SpringConfiguration">
		<property name="store">
			<ref local="workflowStore" />
		</property>
		<property name="factory">
			<ref local="workflowFactory" />
		</property>
	</bean>
	<bean id="workflowTypeResolver"
		class="com.opensymphony.workflow.util.SpringTypeResolver">
	</bean>
	<bean id="workflow"
		class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
		<property name="transactionManager">
			<ref bean="transactionManager" />
		</property>
		<property name="target">
			<ref local="workflowTarget" />
		</property>
		<property name="transactionAttributes">
			<props>
				<prop key="*">PROPAGATION_REQUIRED</prop>
			</props>
		</property>
	</bean>
	<bean id="workflowTarget"
		class="com.opensymphony.workflow.basic.BasicWorkflow">
		<constructor-arg>
			<value>workflow</value>
		</constructor-arg>
		<property name="configuration">
			<ref local="osworkflowConfiguration" />
		</property>
		<property name="resolver">
			<ref local="workflowTypeResolver" />
		</property>
	</bean>
</beans>

 

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:

@Entity
@NamedQuery(name = "findByWorkflowId", query = "Select c from Commissioning c where c.workflowId = ?")
public class Commissioning {

	public static final String findByWorkflowId = "findByWorkflowId";

	@Id
	@GeneratedValue
	private Integer id;

	private String name;

	private String description;

	private Long workflowId;

	private Integer state;

	public Integer getState() {
		return this.state;
	}

	public void setState(final Integer state) {
		this.state = state;
	}

	public Long getWorkflowId() {
		return this.workflowId;
	}

	public void setWorkflowId(final Long workflowId) {
		this.workflowId = workflowId;
	}

	Commissioning() {
		// Sólo el manager puede constuir nuevas instancias
	}

	public String getName() {
		return this.name;
	}

	public void setName(final String name) {
		this.name = name;
	}

	public String getDescription() {
		return this.description;
	}

	public void setDescription(final String description) {
		this.description = description;
	}

	public Integer getId() {
		return this.id;
	}

	public void setId(final Integer id) {
		this.id = id;
	}

}

 

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»:

...
<action ...>
  ....
  <post-functions>
  	<function type="spring">
  		<arg name="bean.name">setWorkflowState</arg>
  	</function>
  </post-functions>
</action>  

 

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.

@Service
public class SetWorkflowState implements FunctionProvider {

	private static final Log log = LogFactory.getLog(SetWorkflowState.class);

	@Resource
	CommissioningMgr commissioningMgr;

	public void execute(final Map transientVars, final Map args, final PropertySet propertySet)
			throws WorkflowException {
		//recuperamos el paso que se acaba de crear en la transición
		final Step step = (Step)transientVars.get("createdStep");
		Commissioning commissioning;
		try {
			commissioning = this.commissioningMgr.findByWorkflowId(step.getEntryId());
		} catch (final MyAppException e) {
			throw new WorkflowException(e);
		}
		if (commissioning == null) {
			throw new WorkflowException("no se ha encontrado ningún pedido asociado al workflow");
		}
		commissioning.setState(step.getStepId());
	}

}

 

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.

@Service
public class CommissioningMgr {

	@Resource
	private Dao dao;

	@Resource
	private WorkflowUtils workflowUtils;

	public Commissioning newCommissioning() {
		return new Commissioning();
	}

	public void persist(final Commissioning commissioning) throws MyAppException {
		// si es nuevo
		if (commissioning.getId() == null) {
			try {
			  //se creará una nueva instancia de workflow y se asocia con la instancia de la clase commissioning 
				commissioning.setWorkflowId(this.workflowUtils.newWorkflow());
				commissioning.setState(1);
				this.dao.persist(commissioning);
			} catch (final WorkflowException e) {
				throw new MyAppException(e);
			}
		} else {
			this.dao.persist(commissioning);
		}

	}

	public List<Commissioning> getCommissionings() {
		final List<Commissioning> list = this.dao.find(Commissioning.class);
		return list;
	}

	public int[] getAvalaibleActions(final Commissioning commissioning) {
		final Long wfId = commissioning.getWorkflowId();
		return this.workflowUtils.getAvalaibleActions(wfId);

	}


	public void executeAction(final Commissioning commissioning, final int actionId) throws MyAppException {
		try {
			final Long wfId = commissioning.getWorkflowId();
			this.workflowUtils.executeAction(wfId, actionId);
			this.dao.refresh(commissioning);
		} catch (final WorkflowException e) {
			throw new MyAppException(e);
		}
	}

	public Commissioning findById(final Integer id) {
		final Commissioning commissioning = this.dao.get(Commissioning.class, id);
		return commissioning;
	}

	public Commissioning findByWorkflowId(final long id) throws MyAppException {
		final Object[] params = { id };
		final List<Commissioning> list = this.dao.findForWorkflowFuntion(Commissioning.findByWorkflowId, params);
		// como máximo sólo debería haber uno
		if (list.size() > 1) {
			throw new MyAppException("solo debería haber un pedido apuntando a este workflow");
		} else if (list.size() == 1) {
			return list.get(0);
		} else {
			return null;
		}
	}
}

 

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.

@Service
public class WorkflowUtils {

	private static final Log log = LogFactory.getLog(WorkflowUtils.class);

	private static final String WF_COMMISSIONING = "commissioning";
	
	private static final int WorkflowUtils.WF_INITIAL_ACTION = 100;

	@Resource
	private Workflow workflow;


	public Long newWorkflow() throws InvalidActionException, InvalidRoleException, InvalidInputException,
			InvalidEntryStateException, WorkflowException {
		final Long id = this.workflow.initialize(WorkflowUtils.WF_COMMISSIONING, WorkflowUtils.WF_INITIAL_ACTION, null);
		return id;
	}

	public int[] getAvalaibleActions(final long wfId) {
		final int[] actions = this.workflow.getAvailableActions(wfId, null);
		return actions;
	}

	public void executeAction(final long wfId, final int actionId) throws NumberFormatException, InvalidInputException,
			WorkflowException {

		this.workflow.doAction(wfId, actionId, null);
	}
}

 

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.

public class WorkflowTest {

	private static String[] files = new String[] { "applicationContext.xml" };

	final ApplicationContext context;

	public WorkflowTest() {
		this.context = new ClassPathXmlApplicationContext(WorkflowTest.files);
	}

	private void crearPedidos() throws MyAppException {
		final CommissioningMgr commissioningMgr = (CommissioningMgr)this.context.getBean("commissioningMgr");
		// creamos un nuevo pedido con el que trabajar
		Commissioning commissioning = commissioningMgr.newCommissioning();
		commissioning.setName("commissioning1");
		commissioning.setDescription("desc1");
		// al guardarlo se creara el workflow
		commissioningMgr.persist(commissioning);
		// vemos las acciones disponibles
		int[] workflowActions = commissioningMgr.getAvalaibleActions(commissioning);
		// sólo debemos tener la accion de comprobar stock
		Assert.assertEquals(1, workflowActions.length);
		// la única acción debe ser preparar el pedido (id=1)
		Assert.assertEquals(1, workflowActions[0]);

		// creamos otronuevo pedido con el que trabaja
		commissioning = commissioningMgr.newCommissioning();
		// al guardarlo se crea el workflow
		commissioningMgr.persist(commissioning);
		// vemos las acciones disponibles
		workflowActions = commissioningMgr.getAvalaibleActions(commissioning);
		// sólo debemos tener la accion de comprobar stock
		Assert.assertEquals(1, workflowActions.length);
		// la única acción debe ser preparar el pedido (id=1)
		Assert.assertEquals(1, workflowActions[0]);

		// comprobamos que tenemos 2 pedidos en la base de datos
		final List<Commissioning> commissionings = commissioningMgr.getCommissionings();
		Assert.assertEquals(2, commissionings.size());
	}

	/**
	 * Crea dos pedidos y recupera el primer pedido y ejecuta todo el workflow hasta el final
	 */
	@Test
	public void testConStock() {
		try {
			this.crearPedidos();
			final CommissioningMgr commissioningMgr = (CommissioningMgr)this.context.getBean("commissioningMgr");
			// creamos un nuevo pedido con el que trabajar
			final Commissioning commissioning = commissioningMgr.findById(1);
			// se debe haber recuperado la instancia
			Assert.assertNotNull(commissioning);
			// vemos las acciones disponibles
			int[] workflowActions = commissioningMgr.getAvalaibleActions(commissioning);
			// sólo debemos tener la accion de preparar
			Assert.assertEquals(1, workflowActions.length);
			// la única acción debe ser preparar el pedido (id=1)
			Assert.assertEquals(1, workflowActions[0]);

			// ejecutamos la accion
			commissioningMgr.executeAction(commissioning, workflowActions[0]);
			// como estamos con el pedido con id=1
			// debe pasar la comprobación de stock, por lo
			// que estaremos en el paso con id=5
			Assert.assertEquals(5, commissioning.getState().intValue());
			// y si ahora recuperamos las acciones disponibles
			// sólo debemos tener una, la acción de empaquetar (id=5)
			workflowActions = commissioningMgr.getAvalaibleActions(commissioning);
			Assert.assertEquals(1, workflowActions.length);
			Assert.assertEquals(5, workflowActions[0]);

			// ejecutamos la acción disponible es empaquetar (id=5)
			commissioningMgr.executeAction(commissioning, workflowActions[0]);
			// hemos pasado al paso 6
			Assert.assertEquals(6, commissioning.getState().intValue());

			// si ahora recuperamos las acciones disponibles
			// sólo debemos tener una, la acción de enviar (id=6)
			workflowActions = commissioningMgr.getAvalaibleActions(commissioning);
			Assert.assertEquals(1, workflowActions.length);
			Assert.assertEquals(6, workflowActions[0]);

			// ejecutamos la acción disponible es empaquetar (id=5)
			commissioningMgr.executeAction(commissioning, workflowActions[0]);
			// hemos pasado al paso 7
			Assert.assertEquals(7, commissioning.getState().intValue());

			// ahora no debemos tener ninguna acción disponible al haber llegado
			// al final del workflow
			workflowActions = commissioningMgr.getAvalaibleActions(commissioning);
			Assert.assertEquals(0, workflowActions.length);

		} catch (final MyAppException e) {
			e.printStackTrace();
			Assert.fail("Error");
		}

	}

	/**
	 * Crea dos pedidos y recupera el segundo pedido, se ejecuta el workflow por la rama de "sin stock", se solicita el
	 * suministro, y en la segunda vuelta del workflow se cancela
	 */
	@Test
	public void testSinStock() {
		try {
			this.crearPedidos();
			final CommissioningMgr commissioningMgr = (CommissioningMgr)this.context.getBean("commissioningMgr");
			// creamos un nuevo pedido con el que trabajar
			final Commissioning commissioning = commissioningMgr.findById(2);
			// se debe haber recuperado la instancia
			Assert.assertNotNull(commissioning);
			// vemos las acciones disponibles
			int[] workflowActions = commissioningMgr.getAvalaibleActions(commissioning);
			// sólo debemos tener la accion de preparar
			Assert.assertEquals(1, workflowActions.length);
			// la única acción debe ser preparar el pedido (id=1)
			Assert.assertEquals(1, workflowActions[0]);

			// ejecutamos la accion
			commissioningMgr.executeAction(commissioning, workflowActions[0]);
			// como estamos con el pedido con id=2
			// NO debe pasar la comprobación de stock, por lo
			// que estaremos en el paso con id=2
			Assert.assertEquals(2, commissioning.getState().intValue());
			// si ahora recuperamos las acciones disponibles
			// sólo debemos tener una, la acción de solicitar al distribuidor (id=2)
			workflowActions = commissioningMgr.getAvalaibleActions(commissioning);
			Assert.assertEquals(1, workflowActions.length);
			Assert.assertEquals(2, workflowActions[0]);

			// ejecutamos la acción disponible es solicitar (id=2)
			commissioningMgr.executeAction(commissioning, workflowActions[0]);

			// estamos en el paso 3
			Assert.assertEquals(3, commissioning.getState().intValue());
			// si ahora recuperamos las acciones disponibles
			// debemos tener 2, la acción de recibir (id=3) y
			// la accion de cancelar (id=4)
			workflowActions = commissioningMgr.getAvalaibleActions(commissioning);
			Assert.assertEquals(2, workflowActions.length);
			if ((workflowActions[0] != 3) && (workflowActions[0] != 4)) {
				Assert.fail("Acción disponible no esperada");
			}
			if ((workflowActions[1] != 3) && (workflowActions[1] != 4)) {
				Assert.fail("Acción disponible no esperada");
			}

			// ejecutamos la acción disponible de recibir (id=3)
			commissioningMgr.executeAction(commissioning, 3);

			// ahora ha vuelto al paso 1
			Assert.assertEquals(1, commissioning.getState().intValue());
			// comprobamos que hemos vuelto al primer paso
			// teniendo únicamente disponible la acción depreparar
			workflowActions = commissioningMgr.getAvalaibleActions(commissioning);
			// sólo debemos tener la accion de preparar
			Assert.assertEquals(1, workflowActions.length);
			Assert.assertEquals(1, workflowActions[0]);

			// ahora volvemos a ejecutar los pasos que hemos probado
			// antes para probar la cancelación y terminar

			// preparar
			commissioningMgr.executeAction(commissioning, 1);
			Assert.assertEquals(2, commissioning.getState().intValue());
			// solicitar distribución
			commissioningMgr.executeAction(commissioning, 2);
			Assert.assertEquals(3, commissioning.getState().intValue());
			// cancelar
			commissioningMgr.executeAction(commissioning, 4);
			// ahora ha se ha ido al paso 4 (estado de cancelado)
			Assert.assertEquals(4, commissioning.getState().intValue());
			// que no tiene ninguna acción
			workflowActions = commissioningMgr.getAvalaibleActions(commissioning);
			Assert.assertEquals(0, workflowActions.length);

		} catch (final MyAppException e) {
			e.printStackTrace();
			Assert.fail("Error");
		}
	}
}

 

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.

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad