BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Articles Supporting Advanced User Interaction Patterns in jBPM

Supporting Advanced User Interaction Patterns in jBPM

Many general purpose business processes include humans actors. Human activities, ranging from simple scenarios, such as manual approval, to complex scenarios involving complicated data entry, introduce new aspects, such as human interaction patterns, into the process implementation. A typical set1 of human interaction patterns includes the following:

  • The 4-eyes principle, often referred to as “separation of duties,” is a common scenario when a decision is made by two or more people independently of one another. In many cases, it is simply enough to obtain a second opinion/signature.
  • Nomination is a situation when a supervisor manually assigns tasks to his team members based on their schedule or workload constraints or expertise.
  • Tasks are often modeled to express the expectation that they will be completed within a certain time frame. If a task is not progressing as expected, an escalation mechanism is required. Two typical escalation implementations are - reassignment of the tasks, often with a notification, that an escalation has occurred and notification (typically to a manager) that a task has not been completed in time.
  • Chained execution is a process (fragment) where a sequence of steps is executed by the same person.

In this article I will describe how these advanced interaction patterns can be implemented using JBoss jBPM.

Task management in jBPM

One of the core functionalities of jBPM2 is the management of tasks and task-lists for people. jBPM allows the use of tasks and task nodes as part of the overall process design.

Typically tasks are defined in jBPM in task-nodes. A single task-node can contain one or more tasks. A common behavior of a jBPM process, containing task-nodes is to wait until all the tasks within a task-node are completed and then continue. A given task can be assigned3 to either an individual user or a user group or a swimlane:

  • If the task is assigned to a specific user, only this user will be able to execute it.
  • If the task is assigned to a group of users, any participant of this group is able to execute the task. Instead of group ID jBPM is using a notion of pooled actors (which can contain a group name, list of group names, list of individual actors, etc.). If users start working on tasks in their group task list, it might result in conflict4 - many people will start working on the same task. To prevent this, before starting working on a task, users should move task instances from group task list into their personal task list.
  • A swimlane is a process role, which is usually assigned to a group of users. It is a mechanism to specify that multiple tasks in the process should be done by the same actor5. So after the first task is assigned to a given swimlane, the actor is remembered by the process for all subsequent tasks that are in the same swimlane.

jBPM provides two basic approaches for defining task assignment – as part of a process definition or programmatically. In the case where assignment as part of a process definition an assignee can be defined by specifying of either a specific user , a user group, or a swimlane. Additionally an expression can be used to dynamically determine the specific user based on process variables. A fully programmatic implementation is based on an assignment handler6, which allows a task to find the user ID, based on the on arbitrary calculations.

A process definition describes a process instance in a manner similar to the way a task describes a task instance. When a process is executing, a process instance – runtime representation of a process - is created. Similarly a task-instance – run time representation of a task is created. Based on the task definition, a task instance is assign to an actor/group of actors.

A role of task instance is to support user interaction – presenting data to the user and collecting data from a user. A jBPM task instance has full access to process (token) variables7 and can also have its own variables. The ability to have task’s own variables is useful for:

  • creating copies of process variables in the task instances so that intermediate updates to the task instance variables don't affect the process variables until the task is completed and the copies are submitted back into the process variables.
  • creating “derived” (calculated) variables better supporting user’s activity.

The task’s own variables are supported in jBPM through a task controller handler8, which can populate the task instance data (from the process data) at a task instance creation and submit task’s instance data into the process variables once the task instance is completed.

Implementing 4-eyes principle

As we have defined above, implementing 4-eyes principle means allowing multiple people to work on the task simultaneously. There are several possible approaches to such an implementation:

  • External to the task – parallel looping9 of the task required amount of times.
  • Using Action handler, attached to a task node enter event to create multiple node instances10, based on the process instance variables.
  • Internal to the task, introducing “task take” (similar to jPDL 4) support and allowing a given task instance to be taken several times

Based on the jBPM best practices11 - “Extend JBPM Api rather then messing with complex process modeling” I decided to go with the internal12 task approach. This required modifications to both task and task-instance classes provided by jBPM.

Extending Task class

