Parallel Review Process: template process definition
alf_dave Oct 18, 2006 1:06 PMI believe the jBPM community can greatly benefit from a library of demo/template process definitions for common workflows.
The current code samples, docs & references to workflow patterns concentrate on the low level building blocks, but it requires a blind jump and effort to pull together the blocks into a coherent workflow.
So, to start, I'd like to put forward the following template definition for supporting a review & approve process where 'N' parallel reviewers can approve or reject. Approval is only reached when a specified percentage of the reviewers approve.
A custom 'ForEachFork' (modified from the contribution on the wiki) is used to implement the parallel part. The process variables 'reviewers' (a list) and 'required_approve_percent' need to be provided when starting the workflow.
Is there a better way?
Would it be useful to start a library of these on the WIKI?
Regards,
David Caruana
Alfresco
<process-definition xmlns="urn:jbpm.org:jpdl-3.1" name="parallelreview">
<swimlane name="initiator"></swimlane>
<start-state name="start">
<task name="submit" swimlane="initiator" />
<transition name="" to="startreview">
<script>
<variable name="approve_count" access="write" />
<expression>
approve_count = 0;
</expression>
</script>
</transition>
</start-state>
<node name="startreview">
<action class="ForEachFork">
<foreach>#{reviewers}</foreach>
<var>reviewer</var>
</action>
<transition name="review" to="review" />
</node>
<task-node name="review">
<task name="review">
<event type="task-create">
<script>
taskInstance.actorId = reviewer;
</script>
</event>
</task>
<transition name="reject" to="endreview" />
<transition name="approve" to="endreview">
<script>
<variable name="approve_count" access="read,write" />
<expression>
approve_count = approve_count +1;
</expression>
</script>
</transition>
</task-node>
<join name="endreview">
<transition to="isapproved" />
</join>
<decision name="isapproved">
<event type="node-enter">
<script>
<variable name="approve_percent" access="write"/>
<expression>
approve_percent = ((approve_count * 100) / reviewers.size());
</expression>
</script>
</event>
<transition name="reject" to="rejected" />
<transition name="approve" to="approved">
<condition>#{approve_percent >= required_approve_percent}</condition>
</transition>
</decision>
<task-node name="rejected">
<task name="rejected" swimlane="initiator" />
<transition to="end" />
</task-node>
<task-node name="approved">
<task name="approved" swimlane="initiator" />
<transition to="end" />
</task-node>
<end-state name="end"/>
</process-definition>
The ForEachFork.java:
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.dom4j.Element;
import org.jbpm.graph.def.ActionHandler;
import org.jbpm.graph.def.Node;
import org.jbpm.graph.def.Transition;
import org.jbpm.graph.exe.ExecutionContext;
import org.jbpm.graph.exe.Token;
import org.jbpm.instantiation.FieldInstantiator;
import org.jbpm.jpdl.el.impl.JbpmExpressionEvaluator;
/**
* For each "item in collection", create a fork.
*/
public class ForEachFork implements ActionHandler
{
private static final long serialVersionUID = 4643103713602441652L;
private Element foreach;
private String var;
/**
* Create a new child token for each item in list.
*
* @param executionContext
* @throws Exception
*/
@SuppressWarnings("unchecked")
public void execute(final ExecutionContext executionContext)
throws Exception
{
//
// process action handler arguments
//
if (foreach == null)
{
throw new Exception("forEach has not been provided");
}
// build "for each" collection
List forEachColl = null;
String forEachCollStr = foreach.getTextTrim();
if (forEachCollStr != null)
{
if (forEachCollStr.startsWith("#{"))
{
Object eval = JbpmExpressionEvaluator.evaluate(forEachCollStr, executionContext);
if (eval == null)
{
throw new Exception("forEach expression '" + forEachCollStr + "' evaluates to null");
}
// expression evaluates to string
if (eval instanceof String)
{
String[] forEachStrs = ((String)eval).trim().split(",");
forEachColl = new ArrayList(forEachStrs.length);
for (String forEachStr : forEachStrs)
{
forEachColl.add(forEachStr);
}
}
// expression evaluates to collection
else if (eval instanceof Collection)
{
forEachColl = (List)eval;
}
}
}
else
{
forEachColl = (List)FieldInstantiator.getValue(List.class, foreach);
}
if (var == null || var.length() == 0)
{
throw new Exception("forEach variable name has not been provided");
}
//
// create forked paths
//
Token rootToken = executionContext.getToken();
Node node = executionContext.getNode();
List<ForkedTransition> forkTransitions = new ArrayList<ForkedTransition>();
// first, create a new token and execution context for each item in list
for (int i = 0; i < node.getLeavingTransitions().size(); i++)
{
Transition transition = (Transition) node.getLeavingTransitions().get(i);
for (int iVar = 0; iVar < forEachColl.size(); iVar++)
{
// create child token to represent new path
String tokenName = getTokenName(rootToken, transition.getName(), iVar);
Token loopToken = new Token(rootToken, tokenName);
loopToken.setTerminationImplicit(true);
executionContext.getJbpmContext().getSession().save(loopToken);
// assign variable within path
final ExecutionContext newExecutionContext = new ExecutionContext(loopToken);
newExecutionContext.getContextInstance().createVariable(var, forEachColl.get(iVar), loopToken);
// record path & transition
ForkedTransition forkTransition = new ForkedTransition();
forkTransition.executionContext = newExecutionContext;
forkTransition.transition = transition;
forkTransitions.add(forkTransition);
}
}
//
// let each new token leave the node.
//
for (ForkedTransition forkTransition : forkTransitions)
{
node.leave(forkTransition.executionContext, forkTransition.transition);
}
}
/**
* Create a token name
*
* @param parent
* @param transitionName
* @return
*/
protected String getTokenName(Token parent, String transitionName, int loopIndex)
{
String tokenName = null;
if (transitionName != null)
{
if (!parent.hasChild(transitionName))
{
tokenName = transitionName;
}
else
{
int i = 2;
tokenName = transitionName + Integer.toString(i);
while (parent.hasChild(tokenName))
{
i++;
tokenName = transitionName + Integer.toString(i);
}
}
}
else
{
// no transition name
int size = ( parent.getChildren()!=null ? parent.getChildren().size()+1 : 1 );
tokenName = Integer.toString(size);
}
return tokenName + "." + loopIndex;
}
/**
* Fork Transition
*/
private class ForkedTransition
{
private ExecutionContext executionContext;
private Transition transition;
}
}