Ad Hoc Task in jBPM 5

1

June 26, 2011 by huionn

Case Management generally refers to

“a collaborative process of assessment, planning, facilitation and advocacy for options and services to achieve an objective through communication and available resources to promote quality cost-effective outcomes”

(The definition is modified from definition of Case Management by Case Management Society of America in the context of health care)

Compared to Business Process Management (BPM), Case Management is ad hoc by nature. A task or activity may need to be created on the fly based on runtime conditions.

Consider a example scenario, a manager is assigned a task to prepare a proposal for a project tender. When the manager start his analysis, he find that he lacks 2 key information. So, he asks 2 of his subordinates to do the study on the subjects.


In BPMN, there is a concept of Ad-Hoc (Sub-)Process:

image

An Ad-Hoc Sub-Process is a specialized type of Sub-Process that is a group of Activities that have no REQUIRED sequence relationships. A set of Activities can be defined for the Process, but the sequence and number of performances for the Activities is determined by the performers of the Activities.

~ BPMN 2.0 spec

Based on this definition and its semantic, ad-hoc sub-process is not truly ad-hoc as its activities need to be predefined beforehand. Just its sequence and triggered condition are flexible compared to normal process.


Web Services – Human Task (WS-HumanTask) is a complementary specification.

“The WS-HumanTask specification introduces the definition of human tasks and notifications, including their properties, behavior and a set of operations used to manipulate human tasks. A coordination protocol is introduced in order to control autonomy and life cycle of service-enabled human tasks in an interoperable manner.”

http://en.wikipedia.org/wiki/BPEL4People

After reading its specification, I found that it is a very pragmatic specification to support real-life scenarios. WS-HumanTask fully supports ad-hoc task in a sense that a sub-task can be created during runtime.

jBPM 5 provides support of WS-HumanTask out of the box. So I decide to try to implement the scenario mentioned above. The implementation is actually the combination of a few test cases.

  1. The manager creates and starts 2 sub tasks.
  2. When each sub task is completed, the manager is notified.
  3. After both tasks are completed by the assignees, the manager can retrieve the results and complete his own task.
public class AdHocTaskTest extends BaseTest {
	private static final int DEFAULT_WAIT_TIME = 5000;
	private static final int MANAGER_COMPLETION_WAIT_TIME = DEFAULT_WAIT_TIME;

	private TaskClient client;
	private WSHumanTaskHandler handler;
	private TaskServer server;

	@Override
	protected void setUp() throws Exception {
		super.setUp();
		server = new HornetQTaskServer(taskService, 5446);
		Thread thread = new Thread(server);
		thread.start();
		System.out.println("Waiting for the HornetQTask Server to come up");
		while (!server.isRunning()) {
			System.out.print(".");
			Thread.sleep(50);
		}
		client = new TaskClient(new HornetQTaskClientConnector("client 1",
				new HornetQTaskClientHandler(
						SystemEventListenerFactory.getSystemEventListener())));
		client.connect("127.0.0.1", 5446);
		handler = new WSHumanTaskHandler();
		handler.setClient(client);
	}

	protected void tearDown() throws Exception {
		handler.dispose();
		client.disconnect();
		server.stop();
		super.tearDown();
	}