Definition of jBPM task is contained in org.jbpm.taskmgmt.def.Task class. In order to support 4-eyes principle we need to add the following fields/methods to the class (Listing 1):

  protected int numSignatures = 1;


  public int getNumSignatures(){
	  return numSignatures;
  }
  public void setNumSignatures(int numSignatures){
	  this.numSignatures = numSignatures;
  }

Listing 1 Additional fields/methods for Task class

This new parameter allows specifying the amount of people that have to process the task in order for it to complete. The default value is 1 meaning, that only a single user should/can process the task.

jBPM is using hibernate for storing/retrieving data to/from database. In order to make our new variable persistent we need to update the hibernate config file for Task class - Task.hbm.xml – located in org.jbpm.taskmgmt.def folder by adding the following line (Listing 2):

    <property name="numSignatures" column="NUMSIGNATURES_" />

Listing 2 Specifying additional field in Task mapping

In order for our new property to be read correctly from the process definition and DB, we need to modify org.jbpm.jpdl.xml.JpdlXmlReader class to correctly read our new attribute (Listing 3)

String numSignatureText = taskElement.attributeValue("numSignatures");
if (numSignatureText != null) {
   	try{
    	task.setNumSignatures(Integer.parseInt(numSignatureText));
    }
    catch(Exception e){}
}

Listing 3 Reading numSignature attribute

Finally, because JpdlXmlReader class validates xml against schema we need to add an attribute definition to jpdl-3.2.xsd (Listing 4):

  <xs:element name="task">
	………………….
      <xs:attribute name="numSignatures" type="xs:string" />

Listing 4 Adding numSignatures attribute to jpdl-3.2.xsd

When all this is done, a task definition can be extended with numSignatures attribute (Listing 5):

 <task name="task2" numSignatures = "2">
<assignment pooled-actors="Peter, John"></assignment>
</task>

Listing 5 Adding numSignatures attribute to task definition

Extending TaskInstance class

Once a task class is extended we also need to create a custom task instance class keeping track of actors assigned to a task instance13 and making sure that all assigned actors complete class execution (Listing 6).

package com.navteq.jbpm.extensions;


import java.util.Date;
import java.util.LinkedList;
import java.util.List;


import org.jbpm.JbpmException;
import org.jbpm.taskmgmt.exe.TaskInstance;


public class AssignableTaskInstance extends TaskInstance {


	private static final long serialVersionUID = 1L;


	private List<Assignee> assignees = new LinkedList<Assignee>();


	private String getAssigneeIDs(){
		StringBuffer sb = new StringBuffer();
		boolean first = true;
		for(Assignee a : assignees){
			if(!first)
				sb.append(" ");
			else 
				first = false;
			sb.append(a.getUserID());
		}
		return sb.toString();
	}

	public List<Assignee> getAssignees() {
		return assignees;
	}


	public void reserve(String userID) throws JbpmException{

		if(task == null)
		    throw new JbpmException("can't reserve instance with no task");

		// Duplicate assignment is ok
		for(Assignee a : assignees){
			if(userID.equals(a.getUserID()))
				return;
		}


		// Can we add one more guy?


		if(task.getNumSignatures() > assignees.size()){
			assignees.add(new Assignee(userID));
			return;
		}


	    throw new JbpmException("task is already reserved by " +
getAssigneeIDs());


	}


	public void unreserve(String userID){


		for(Assignee a : assignees){
			if(userID.equals(a.getUserID())){
				assignees.remove(a);
				return;
			}
		}
	}


	private void completeTask(Assignee assignee, String transition){


		assignee.setEndDate(new Date());


		// Calculate completed assignments
		int completed = 0;
		for(Assignee a : assignees){
			if(a.getEndDate() != null)
				completed ++;
		}
		if(completed < task.getNumSignatures())
			return;
		if(transition == null)
			end();
		else 
			end(transition);
	}


	public void complete(String userID, String transition) throws JbpmException{


		if(task == null)
		    throw new JbpmException("can't complete instance with no task");


		// make sure it was reserved
		for(Assignee a : assignees){
			if(userID.equals(a.getUserID())){
				completeTask(a, transition);
				return;
			}
		}
	    throw new JbpmException("task was not reserved by " + userID);
	}


	public boolean isCompleted(){


		return (end != null);


	}
}

Listing 6 Extended TaskInstance class

