Page History
...
- 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?
As the plain messages we currently use don't work well with tasks, we figured out two options
- Using jBPM User Tasks
- PRO
- would be the "proper" way to implement manual tasks in jBPM (we currently fake them with a Service Task, that is a task usually performed by an automated system, which sends a message to Pulse)
- we'd take advantage of all the properties already available there, such as groupId, actorId, comment, etc., no need to reinvent the wheel
- CONTRA
- no clear documentation on how to integrate it - one is basically left with the source code only. TODO verify that this claim is actually true
- what about tasks independent of a workflow engine presence?
- how to make the workflow engine aware of Magnolia groups and users?
- PRO
- Extending the Message class into a generic Task
- PRO
- would handle any type of task, regardless of a workflow engine presence
- easier to persist and query, most of the job already being done by the MessageStore API
- CONTRA
- would make our workflow solution still rely on our "fake" UserTask
- PRO
...
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
.
Code Block | ||||
---|---|---|---|---|
| ||||
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
Code Block | ||||
---|---|---|---|---|
| ||||
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
Anchor | ||||
---|---|---|---|---|
|
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:
Code Block | ||||
---|---|---|---|---|
| ||||
/**
* 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 )
Code Block |
---|
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>