	public void testAdHocTask() throws Exception {

		TestWorkItemManager manager = new TestWorkItemManager();
		// Create the parent task
		WorkItem workItem = createParentTaskWorkItem();
		handler.executeWorkItem(workItem, manager);

		Thread.sleep(500);

		// Test if the task is succesfully created
		BlockingTaskSummaryResponseHandler responseHandler = new BlockingTaskSummaryResponseHandler();
		client.getTasksAssignedAsPotentialOwner("Darth Vader", "en-UK",
				responseHandler);
		List tasks = responseHandler.getResults();
		assertEquals(1, tasks.size());
		TaskSummary task = tasks.get(0);
		assertEquals("TaskNameParent", task.getName());
		assertEquals(10, task.getPriority());
		assertEquals("CommentParent", task.getDescription());
		assertEquals(Status.Reserved, task.getStatus());
		assertEquals("Darth Vader", task.getActualOwner().getId());

		// Create the child task
		workItem = createSubTaskWorkItem("TaskNameChild1", "CommentChild1",
				"Bobba Fet", task);
		handler.executeWorkItem(workItem, manager);

		Thread.sleep(500);

		// Create the child task2
		Task subTask = createSubTask("TaskNameChild2", "CommentChild2",
				"Jabba Hutt", task);
		BlockingTaskOperationResponseHandler operationResponseHandler = new BlockingTaskOperationResponseHandler();
		client.addTask(subTask, null, null);

		Thread.sleep(500);

		// Start the parent task
		System.out.println("Starting task " + task.getId());
		operationResponseHandler = new BlockingTaskOperationResponseHandler();
		client.start(task.getId(), "Darth Vader", operationResponseHandler);
		operationResponseHandler.waitTillDone(DEFAULT_WAIT_TIME);
		System.out.println("Started task " + task.getId());

		// Check if the parent task is InProgress
		BlockingGetTaskResponseHandler getTaskResponseHandler = new BlockingGetTaskResponseHandler();
		client.getTask(task.getId(), getTaskResponseHandler);
		Task parentTask = getTaskResponseHandler.getTask();
		assertEquals(Status.InProgress, parentTask.getTaskData().getStatus());
		assertEquals(users.get("darth"), parentTask.getTaskData()
				.getActualOwner());

		// Get all the subtask created for the parent task based on the
		// potential owner
		responseHandler = new BlockingTaskSummaryResponseHandler();
		client.getSubTasksByParent(parentTask.getId(), responseHandler);
		List subTasks = responseHandler.getResults();
		assertEquals(2, subTasks.size());
		TaskSummary subTaskSummary1 = subTasks.get(0);
		TaskSummary subTaskSummary2 = subTasks.get(1);
		assertNotNull(subTaskSummary1);
		assertNotNull(subTaskSummary2);

		addEventListener(subTaskSummary1.getId(), task);
		addEventListener(subTaskSummary2.getId(), task);

		// Starting the sub task 1
		System.out.println("Starting sub task " + subTaskSummary1.getId());
		operationResponseHandler = new BlockingTaskOperationResponseHandler();
		client.start(subTaskSummary1.getId(), "Bobba Fet",
				operationResponseHandler);
		operationResponseHandler.waitTillDone(DEFAULT_WAIT_TIME);
		System.out.println("Started sub task " + subTaskSummary1.getId());

		// Starting the sub task 2
		System.out.println("Starting sub task " + subTaskSummary2.getId());
		operationResponseHandler = new BlockingTaskOperationResponseHandler();
		client.start(subTaskSummary2.getId(), "Jabba Hutt",
				operationResponseHandler);
		operationResponseHandler.waitTillDone(DEFAULT_WAIT_TIME);
		System.out.println("Started sub task " + subTaskSummary2.getId());

		// Check if the child task 1 is InProgress
		getTaskResponseHandler = new BlockingGetTaskResponseHandler();
		client.getTask(subTaskSummary1.getId(), getTaskResponseHandler);
		Task subTask1 = getTaskResponseHandler.getTask();
		assertEquals(Status.InProgress, subTask1.getTaskData().getStatus());
		assertEquals(users.get("bobba"), subTask1.getTaskData()
				.getActualOwner());

		// Check if the child task 2 is InProgress
		getTaskResponseHandler = new BlockingGetTaskResponseHandler();
		client.getTask(subTaskSummary2.getId(), getTaskResponseHandler);
		Task subTask2 = getTaskResponseHandler.getTask();
		assertEquals(Status.InProgress, subTask2.getTaskData().getStatus());
		assertEquals(users.get("jabba"), subTask2.getTaskData()
				.getActualOwner());

		// Complete the child task 1
		System.out.println("Completing sub task " + subTask1.getId());
		operationResponseHandler = new BlockingTaskOperationResponseHandler();
		client.complete(subTask1.getId(), "Bobba Fet", null,
				operationResponseHandler);
		operationResponseHandler.waitTillDone(DEFAULT_WAIT_TIME);
		System.out.println("Completed sub task " + subTask1.getId());

		// Complete the child task 2
		System.out.println("Completing sub task " + subTask2.getId());
		operationResponseHandler = new BlockingTaskOperationResponseHandler();
		client.complete(subTask2.getId(), "Jabba Hutt", null,
				operationResponseHandler);
		operationResponseHandler.waitTillDone(DEFAULT_WAIT_TIME);
		System.out.println("Completed sub task " + subTask2.getId());

		// Check if the child task 1 is Completed

		getTaskResponseHandler = new BlockingGetTaskResponseHandler();
		client.getTask(subTask1.getId(), getTaskResponseHandler);
		subTask1 = getTaskResponseHandler.getTask();
		assertEquals(Status.Completed, subTask1.getTaskData().getStatus());
		assertEquals(users.get("bobba"), subTask1.getTaskData()
				.getActualOwner());

		// Check if the child task 2 is Completed

		getTaskResponseHandler = new BlockingGetTaskResponseHandler();
		client.getTask(subTask2.getId(), getTaskResponseHandler);
		subTask2 = getTaskResponseHandler.getTask();
		assertEquals(Status.Completed, subTask2.getTaskData().getStatus());
		assertEquals(users.get("jabba"), subTask2.getTaskData()
				.getActualOwner());

		Thread.sleep(500);

		// check results of 2 sub tasks
		getTaskResponseHandler = new BlockingGetTaskResponseHandler();
		client.getTask(parentTask.getId(), getTaskResponseHandler);
		parentTask = getTaskResponseHandler.getTask();
		// dummy check for sub task results
		if (parentTask.getTaskData().getComments().size() == 2) {
			for (Comment c : parentTask.getTaskData().getComments()) {
				System.out.println("subtask result: " + c.getText());
			}
			operationResponseHandler = new BlockingTaskOperationResponseHandler();
			client.complete(parentTask.getId(), "Darth Vader", null,
					operationResponseHandler);
		}

		// Check is the parent task is Complete
		getTaskResponseHandler = new BlockingGetTaskResponseHandler();
		client.getTask(parentTask.getId(), getTaskResponseHandler);
		parentTask = getTaskResponseHandler.getTask();
		assertEquals(Status.Completed, parentTask.getTaskData().getStatus());
		assertEquals(users.get("darth"), parentTask.getTaskData()
				.getActualOwner());

		assertTrue(manager.waitTillCompleted(MANAGER_COMPLETION_WAIT_TIME));
	}