This implementation extends a TaskInstance class, provided by jBPM and keeps track of the amount of actors, required to complete an instance. It introduces several new methods, allowing to reserve/unreserve task instances by an actor and complete task execution by a given actor.

An implementation (Listing 6) relies on a supporting class Assignee (Listing 7)

package com.navteq.jbpm.extensions;

import java.io.Serializable;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;


public class Assignee implements Serializable{


	private static final long serialVersionUID = 1L;
	private static final DateFormat dateFormat = new 
SimpleDateFormat("yyyy/MM/dd HH:mm:ss");


	long id = 0;
	protected String startDate = null;
	protected String userID = null;
	protected String endDate = null;


	public Assignee(){}


	public Assignee(String uID){


		userID = uID;
        startDate = dateFormat.format(new Date());
	}



////////////Setters and Getters ///////////////////


	public long getId() {
		return id;
	}
	public void setId(long id) {
		this.id = id;
	}
	public String getStartDate() {
		return startDate;
	}
	public void setStartDate(String startDate) {
		this.startDate = startDate;
	}
	public String getUserID() {
		return userID;
	}
	public void setUserID(String id) {
		userID = id;
	}
	public String getEndDate() {
		return endDate;
	}
	public void setEndDate(String endDate) {
		this.endDate = endDate;
	}


	public void setEndDate(Date endDate) {
		this.endDate = dateFormat.format(endDate);
	}


	public void setEndDate() {
		this.endDate = dateFormat.format(new Date());
	}


	public String toString(){


		StringBuffer bf = new StringBuffer();
		bf.append(" Assigned to ");
		bf.append(userID);
		bf.append(" at ");
		bf.append(startDate);
		bf.append(" completed at ");
		bf.append(endDate);
		return bf.toString();
	}
}

Listing 7 Assignee class

Both custom Task Instance class and Assignee class have to be storable in the database. This means that it is necessary to implement hibernate mappings for both of them14 (Listing 8, Listing 9):

<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">

<hibernate-mapping auto-import="false" default-access="field">

<subclass namename="com.navteq.jbpm.extensions.AssignableTaskInstance"
extends="org.jbpm.taskmgmt.exe.TaskInstance"
discriminator-value="A">
<list name="assignees" cascade="all" >
<key column="TASKINSTANCE_" />
<index column="TASKINSTANCEINDEX_"/>
<one-to-many class="com.navteq.jbpm.extensions.Assignee" />
</list>

</subclass>
</hibernate-mapping>

Listing - 8 Hibernate mapping for custom task instance

<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">

<hibernate-mapping auto-import="false" default-access="field">
<class name="com.navteq.jbpm.extensions.Assignee"
table="JBPM_ASSIGNEE">
<cache usage="nonstrict-read-write"/>
<id name="id" column="ID_"><generator class="native" /></id>
<!-- Content -->
<property name="startDate" column="STARTDATE_" />
<property name="userID" column="USERID_" />
<property name="endDate" column="ENDDATE_" />
</class>
</hibernate-mapping>

Listing 9 Hibernate mapping for Assignee class

In order for jBPM to be able to use our custom task instance implementation we also need to provide a custom task instance factory (Listing 10)

package com.navteq.jbpm.extensions;


import org.jbpm.graph.exe.ExecutionContext;
import org.jbpm.taskmgmt.TaskInstanceFactory;
import org.jbpm.taskmgmt.exe.TaskInstance;


public class AssignableTaskInstanceFactory implements TaskInstanceFactory {


	private static final long serialVersionUID = 1L;


	@Override
	public TaskInstance createTaskInstance(ExecutionContext executionContext) {


		return new AssignableTaskInstance();
	}


}

Listing 10 Custom taskInstance factory

Finally, in order for jBPM runtime to use the correct task instance factory (Listing 10), a new jBPM configuration file (Listing 11) has to be created.

<jbpm-configuration>

<bean name="jbpm.task.instance.factory"
class="com.navteq.jbpm.extensions.AssignableTaskInstanceFactory" singleton="true"
/>

</jbpm-configuration>

Listing 11 jBPM configuration

With all this changes (Listing 1 - Listing 11) in place, a typical task processing will look as follows (Listing 12):

