Page tree
Skip to end of metadata
Go to start of metadata

Intro

This concept is meant to illustrate what is needed in technical terms in order to implement the new UX Pulse concept with particular reference to the workflow.

Problem

At the moment, work items or tasks as we will call them from now on (work item is very jBPM specific) are treated like any informational Pulse message (i.e. errors, warnings) and there's no way to know what the status of a given item is, unless one tries to perform an action on it. What's worse, one can delete a work item without handling it, which might basically break the workflow, leaving a process pending forever, unless some other user in the same group the task has been assigned to, takes it over. 

The UX concept puts special emphasis on the handling of workflow tasks, how to display their current state (i.e. who's in charge, whether it has been already handled or not, what the outcome was, etc.), how to perform the necessary actions so that each workflow process can be successfully carried through. It also worth mentioning that, as specified in the UX concept "The user does not see the entire work flow and its current state in Pulse - there's actually a separate app for that".

 

Workflow and Pulse interaction

Here we briefly sketch how Workflow works and how it currently interacts with Pulse 

Workflow
  • workflow process defined as bpmn 2.0 (Business Process Model and Notation) file
  • each process is registered by name at startup and made available to the workflow engine, see /modules/workflow/workflows
  • each step in the workflow is a workitem defined in a wid (work item definition, a jBPM specific file), e.g. MgnlDefinitions.wid
  • each work item is mapped to a work item handler. Handlers are under workItemHandlers nodes, eg. /modules/workflow-jbpm/workItemHandlers
  • handlers implement WorkItemHandler, they are a Java class executed at runtime when the workflow reaches the corresponding work item or step
  • workflows are launched via commands, e.g. /modules/workflow/workflows/activate which has a workflow property referring to the name of the workflow to launch
Interaction example with Pulse
  • activation workflow is defined as SimpleActivation.bpmn 

  • its first workitem is called publishNotification which maps to a NotificationWorkItemHandler class
  • when activation workflow is started, publishNotification is executed
  • publishNotification populates a map of arbitrary data with
    • the work item id 
    • the process id this work item belongs to
    • who started the process
    • who's been assigned to
    • the path to activate
    • if activation is recursive
    • publication date in case of scheduled activation
    • the message view to handle the item in Pulse
  • publishNotification creates a Message with the above data and sends it to Pulse
  • user opens the message in Pulse and decides to approve or reject
    • in either case a CompleteTaskAction is triggered and the decision is passed back to the workflow engine in a results map

Tasks inbox

Of the many new things introduced by the Pulse UX concept, it was agreed that the first thing to tackle, which would solve most of the problems currently affecting the workflow, is the new Tasks inbox. As illustrated by the mockup below, the main novelty is the sub tabs (all items, not started, ongoing, done) introducing filters displaying the status of the each task assigned to the current user or the group he or she is part of. An additional status column has been introduced too.

 

  • we want to be able to display tasks independent of the presence of an underlying workflow engine
    • i.e a one-shot task (e.g. a short translation assignment given to a particular user) for which setting up a whole workflow process would be overkill.
  • we want to be able to retrieve the status of a given task
  • ideally we would like the list to be updated automatically as new tasks arrive, are taken, updated or completed. 

jBPM User Tasks

What is a User Task?

From the official jBPM documentation:

Processes can also involve tasks that need to be executed by human actors. A User Task represents an atomic task to be executed by a human actor. It should have one incoming connection and one outgoing connection. User Tasks can be used in combination with Swimlanes to assign multiple human tasks to similar actors. Refer to the chapter on human tasks for more details. A User Task is actually nothing more than a specific type of service node (of type "Human Task"). A User Task contains the following properties:


Instead of using human tasks for interacting with human actors Magnolia decided to leave this concept out and use Service Tasks for sending messages to The Pulse.

But what exactly is the difference between a Service Tasks and Human Tasks?

From a technical point of view: None.

Both Tasks are handled by a WorkItemHandler which implement the same interface.

When modelling your process it does matter. A User Task gives you much more possibilities to set parameters tailored for user interactions:

Further more User Tasks allow defining Swim lanes inside your process. A swim lane allows you to define multiple steps or user tasks inside your process to be automatically assigned to the first user who picks up the first task of the swim lane.

Note: Human Tasks allow setting a Task Name, which could be used to define the further visual representation or the actions available for a certain task in Magnolia. This is not the case for other Service Tasks, which is why you end up creating a handler per service task. What the Implementation field can be used for has to be clarified.

Human Task WorkItemHandler 

The handler is simply registered in the RegisterableItemsFactory using Human Task as identifier. Magnolia's 5.3 version of workflow already provides a custom class called RegisteredItemsFactory.

 

org.jbpm.runtime.manager.impl.DefaultRegisterableItemsFactory
    public Map<String, WorkItemHandler> getWorkItemHandlers(RuntimeEngine runtime) {
	    WorkItemHandler handler = getHTWorkItemHandler(runtime);
        defaultHandlers.put("Human Task", handler);
		...
	}
 
    protected WorkItemHandler getHTWorkItemHandler(RuntimeEngine runtime) {
        
        ExternalTaskEventListener listener = new ExternalTaskEventListener();
        LocalHTWorkItemHandler humanTaskHandler = new LocalHTWorkItemHandler();

        humanTaskHandler.setRuntimeManager(((RuntimeEngineImpl)runtime).getManager());
        if (runtime.getTaskService() instanceof EventService) {
            ((EventService)runtime.getTaskService()).registerTaskEventListener(listener);
        }
		...
   }
       

What the Handler does internally is:

  • create a Task based on the parameters provided by the workItem
  • create the ContentData based on parameters from the workItem (? This data is then altered by the user executing the task and later on appended as result map to the process)
  • add the task to the TaskService
  • claim the task is the task is part of a swim lane, otherwise an actor would have to manually claim it
HTWorkItemHandler
public class LocalHTWorkItemHandler extends AbstractHTWorkItemHandler {
   
    @Override
    public void executeWorkItem(WorkItem workItem, WorkItemManager manager) {
        

        Task task = createTaskBasedOnWorkItemParams(ksessionById, workItem);
        ContentData content = createTaskContentBasedOnWorkItemParams(ksessionById, workItem);
        try {
            long taskId = ((InternalTaskService) runtime.getTaskService()).addTask(task, content);
            if (isAutoClaim(workItem, task)) {
                runtime.getTaskService().claim(taskId, (String) workItem.getParameter("SwimlaneActorId"));
            }
        } 
        ...
    }
        
    }

At this point the WorkitemHandler is finished. It does not complete the WorkItem and for now the process is paused at this stage. Even more the whole execution of the task is decoupled from the process execution and has a completely separated life cycle. This lifecycle is taken care of by the TaskService.

Proceed execution of process after User tasks completes

Because the User Tasks lifecycle is decoupled, the process needs to be informed when a task has been completed and allow the process to proceed its execution. This is is done exactly the same way other tasks are completed inside the process by notifying the WorkItemManager (manager.completeWorkItem()), but as part of the HTWorkItemHandler it is triggered by an event fired by LifeCycleManager and handled by e.g. org.jbpm.services.task.wih.ExternalTaskEventListener#processTaskState. The Listener is registered when registering the HTWorkItemHandler inside the RegisterableItemsFactory.

org.jbpm.services.task.wih.ExternalTaskEventListener#processTaskState

TaskService

The TaskService is registered by the RuntimeManager to the RuntimeEngine. Magnolia provides it's own RuntimeManager implementation for easy registration of a custom TaskService Implementation. It's purpose is to handle the lifecycle of User Tasks, which is pretty obvious when looking at its interface:

TaskService
/**
 * The Task Service Entry Point serves as 
 *  facade of all the other services, providing a single entry point
 *  to access to all the services
 */
public interface TaskService {
    
    void activate(long taskId, String userId);
    void claim(long taskId, String userId);
    void claimNextAvailable(String userId, String language);
    void complete(long taskId, String userId, Map<String, Object> data);
    void delegate(long taskId, String userId, String targetUserId);
    void exit(long taskId, String userId);
    void fail(long taskId, String userId, Map<String, Object> faultData);
    void forward(long taskId, String userId, String targetEntityId);
    Task getTaskByWorkItemId(long workItemId);
    Task getTaskById(long taskId);
    List<TaskSummary> getTasksAssignedAsBusinessAdministrator(String userId, String language);
    List<TaskSummary> getTasksAssignedAsPotentialOwner(String userId, String language);
    List<TaskSummary> getTasksAssignedAsPotentialOwnerByStatus(String userId, List<Status> status, String language);
    List<TaskSummary> getTasksOwned(String userId, String language);
    List<TaskSummary> getTasksOwnedByStatus(String userId, List<Status> status, String language);
    List<TaskSummary> getTasksByStatusByProcessInstanceId(long processInstanceId, List<Status> status, String language);
    List<Long> getTasksByProcessInstanceId(long processInstanceId);
    
    List<TaskSummary> getTasksByVariousFields( List<Long> workItemIds, List<Long> taskIds, List<Long> procInstIds, 
            List<String> busAdmins, List<String> potOwners, List<String> taskOwners, 
            List<Status> status, boolean union);
    
    List<TaskSummary> getTasksByVariousFields(Map <String, List<?>> parameters, boolean union);
    
    long addTask(Task task, Map<String, Object> params);
    void release(long taskId, String userId);
    void resume(long taskId, String userId);
    void skip(long taskId, String userId);
    void start(long taskId, String userId);
    void stop(long taskId, String userId);
    void suspend(long taskId, String userId);
    void nominate(long taskId, String userId, List<OrganizationalEntity> potentialOwners);
    Content getContentById(long contentId);
    Attachment getAttachmentById(long attachId);
    
}

 

UML showing the default jBPM implementation

This diagram shows how User Tasks are persisted in JPA. Please compare this with the current implementation to see where Magnolia's implementation can easily be extended for a custom TaskService implementation.

Magnolias Approach

Introduce own implementation of TaskService which delegates to Magnolia's TaskManager.

By choosing this approach we would sacrifice the idea of having a complete integration of jBPM (in case that idea ever was there..) in Magnolia. But, as part of workflow 5.3 we have laid the ground to hook into jBPM's engine at the right spots and provide custom, yet slimmed down implementations targeted towards our needs and most probably towards 97,6% of our customers needs. Still if somebody would need to have the full persistence of User Tasks as specified by jBPM registering the default CommandBasedTaskService based on a custom TaskPersistenceContext is possible, in theory.

 

Update

During implementation we realised that this will be more complicated than necessary. While registering the TaskService to the RuntimeEngine would be nice, the gain is not much more than being a bit more consistent with jBPM. And when looking at the TaskService interface we would somehow have to wrap a lot of objects to work with our own Task's implementation, which should be part of the UI.

So instead of implementing this the TaskService interface and registering it to the RuntimeEngine we are going to create a custom TaskManager interface (naming due to consistency with MessageManager/MessageStore).

Compared to the first diagram, the TaskManager is not registered to the Runtime, but it is injected into the HTWorkItemHandler. The LifeCycleManager which takes care of notifying the process about completed tasks is replaced by TasksEventManager, which is part of the UI project and allows registering EventHandlers. This could e.g. be used to register an handler which sends mail every time a task is created, or in our case we register an handler, which takes care of notifying the process about completed user tasks.

This approach allows a very slim integration with Magnolia's TaskManager. The whole Task Management is a standalone, reusable part of the UI project and for jBPM we communicate with the standard TaskManager interfaces using the HTWorkItemHandler and event handlers registered to the TaskEventmanager

Input from architecture meeting

3.4.2014: Task related classes should go to a separate module in main

Possible Risks and Problems

Accessing the TaskManager

Accessing the TaskManager is different than in jBPM. In jBPM the TaskService is bound to a RuntimeEngine and can be obtained like this:

long taskId = ((InternalTaskService) runtime.getTaskService()).addTask(task, content);

This will not be doable with our approach. 

Extending the Task object

How to extend the Task object by workflow specific parameters, how to store it?

 

Other Possible approaches

JcrTaskPersistenceContext

By implementing a JCR implementation of the TaskPersistenceContext we would be able to create a complete storage for User Tasks. This would be the recommended approach in case we wanted to provide a complete integration of jBPM in Magnolia where customers would be able to take full advantage of jBPM and possibly also integrate 3rd party clients to connect to Magnolia's engine.

Having the persistence in place still leaves questions open on how to actually interact with the tasks. We would have to access the stored tasks, and provide a limited set of possibilities to manipulate them. 

The JPATaskPersistenceContext alone is around 550 LOC and all it does is delegate the to the EntityManager using calls like em.merge(object) em.remove( object )

package org.kie.internal.task.api;

public interface TaskPersistenceContext {

	Task findTask(Long taskId);
	Task persistTask(Task task);
	Task updateTask(Task task);
	Task removeTask(Task task);
	Group findGroup(String groupId);
	Group persistGroup(Group group);
    Group updateGroup(Group group);
	Group removeGroup(Group group);
	User findUser(String userId);
	User persistUser(User user);
	User updateUser(User user);
	User removeUser(User user);
	OrganizationalEntity findOrgEntity(String orgEntityId);
	OrganizationalEntity persistOrgEntity(OrganizationalEntity orgEntity);
	OrganizationalEntity updateOrgEntity(OrganizationalEntity orgEntity);
	OrganizationalEntity removeOrgEntity(OrganizationalEntity orgEntity);
	Content findContent(Long contentId);
	Content persistContent(Content content);
	Content updateContent(Content content);
	Content removeContent(Content content);
	Attachment findAttachment(Long attachmentId);
	Attachment persistAttachment(Attachment attachment);
	Attachment updateAttachment(Attachment attachment);
	Attachment removeAttachment(Attachment attachment);
	Comment findComment(Long commentId);
	Comment persistComment(Comment comment);
	Comment updateComment(Comment comment);
	Comment removeComment(Comment comment);
	Deadline findDeadline(Long deadlineId);
	Deadline persistDeadline(Deadline deadline);
	Deadline updateDeadline(Deadline deadline);
	Deadline removeDeadline(Deadline deadline);
	
	/*
	 * Query related methods
	 */
	
	...
    
    /*
     * Following are optional methods that are more like extension to 
     * default data model to allow flexible add-ons
     */
	<T> T persist(T object);
	<T> T find(Class<T> entityClass, Object primaryKey);
	<T> T remove(T entity);
    <T> T merge(T entity);
    
    /*
     * life cycle methods 
     */
    boolean isOpen();
    void joinTransaction();
    void close();
}

Custom approach

Continue down the road we chose and ignore User Tasks:

<Insert magic here>