	private WorkItem createSubTaskWorkItem(String taskName, String comment,
			String actorId, TaskSummary parentTask) {
		WorkItem workItem = new WorkItemImpl();
		workItem.setName("Human Task");
		workItem.setParameter("TaskName", taskName);
		workItem.setParameter("Comment", comment);
		workItem.setParameter("Priority", "10");
		workItem.setParameter("ActorId", actorId);
		workItem.setParameter("ParentId", parentTask.getId());
		return workItem;
	}

	private Task createSubTask(String taskName, String comment, String actorId,
			TaskSummary parentTask) {
		Task task = new Task();
		if (taskName != null) {
			List names = new ArrayList();
			names.add(new I18NText("en-UK", taskName));
			task.setNames(names);
		}
		if (comment != null) {
			List descriptions = new ArrayList();
			descriptions.add(new I18NText("en-UK", comment));
			task.setDescriptions(descriptions);
			List subjects = new ArrayList();
			subjects.add(new I18NText("en-UK", comment));
			task.setSubjects(subjects);
		}
		task.setPriority(10);

		TaskData taskData = new TaskData();
		taskData.setWorkItemId(0);
		taskData.setProcessInstanceId(0);
		// Sub Task Data
		taskData.setParentId(parentTask.getId());

		PeopleAssignments assignments = new PeopleAssignments();
		List potentialOwners = new ArrayList();

		if (actorId != null && actorId.trim().length() > 0) {
			String[] actorIds = actorId.split(",");
			for (String id : actorIds) {
				potentialOwners.add(new User(id.trim()));
			}
			// Set the first user as creator ID??? hmmm might be wrong
			if (potentialOwners.size() > 0) {
				taskData.setCreatedBy((User) potentialOwners.get(0));
			}
		}

		assignments.setPotentialOwners(potentialOwners);
		List businessAdministrators = new ArrayList();
		businessAdministrators.add(new User("Administrator"));
		assignments.setBusinessAdministrators(businessAdministrators);
		task.setPeopleAssignments(assignments);

		task.setTaskData(taskData);

		return task;
	}

	private WorkItem createParentTaskWorkItem() {
		WorkItem workItem = new WorkItemImpl();
		workItem.setName("Human Task");
		workItem.setParameter("TaskName", "TaskNameParent");
		workItem.setParameter("Comment", "CommentParent");
		workItem.setParameter("Priority", "10");
		workItem.setParameter("ActorId", "Darth Vader");
		// Set the subtask policy
		workItem.setParameter("SubTaskStrategies",
				"OnParentAbortAllSubTasksEnd");
		return workItem;
	}