List<String> actorIds = new LinkedList<String>();
actorIds.add("Peter");
List<TaskInstance> cTasks = jbpmContext.getGroupTaskList(actorIds)
TaskInstance cTask = cTasks.get(0);
AssignableTaskInstance aTask = (AssignableTaskInstance)cTask;
try{
	aTask.reserve("Peter");
	// Save
	jbpmContext.close();
}
catch(Exception e){
	System.out.println("Task " + cTask.getName() + " is already reserved");
	e.printStackTrace();
}

Listing 12 Processing assignable task instance

Here, after getting task instance for a given user and converting it to assignable task instance we are trying to reserve it15. If reservation is successful, we are closing jBPM context in order to commit the transaction.

Implementing nominations

JBoss jBPM makes it very easy to manually assign tasks to specific users. Based on simple APIs, provided by jBPM for moving task instance from one task list to another, assigning task to a given user is fairly straightforward (Listing 13)

List<String> actorIds = new LinkedList<String>();
actorIds.add("admins");
String actorID = "admin";
List<TaskInstance> cTasks = jbpmContext.getGroupTaskList(actorIds);
TaskInstance cTask = cTasks.get(0);
cTask.setPooledActors((Set)null);
cTask.setActorId(actorID);

Listing 13 Reassigning task to a given user

jBPM provides two different APIs for setting pooled actors – one taking a string array of ids and another taking a Set of IDs. For clearing a pool, the one taking a set (with null Set) should be used.

Implementing escalation

As we defined above, an escalation is typically implemented as either a reassignment of the tasks, often with a notification, that an escalation has occurred or notification that a task instance was not completed in time.

Escalation through reassignment

Although jBPM does not directly support escalation, it provides two basic mechanisms – timeout and reassignment (see above). Although it seems like combining the two for implementing escalation would be straightforward, a closer look reveals several complications:

  • Relationships in jBPM implementations are not always bidirectional. For example, based on a task node, we can find all tasks, defined by this node, but given a task, there is no API to find containing task node16; given a task instance, you can get a task, but there is no API to get all the instances for a given task; etc.
  • Timeouts are not on the tasks themselves, but rather on the task node. Because a given node can have multiple tasks, associated with it and jBPM relationships implementations are not bidirectional (see above) additional support is required to keep track of current task instances.

The overall implementation17 of escalation through reassignment involves three handlers:

  • Assignment handler responsible for assign actors to the task. This handler keeps track of whether it is a first or escalated task invocation. A sample assignment handler is presented at Listing 14.
package com.sample.action;

import org.jbpm.graph.def.Node;
import org.jbpm.graph.exe.ExecutionContext;
import org.jbpm.taskmgmt.def.AssignmentHandler;
import org.jbpm.taskmgmt.exe.Assignable;


public class EscalationAssignmentHandler implements AssignmentHandler {

	private static final long serialVersionUID = 1L;

	@Override
	public void assign(Assignable assignable, ExecutionContext context)
	throws Exception {


		Node task = context.getToken().getNode();
		if(task != null){
			String tName = task.getName();


			String vName = tName + "escLevel";
			Long escLevel = (Long)context.getVariable(vName);
			if(escLevel == null){
				// First time through
				assignable.setActorId("admin");
			}
			else{
				// Escalate
				assignable.setActorId("bob");
			}
		}
	}


}

Listing 14 Sample assignment handler

Here we are trying to get a process variable, containing the count of escalation for a given task. If the variable is not defined, then an “admin” is assigned as a task owner, otherwise, a task is assigned to “bob”. Any other assignment strategy can be used in the handler.

  • Task Instance creation action handler (Listing 15), storing an id of a task instance in process instance context
package com.sample.action;


import org.jbpm.graph.def.ActionHandler;
import org.jbpm.graph.def.Node;
import org.jbpm.graph.exe.ExecutionContext;
import org.jbpm.taskmgmt.exe.TaskInstance;


public class TaskCreationActionHandler implements ActionHandler {


	private static final long serialVersionUID = 1L;


	@Override
	public void execute(ExecutionContext context) throws Exception {

		Node task = context.getToken().getNode();
		TaskInstance current = context.getTaskInstance();
		if((task == null) || (current == null))
			return;


		String tName = task.getName();
		String iName = tName + "instance";


		context.setVariable(iName, new Long(current.getId()));
	}



}

Listing 15 Task instance creation handler

  • Timeout handler (Listing 16) is invoked when a task node timer is fired.
package com.sample.action;


import org.jbpm.graph.def.ActionHandler;
import org.jbpm.graph.def.GraphElement;
import org.jbpm.graph.exe.ExecutionContext;
import org.jbpm.taskmgmt.exe.TaskInstance;


public class EscalationActionHandler implements ActionHandler {


	private static final long serialVersionUID = 1L;


	private String escalation;


	@Override
	public void execute(ExecutionContext context) throws Exception {


		GraphElement task = context.getTimer().getGraphElement();
		if(task == null)
			return;


		String tName = task.getName();
		String vName = tName + "escLevel";
		long escLevel = (long)context.getVariable(vName);
		if(escLevel == null)
			escLevel = new long(1);
		else
			escLevel += 1;
		context.setVariable(vName, escLevel);
		String iName = tName + "instance";


		long taskInstanceId = (long)context.getVariable(iName);


		TaskInstance current = 
context.getJbpmContext().getTaskInstance(taskInstanceId);


		if(current != null){
			current.end(escalation);
		}
	}
}

Listing 16 Timeout handler

This handler first pegs the escalation counter and then completes a task instance, associated with this node. Task instance completion is accompanied by a transition (typically back to task node).

A simple example of a process using described above handlers to implement escalation is presented in Listing 17.

<?xml version="1.0" encoding="UTF-8"?>
<process-definition
xmlns="urn:jbpm.org:jpdl-3.2"
name="escalationHumanTaskTest">
<start-state name="start">
<transition to="customTask"></transition>
</start-state>
<task-node name="customTask">
<task name="task2">
<assignment class="com.sample.action.EscalationAssignmentHandler"><
/assignment>
</task>
<event type="task-create">
<action name="Instance Tracking" class="com.sample.action.TaskCreationActionHandler"></action>
</event>
<timer duedate="10 second" name="Escalation timeout">
<action class="com.sample.action.EscalationActionHandler">
<escalation>
escalation
</escalation>
</action>
</timer>

<transition to="end" name="to end"></transition>
<transition to="customTask" name="escalation"></transition>
</task-node>
<end-state name="end"></end-state>
</process-definition>

Listing 17 Simple process of escalation

Escalation through notification

jBPM provides a powerful support for mail delivery18, which makes it very simple to implement escalation through notification. Mail delivery can be triggered by attaching a timer to a node, which uses a scripted mail action for the notification delivery.

Implementing chained execution

Chained execution is directly supported by jBPM swimlines and does not require any additional development.

Conclusion

Despite our best efforts at automation, there will likely always be a need for a “human-in-the-loop” when it comes to complex business processes. In this article I presented a set of established advanced human interaction patterns and showed how easy it is to implement them using jBPM.

1 WS-BPEL Extension for People - BPEL4People

2 This article is based on jBPM 3. jBPM 4 introduces some extensions to a task support.

3 Task definition specifies an assignment definition. The actual assignment happens when a task instance is created. See task instance below.

4 This issue is alleviated in jBPM 4 through introducing API to take the task in order to complete it.

5 See chained execution pattern above.

6 jBPM task nodes development

7 Orchestrating Long Running Activities with JBoss / JBPM

8 Unlike process variables, jBPM task controller handler variables are kept in memory. Consequently, if it is necessary to allow multiple accesses to a task instance supporting saving of the intermediate execution results, usage of a task controller handler might be a wrong solution.

9 See InfoQ article for implementation of parallel loop.

10 This requires adding create-tasks="false" attribute to the task node.

11 JBPM best practices

12 Technically, usage of task handler approach would do what I needed and required less code, but it is not compatible with escalation implementation (below). I also wanted to show how to modify default task and task instance implementation.

13 Similar to jPDL 4 implementation.

14 When creating hibernate mapping for custom task instance, it is much simpler to implement mapping as a subclass of a default class interface. Existing jBPM hibernate mapping relies heavily on task instance mapping, consequently subclassing standard implementation will minimize changes required for introduction of new task instance.

15 There is still a possibility to have racing conditions, but its minimal.

16 This can be done using Hibernate Queries, but there is no direct APIs.

17 This implementation assumes, that a given task node contains a single task, which creates a single task instance.

18 Jbpm Mail delivery

Rate this Article

Adoption
Style

BT