Tuesday, January 27, 2009

Drools Flow and OSWorkflow Migration

Hi, my name is Miguel Fossati, I am a developer at Synapsis Argentina, and I am working with Chief Enterprise Architect Diego Naya (author of Using OSWorkflow in your Application"), Maurticio Salatino (A.K.A. Salaboy) and the Drools team in improving Drools Flow. We work together on projects for Argentina's biggest healthcare provider, who are big OSWorkflow users. However with Drools offering a much more powerful and complete framework, that integrates rules, processes and event processing there is now a desire to move to this as the standard. As part of this effort we need a migration path for al the legacy OSWorkflow applications.

OSWorkflow is an open source workflow tool developed by OpenSymphony, widely used in open source, commercial systems and third party tools (like Jira, and others).

Since Drools Flow, part of up coming Drools 5 release, supports a pluggable node framework for process definitions it's possible for us to implement other execution models. We are developing a tool to migrate OSWorkFlows processes into Drools Flow process format. This now makes it possible to migrate OSWorkflow processes and definitions to Drools. We expect this tool to be very useful for developers and users that want to migrate their existing OSWorkflow based systems, to take advantage of the Drools Flow process capabilities.

We have extended the underlying nodes framework adding a Step node to fully support OSWorkflow's step concept. In OSWorkflow a Step node is a kind of a wait state that have some actions to take and move to another step. It supports conditional actions that implements OSWorkflow's Condition contract, and execution of functions via the FunctionProvider interface. It also supports Split and Join nodes using the native RuleFlow Split and Join nodes.

Lets see an example, starting with this simple OSWorkflow definition:
<workflow>
<initial-actions>
<action id="1" name="Start Workflow">
<results>
<unconditional-result old-status="Finished"
status="Queued" step="1" />
</results>
</action>
</initial-actions>
<steps>
<step id="1" name="First Draft">
<actions>
<action id="2" name="Start First Draft">
<restrict-to>
<conditions>
<condition type="class">
<arg name="class.name">
com.opensymphony.workflow.util.StatusCondition
</arg>
<arg name="status">Queued</arg>
</condition>
</conditions>
</restrict-to>
<pre-functions>
<function type="class">
<arg name="class.name">
com.opensymphony.workflow.util.Caller
</arg>
</function>
<function type="beanshell">
<arg name="script">
System.out.println("Before executing actionid 2");
</arg>
</function>
</pre-functions>
<results>
<unconditional-result old-status="Finished"
status="Underway" step="1" owner="${caller}" />
</results>
</action>
<action id="3" name="Finish First Draft">
<restrict-to>
<conditions type="AND">
<condition type="class">
<arg name="class.name">
com.opensymphony.workflow.util.StatusCondition
</arg>
<arg name="status">Underway</arg>
</condition>
<condition type="class">
<arg name="class.name">
com.opensymphony.workflow.util.AllowOwnerOnlyCondition
</arg>
</condition>
</conditions>
</restrict-to>
<results>
<unconditional-result old-status="Finished"
status="Queued" step="2" />
</results>
</action>
</actions>
</step>
<step id="2" name="finished" />
</steps>
</workflow>
Our Drools Flow definition matching the above would be like this:
<process xmlns="http://drools.org/drools-4.0/osworkflow"
xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
xs:schemaLocation="http://drools.org/drools-4.0/osworkflow drools-osworkflow-4.0.xsd"
type="OSWorkflow" name="simple" id="simple" package-name="org.drools.osworkflow" >

<header>
<initial-actions>
<action id="1" name="Start Workflow">
<results>
<unconditional-result old-status="Finished"
status="Queued" step="1" />
</results>
</action>
</initial-actions>
</header>

<nodes>
<step id="1" name="First Draft" >
<action id="2" name="Start First Draft">
<restrict-to>
<conditions>
<condition type="class">
<arg name="class.name">
com.opensymphony.workflow.util.StatusCondition
</arg>
<arg name="status">Queued</arg>
</condition>
</conditions>
</restrict-to>
<pre-functions>
<function type="class">
<arg name="class.name">
com.opensymphony.workflow.util.Caller
</arg>
</function>
<function type="beanshell">
<arg name="script"><![CDATA[
System.out.println("Before executing actionid 2");
]]></arg>
</function>
</pre-functions>
<results>
<unconditional-result old-status="Finished" status="Underway" step="1" owner="${caller}"/>
</results>
</action>
<action id="3" name="Finish First Draft">
<restrict-to>
<conditions type="AND">
<condition type="class">
<arg name="class.name">
com.opensymphony.workflow.util.StatusCondition
</arg>
<arg name="status">Underway</arg>
</condition>
<condition type="class">
<arg name="class.name">
com.opensymphony.workflow.util.AllowOwnerOnlyCondition
</arg>
</condition>
</conditions>
</restrict-to>
<results>
<unconditional-result old-status="Finished" status="Queued" step="2"/>
</results>
</action>
</step>
<step id="2" name="finished" >
</step>
</nodes>
<connections>
<connection from="1" fromType="3" to="2" toType="Queued" />
</connections>
</process>
Persistence is currently managed by a mix of JPA and a Serialization process that store the status of the process inside a relational schema that is updated every time the process reach a wait state, taking out of memory the current processInstance until someone else interacts with the process. This persistence strategy is beeing extended but at the moment you can use it with the SingleCommandSessionService.

To run our process, we first build our knowledge base, as any native Drools flow:
            // create a builder
PackageBuilder builder = new PackageBuilder();
// load the process
Reader source = new InputStreamReader(this.getClass().getResourceAsStream(resourceName));
builder.addProcessFromXml(source);
// create the knowledge base
Package pkg = builder.getPackage();
RuleBase ruleBase = RuleBaseFactory.newRuleBase();
ruleBase.addPackage(pkg);
As you can see here we load our process in the working memory, exactly in the same way that we load a RuleFlow process.

When we want to interact with the process we must use the SingleSessionCommandService to execute commands that influence our process to jump from one step to another.
This SingleSessionCommandService is configured with a JPA session that store the status of process when it reachs a wait state.

Here we start an instance of our process using the ID of the process, in this case "simple", this will execute the OSWorkflow initial actions and point the execution of the process to the first step node called "First Draft".
        SingleSessionCommandService service = new SingleSessionCommandService(ruleBase);
StartProcessCommand startProcessCommand = new StartProcessCommand();
startProcessCommand.setProcessId("simple");
ProcessInstance processInstance = (ProcessInstance) service.execute(startProcessCommand);
System.out.println("Started process instance " + processInstance.getId());

service = new SingleSessionCommandService(ruleBase);
GetProcessInstanceCommand getProcessInstanceCommand = new GetProcessInstanceCommand();
getProcessInstanceCommand.setProcessInstanceId(processInstance.getId());
processInstance = (ProcessInstance) service.execute(getProcessInstanceCommand);
System.out.println("Now working with processInstance " + processInstance.getId());
Now we want to execute an action to move to another step node in the workflow, so we use the command DoActionCommand to tell the process that we want to move to the next node. In this stage, the process will be retrieved from the database and a deserialization process of the processInstance status will take place. After this deserialization the action will be executed and the execution of the process will reach the next step. When the execution enter in the new step the process waits until someone execute another action, so the process will be persisted again.
        service = new SingleSessionCommandService(ruleBase);
DoActionCommand doActionCmd = new DoActionCommand();
doActionCmd.setProcessInstanceId(processInstance.getId());
doActionCmd.setActionId(2); //Action to be executed at current step
service.execute(doActionCmd);
Please, feel free to test this tool, and provide feedback that will help us improve it. This work is currently in a branch and will be merged into Drools trunk this week, we will announce in the blog once that merge is complete and ready for testing.

No comments:

Post a Comment