	private void addEventListener(long taskId, TaskSummary parentTask) {
		EventKey key = new TaskEventKey(TaskCompletedEvent.class, taskId);
		TaskCompletedHandler handler = new TaskCompletedHandler(parentTask);
		client.registerForEvent(key, true, handler);
	}

	private class TestWorkItemManager implements WorkItemManager {

		private volatile boolean completed;
		private volatile boolean aborted;
		private volatile Map results;

		public synchronized boolean waitTillCompleted(long time) {
			if (!isCompleted()) {
				try {
					wait(time);
				} catch (InterruptedException e) {
					// swallow and return state of completed
				}
			}

			return isCompleted();
		}

		public void abortWorkItem(long id) {
			setAborted(true);
		}

		private synchronized void setAborted(boolean aborted) {
			this.aborted = aborted;
			notifyAll();
		}

		public void completeWorkItem(long id, Map results) {
			this.results = results;
			setCompleted(true);
		}

		private synchronized void setCompleted(boolean completed) {
			this.completed = completed;
			notifyAll();
		}

		public synchronized boolean isCompleted() {
			return completed;
		}

		public void registerWorkItemHandler(String workItemName,
				WorkItemHandler handler) {
		}

	}

	private class TaskCompletedHandler extends AbstractBaseResponseHandler
			implements EventResponseHandler {
		private final TaskSummary parentTask;

		public TaskCompletedHandler(TaskSummary parentTask) {
			this.parentTask = parentTask;
		}

		public void execute(Payload payload) {
			TaskEvent event = (TaskEvent) payload.get();
			long taskId = event.getTaskId();
			System.out.println("notify parent task owner - "
					+ parentTask.getActualOwner() + ": Task completed "
					+ taskId);
			client.getTask(taskId, new GetCompletedTaskResponseHandler(
					parentTask));
		}

		public boolean isRemove() {
			return true;
		}
	}

	private class GetCompletedTaskResponseHandler extends
			AbstractBaseResponseHandler implements GetTaskResponseHandler {
		private final TaskSummary parentTask;

		public GetCompletedTaskResponseHandler(TaskSummary parentTask) {
			this.parentTask = parentTask;
		}

		public void execute(Task task) {
			Comment comment = new Comment();
			comment.setText("result of sub task " + task.getId() + ": OK ("
					+ task.getTaskData().getActualOwner().getId() + ")");
			comment.setAddedAt(new Date());
			comment.setAddedBy(task.getTaskData().getActualOwner());
			client.addComment(parentTask.getId(), comment, null);
		}
	}
}

I purposely create programmatically 2 sub tasks in 2 different ways – 1) executeWorkItem and 2) instantiate new Task directly.

I also have to modify OnParentAbortAllSubTasksEndStrategy as it does not behave as is its name implies. (I wish I can plug in my own SubTasksStrategy but it is impossible at the moment.)

public void execute(TaskServiceSession taskServiceSession, TaskService service, Task task) {
    	if (task.getTaskData().getStatus() == Status.Obsolete) {

I implement a customized TaskCompletedHandler (registered to TaskCompletedEvent) to add the comment from completed subtask to parent task. I choose to store the result as comment (possibly in json/xml or MVEL format for complicated result). Alternatively, it is possible to store the result as Attachment too. As shown in jBPM example – org.jbpm.examples.humantask.HumanTaskExample, the correct way of passing the results is through ContentData in TaskClient.complete(…). The result is mapped to ProcessInstance variables with “Result Mapping” in bpmn file. (The design of ContentData is not intuitive. In my opinion, it should have MapContentDate with constructor MapContentData(Map) or some sort of factory method.).

After this simple experiment, I think jBPM 5 is powerful, flexible and has clean design. Unfortunately, there are no much articles and tutorials for reference on the web (google returns mostly jBPM 3 & 4 related results). Most examples are available from plugtree (salaboy and esteban). I have to dig into its test cases and source codes to understand how jBPM 5 works in details.

Advertisements

One thought on “Ad Hoc Task in jBPM 5

  1. […] on the discussion in http://community.jboss.org/thread/162963, I am able to rewrite https://huionn.wordpress.com/2011/06/26/ad-hoc-task-in-jbpm-5/ to be in-memory task service (instead of using task server through […]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: