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; } }