1. Introduction
1.1. Overview
This introduction will show you how edoras gear can be used to support the development of work management applications, starting with lightweight ad-hoc tasks and progressing in simple steps to full-featured case management and automated process execution. The goal is to provide an overview of the features provided by edoras gear and to show how they fit together to create a comprehensive and flexible work management framework.
edoras gear is easy to integrate into other applications that may use a wide variety of technologies to provide a user interface. We will not show any details of the integration or GUI code here, as it would just add noise and distract from the topics that we are really interested in. We will, however, include code snippets and use some mock-ups of typical user interfaces to show how edoras gear allows us not just to support the technical implementation of business workflows but also to improve the user experience.
As this is an introductory guide there will be many details that cannot be covered, so if you need more information on a particular topic please refer to the full edoras gear user guide. |
If you want to try out the ideas presented here, a good starting point is the edoras gear basic setup document which describes the steps needed to set up a working edoras gear environment. Note that although this document contains a lot of code snippets, it does not show a single complete worked example that can be entered into an IDE and executed. |
1.2. Starting out: task management
One day you get a call from your friend Mark who runs a small design company. He would like to bring some order into the way his team arranges business trips, as there have been a few problems recently and he worries that as his team grows that the problems will grow too, and that he will lose control over which trips are being planned and how much they are costing.
The company is currently organised into three groups: Mark is the sole manager, with a design team and two administrators to take care of finances, travel arrangements etc.:
At the moment travel requests are simply sent to the administration department as e-mails, for example:
To: Andy
From: Dave
Subject: Travel request for Berlin: 12/3 - 14/3
===========
Could you please book a flight to Berlin for me, flying on the morning of the 12th March and returning
on the evening of 14th.
Many thanks!
Dave
If there are questions, these may be resolved either by e-mail or verbally. Details of booked travel and hotels are generally also sent by e-mail. This ad-hoc approach works well most of the time, but there are a number of potential problems:
-
the e-mails for booking travel can simply get lost among the other messages in an administrator’s mailbox.
-
there is no one place where all the information relating to a particular travel request is collected.
-
if one of the administrators is away, then open travel requests can just sit in a mailbox that is not being read.
-
there is no easy way to get an overview of what travel request are being made.
Obviously some sort of improved process will be required, but first of all let’s start by simply transferring the current process to edoras gear almost as it stands. We can then make step-by-step improvements as we learn more about edoras gear’s features.
1.2.1. Basic setup
If you want to try out the code snippets presented in this document then you should set up an environment as described in
the edoras gear basic setup documentation. For our first examples we will use an even more basic application configuration
that doesn’t include the process engine. The gear:activiti-process-engine
and gear:process-management
configurations can be deleted for now, and we can use the default task
management settings:
<!-- Task Management definition -->
<gear:task-management id="taskManagement"/>
1.2.2. Users and Groups
Our first step is to create a representation of the users and groups involved. To do this we use the UserId
and GroupId
classes provided by edoras gear.
We can define a simple service interface to look up users and groups and provide the IDs that we require:
import com.edorasware.commons.core.entity.GroupId;
import com.edorasware.commons.core.entity.UserId;
public interface DesignCompanyUserService {
UserId lookupUserId(String userName);
GroupId lookupGroupId(String groupName);
}
In a real-life solution we would probably interface to a user management system (e.g. LDAP) which can not only validate that the user is known to the system but also provide more information such as which group(s) the user belongs to. For the moment, however, we just want to explore the task management functionality, so we can just create our IDs on-the-fly with no reference to an external system:
public class MockDesignCompanyUserService implements DesignCompanyUserService {
@Override
public UserId lookupUserId(String userName) {
return UserId.get(userName);
}
@Override
public GroupId lookupGroupId(String groupName) {
return GroupId.get(groupName);
}
}
Note that there is no dependency on a specific user management framework, we just need to be able to create unique identifiers to represent each user and group.
To make the user service available to our application, we need to add this mock user service to the application configuration:
<!-- Our test user service -->
<bean id="userService" class="com.edorasware.gear.documentation.introduction.MockDesignCompanyUserService"/>
1.2.3. Identity management
The identity management component is responsible for managing the information about the environment in which the application is running, and supports two services:
-
CurrentUserService
is used to obtain information about the user that is executing the current action. -
CurrentTenantService
is used to obtain information about the current tenant. A tenant is a separate data space within which work objects can be created and manipulated. A single edoras gear instance can support multiple tenants at the same time but by default will run in a single-tenant mode. Multi-tenant support is fully transparent to the developer and does not require any special handling.
The current user service is used when creating or updating work objects to maintain a simple audit log and this information is available through the work object interface. For more details please refer to the edoras gear user guide. |
For our initial investigations we will simply use the default identity management settings, which provide a single tenant without any information about the current user:
<!-- Identity Management definition -->
<gear:identity-management id="identityManagement"/>
At this point we should have the complete application configuration needed for our first implementation:
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright (c) 2013. edorasware ag.
-->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:gear="http://www.edorasware.com/schema/gear"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.edorasware.com/schema/gear
http://www.edorasware.com/schema/gear/edoras-gear-3.0.2.S66.xsd">
<!-- License Management definition-->
<gear:license-management id="licenseManagement"/>
<!-- Persistence bean definitions -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="dataSource" class="org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseFactoryBean">
<property name="databaseType" value="H2"/>
<property name="databaseName" value="Introduction"/>
</bean>
<!-- configure edoras database schema service/manager -->
<bean id="databaseSchemaService" class="com.edorasware.commons.core.persistence.schema.internal.DefaultDatabaseSchemaService">
<constructor-arg name="dataSource" ref="dataSource"/>
<constructor-arg name="migrationsLocation" value="com/edorasware/commons/core/persistence/schema"/>
<constructor-arg name="transactionManager" ref="transactionManager"/>
</bean>
<bean id="databaseSchemaManager" class="com.edorasware.commons.core.persistence.schema.internal.StrategyBasedDatabaseSchemaManager" init-method="initialize">
<constructor-arg name="databaseSchemaService" ref="databaseSchemaService"/>
<constructor-arg name="strategy" value="CREATE_DROP"/>
</bean>
<bean id="databaseSchemaManagerLifecycleBean" class="com.edorasware.gear.core.persistence.schema.DatabaseSchemaManagerLifecycleBean">
<constructor-arg name="databaseSchemaManager" ref="databaseSchemaManager"/>
</bean>
<!-- Persistence Management definition -->
<gear:persistence-management id="persistenceManagement" database-schema-creation-strategy="CREATE_DROP"/>
<!-- tag::snippet0[] -->
<!-- Task Management definition -->
<gear:task-management id="taskManagement"/>
<!-- end::snippet0[] -->
<!-- tag::snippet1[] -->
<!-- Our test user service -->
<bean id="userService" class="com.edorasware.gear.documentation.introduction.MockDesignCompanyUserService"/>
<!-- end::snippet1[] -->
<!-- tag::snippet2[] -->
<!-- Identity Management definition -->
<gear:identity-management id="identityManagement"/>
<!-- end::snippet2[] -->
<!-- WorkObject Management definition -->
<gear:work-object-management id="workObjectManagement"/>
</beans>
We now have all the infrastructure needed to create our first test, and we can use Spring to insert references to the services that we need into our test class:
@Inject
private DesignCompanyUserService userService;
@Inject
private TaskService taskService;
protected DesignCompanyUserService getUserService() {
return this.userService;
}
protected TaskService getTaskService() {
return this.taskService;
}
We can also create some test values that will be used frequently within our examples:
protected UserId andyId;
protected UserId annaId;
protected UserId daveId;
protected GroupId adminId;
protected GroupId managementId;
private Date departureDate;
private Date returnDate;
@Before
public void initializeUsersAndGroups() {
this.andyId = getUserService().lookupUserId("andy");
this.annaId = getUserService().lookupUserId("anna");
this.daveId = getUserService().lookupUserId("dave");
this.adminId = getUserService().lookupGroupId("admin");
this.managementId = getUserService().lookupGroupId("management");
this.departureDate = parseDate("2013-03-20 08:00");
this.returnDate = parseDate("2013-03-27 17:00");
}
1.2.4. Creating a simple travel request task
Now that we have set up the basic infrastructure and defined some test values we can transfer the existing e-mail-based process to edoras gear.
The main objects managed by edoras gear are called work objects. We will see various work object types during the course of this
introduction, but for now we only need some Task
instances (from the package com.edorasware.gear.core.task
). A task represents some unit of work that needs to be
performed, whether manually or automatically.
New work objects are created using an appropriate work object builder. Using a task builder we can create a direct equivalent of the e-mail-based travel request:
return Task.builder()
.name("Travel request for Berlin: 12/3 - 14/3")
.description("please book a flight to Berlin for me, ...")
.assigneeId(this.andyId)
.ownerId(this.daveId)
.build();
This creates an in-memory Task
instance with the information we require, but if the
application now terminates then the in-memory data will be lost, so we need to save a copy of this task in the database.
Each work object type has a corresponding work object service which provides various methods for
creating and manipulating work objects. In our Spring configuration we defined a task management bean, which provides the
task service that we autowired earlier. We can now use this service to save a copy of our task:
TaskId taskId = getTaskService().addTask(travelTask, "first travel request");
The TaskId
value returned by this call permanently identifies our new task instance so that we can retrieve it again later as required.
We can now create the first version of the user interface to enter a new travel request using the methods that we have just described. We assume that the owner can be set using the current user’s login details:
1.2.5. Owners and Assignees
The meaning of the owner and assignee fields isn’t rigidly defined by edoras gear, and so there is some flexibility in how they can be interpreted by any particular application. Tasks will typically be moved between several different users over time, either because different expertise is required, because a particular team member is overloaded, or because a task has to be escalated to someone with more responsibility. Generally the owner will be the user that is ultimately responsible, and the assignee will be the user that this responsibility has been (temporarily) delegated to.
In these examples we have defined the owner as the originator of the travel request, and the assignee as the administrator currently responsible for the booking.
1.2.6. Searching for tasks
So far, so good. We can now create new travel request tasks and store them in edoras gear, but for a real application we also need to retrieve tasks that have already been created. edoras gear provides various search methods to retrieve work objects, as different use cases will need to search for tasks in different ways:
-
when a task is selected then we want to fetch the details of that specific object
-
an administrator needs to find their own open tasks
-
a manager may be interested in how many open tasks there are overall, without knowing the details
-
…
and so on …
The various search methods are also provided by the work object service (in this case the task service). In cases where we already know the task ID, retrieving the work object is straightforward:
// find a specific task given the task ID
Task task = getTaskService().findTaskById(taskId);
For more complex use cases the edoras gear work object services provide a powerful predicate-based search mechanism. A predicate is a simple conditional operation that returns true or false based on the contents of a specific work object. The edoras gear query API provides a number of predefined constants that can be used to build predicate search queries. For example a predicate search to find all tasks assigned to a specific user could be executed as follows:
// find all tasks assigned to Andy
Predicate assigneePredicate = Task.ASSIGNEE_ID.eq(this.andyId);
List<Task> tasks = getTaskService().findTasks(assigneePredicate);
Although each predicate is simple on its own, predicates can be combined using and
or or
operators to create arbitrarily complex searches:
// find all tasks assigned to Andy or Anna with a name that starts with "Travel request for "
Predicate assigneePredicate = Task.ASSIGNEE_ID.in(this.andyId, this.annaId);
Predicate namePredicate = Task.NAME.like("Travel request for *");
Predicate combined = Predicates.and(namePredicate, assigneePredicate);
List<Task> tasks = getTaskService().findTasks(combined);
There are two ways to combine predicates. The example shown here uses the explicit |
In some cases only the number of matching tasks is required, not the actual task objects; in this case the countTasks()
method can be used:
// count the tasks assigned to Andy
Predicate assigneePredicate = Task.ASSIGNEE_ID.in(this.andyId);
long assignedTaskCount = getTaskService().countTasks(assigneePredicate);
Many more search predicates are provided than can be presented here. For more details please refer to the edoras gear user guide. |
Using the work object search functionality we can now search for the travel request tasks that are assigned to a given user and display the results in our user interface:
1.2.7. Modifying tasks
The tasks created by the task builder or returned by a search are immutable copies of the work object state, so the contents cannot be modified directly. This has advantages (for example work object references may be shared between threads without worrying about multi-threading issues) but how can the state of a work object be changed? Again, the answer is to use the relevant work object service:
getTaskService().setName(taskId, "Travel request for Berlin: 13/3 - 15/3", "changed travel dates");
Task updatedTask = getTaskService().findTaskById(taskId);
The objects returned by edoras gear are also detached objects, i.e. they will not be updated when the persistent data from which they were created is changed. In this example our existing copy will not be changed by the call to the task service so after the object has been changed we have to fetch a new copy of the task to see those changes in memory.
1.2.8. Reassigning tasks
We can easily reassign tasks by simply using the task service to set a new assigned user ID:
getTaskService().setAssignedUser(taskId, this.annaId, "reassigned to Anna");
Work objects provide methods to get not only the current assignee, but also the previous and initial assignee, which can be useful information when a task has to be reassigned. |
1.2.9. Candidate groups
We have now created a direct equivalent of the original travel booking system using edoras gear and we can look at making our first improvement to the task management process. One problem with the original process was that e-mails were sent to a particular administrator, and if that administrator was away or otherwise busy the request may not get processed. To improve our task management it would be useful to leave new tasks unassigned until someone has time to work on them. When a suitable person has time available they can take a task from the unassigned pool and start to work on it.
But if tasks are unassigned how can we know which tasks are intended for a particular group of users? As our system grows we may have all sorts of different tasks waiting to be processed, only some of which are interesting for a particular user.
We can use the concept of candidate groups to solve this problem. For example any user that is a member of the "admin" candidate group can deal with a travel request. So when we create a new task we don’t have to assign it to a specific user as we did before, we can instead leave it unassigned and set the candidate group instead. In this way the task will be added to a pool of tasks for that group:
return Task.builder()
.name("Travel request for Berlin: 12/3 - 14/3")
.description("please book a flight to Berlin for me, ...")
.addCandidateGroupId(this.adminId)
.ownerId(this.daveId)
.build();
When a particular user has free resources, we can take a task from the pool of unassigned tasks and assign it to that user to make it clear that they are now responsible for dealing with it:
// find an unassigned task for the "admin" group and assign it to Anna
Predicate groupPredicate = Task.CANDIDATE_GROUP_IDS.containsAnyOf(this.adminId);
Predicate unassignedPredicate = Task.ASSIGNEE_ID.isNull();
List<Task> tasks = getTaskService().findTasks(groupPredicate.and(unassignedPredicate));
if (!tasks.isEmpty()) {
getTaskService().setAssignedUser(tasks.get(0).getId(), this.annaId, "reassigned to Anna");
}
Note that a task may have a number of candidate groups, and users may belong to a number of different groups at the same time (a manager may also be able to deal with administration tasks). In a real application the groups that a particular user belongs to would also be retrieved from the user management system.
As well as candidate groups, edoras gear supports a candidate user list which contains specific user IDs that the task may be assigned to. This is essentially an ad-hoc candidate group. For more details please refer to the edoras gear user guide. |
We can now simplify our travel request GUI, as tasks can now be assigned automatically to the administration group without having to select a specific user:
By searching for all tasks with the "admin" candidate group we can also easily provide an overview of all administration tasks:
1.3. Improving the data model
So far we have simply copied the contents of the original travel request into a task object. This is already has several advantages over the e-mail based process, but the requests are still very unstructured. The information about travel dates and other requirements is buried in the plain text name and/or description, which is flexible in some ways but also has some limitations:
-
users have no standard structure to work with, so they may forget to add certain information when making the request, or may have difficulty finding the information they need to process the request.
-
we can’t easily provide consistency checks or improved functionality from our application based on the request details.
-
searching based on the task contents is especially difficult.
Of course if we are creating the tasks as part of an application we can provide a form with predefined fields and encode this information in a uniform way in the description, but we still have to deal with the encoding/decoding issues and this approach is likely to be very inefficient if we need frequent access to the encoded information. Simple keyword searches may be possible, e.g. a search for travel requests to Berlin may look something like the following:
Predicate destinationPredicate = Task.NAME.like("*Berlin*");
List<Task> tasks = getTaskService().findTasks(destinationPredicate);
but this sort of search is not reliable (e.g. it would return a match for Travel request to Berlingen
).
For many searches, however, simple substring searches are no longer possible at all. For example it would very hard to find all
travel requests with a given range of departure dates without loading all objects into memory and picking apart the contents
to extract the departure date. This is hard to implement, hard to maintain, and very inefficient.
In this chapter we will introduce the edoras gear variable mechanism and show how this can be used to manage structured data within a work object.
1.3.1. Variables
To store structured data within a work object, we need to define which data will be stored and what data type
that data will have. edoras gear allows us to attach Variable
instances to any work object, where each variable is identified by a variable name and contains a value
object. Although there are some lower-level variable access methods, we will use the type-safe variable interfaces to ensure that the values that we read and write are
type-checked by the Java compiler. Type-safe variables are described by defining
VariableName
instances, typically as constants. For example we can create some type-safe variable definitions for the travel destination, departure and return times:
public static final VariableName<String, String> DESTINATION = VariableName.create("destination", String.class);
public static final VariableName<Boolean, Boolean> IS_INTERNATIONAL = VariableName.create("isInternational", Boolean.class);
public static final VariableName<Date, Date> DEPART_DATE = VariableName.create("depart", Date.class);
public static final VariableName<Date, Date> RETURN_DATE = VariableName.create("return", Date.class);
The two Java Generics type parameters used here correspond to the original and serialized data types. A number of common variable data types are supported by default, and it’s possible to create variables of arbitrary types by specifying a suitable serialization / de-serialization mechanism in the variable definition (for example a standard JSON converter). This capability is beyond the scope of this introduction, so please refer to the edoras gear user guide for more details. |
Our new variable definitions can now be used to add structured data to the travel request using the task builder:
Task berlinRequest = Task.builder()
.name("Travel request")
.candidateGroupIds(newHashSet(this.adminId))
.putVariable(DESTINATION, "Berlin")
.putVariable(IS_INTERNATIONAL, true)
.putVariable(DEPART_DATE, getDepartureDate())
.putVariable(RETURN_DATE, getReturnDate())
.build();
TaskId berlinRequestId = getTaskService().addTask(berlinRequest, null);
and also to read the values back out again:
Task request = getTaskService().findTaskById(berlinRequestId);
String requestDestination = request.getVariableValue(DESTINATION);
boolean requestIsInternational = request.getVariableValue(IS_INTERNATIONAL);
Date requestDepartDate = request.getVariableValue(DEPART_DATE);
Date requestReturnDate = request.getVariableValue(RETURN_DATE);
All of these variable accesses are type-checked by the compiler, so it is always clear that we have data of the correct type.
It is also possible to add or update variables using the task service after the task has been created:
getTaskService().putVariable(berlinRequestId, DESTINATION, destination, "set request details");
getTaskService().putVariable(berlinRequestId, IS_INTERNATIONAL, true, "set request details");
getTaskService().putVariable(berlinRequestId, DEPART_DATE, departDate, "set request details");
getTaskService().putVariable(berlinRequestId, RETURN_DATE, returnDate, "set request details");
Updating each variable separately may be inefficient, so several variables may be updated at the same time with a single
call, using a VariableMap
to pass in the variables to be updated:
VariableMap variableMap = VariableMap.builder()
.put(DESTINATION, destination)
.put(IS_INTERNATIONAL, true)
.put(DEPART_DATE, departDate)
.put(RETURN_DATE, returnDate)
.build();
getTaskService().putVariables(berlinRequestId, variableMap, "set request details");
Using a better data model for our travel requests allows us to add more structure to the request form, making it clear which information is required:
We are also in a position to execute more interesting searches based on the rich data model, which will be covered in the next section.
1.3.2. Searching using variables
The edoras gear query mechanism also supports searches based on variables. Task.VARIABLE
provides a number of ways to build variable-based predicates,
allowing us to include variables and their values into our searches.
As an example:
// find all travel requests with a destination that starts with "Ber"
Predicate namePredicate = Task.VARIABLE.name().eq(DESTINATION.getName());
Predicate valuePredicate = Task.VARIABLE.stringValue().like("Ber*");
List<Task> tasks = getTaskService().findTasks(namePredicate.and(valuePredicate));
Many different variable predicates are provided by edoras gear and these can be combined in different ways to constrain searches using one or more variables within a work object. The full details are beyond the scope of this document, however. Be aware that simply combining variable predicates with Please refer to the edoras gear user guide for more information. |
1.3.3. More advanced searching
Searching using predicate-based searches is powerful, but we can get better control over the search results by
wrapping the predicate in a Query
before executing the search. Using a Query
-based search allows us to tailor the search results to meet our exact requirements. We can:
-
define the sort order
-
define the start offset and number of results that are returned
Query
instances are also assembled using a builder, for example we can search for the 10 oldest
travel requests:
// search for the 10 oldest travel requests
TaskQuery taskQuery = TaskQuery.builder()
.predicate(Task.CANDIDATE_GROUP_IDS.containsAnyOf(this.adminId))
.offset(0)
.limit(10)
.sorting(Task.CREATION_TIME.orderAsc())
.build();
List<Task> tasks = getTaskService().findTasks(taskQuery);
The simple predicate-based search functionality that we used earlier on is just a convenient shortcut which can be used whenever this additional control over the search results is not required.
1.3.4. Query result optimization
Sometimes we are not really interested in all of the information that is returned by a search. We may only be interested in the basic information provided by the work object, not in the variable content or the candidate users/groups. Or we may only need the value of a specific variable. In these cases we can provide hints to a search query which allows edoras gear to skip the parts of the data that we are not interested in.
As an example we can optimize our previous query to omit all of the variables in the result set:
// search for the 10 oldest travel requests, omitting all variables from the search results
TaskQuery taskQuery = TaskQuery.builder()
.predicate(Task.CANDIDATE_GROUP_IDS.containsAnyOf(this.adminId))
.offset(0)
.limit(10)
.sorting(Task.CREATION_TIME.orderAsc())
.hints(TaskQuery.Hint.OMIT_VARIABLES)
.build();
List<Task> tasks = getTaskService().findTasks(taskQuery);
Hints are provided to omit candidate user and group information, omit all variables or just return specific variables. For details please refer to the edoras gear user guide.
1.3.5. Managing state
So far we have learnt how to create structured ad-hoc tasks, perform complex searches and make changes, but how can we track the status of a task? When the requested travel has been booked there is no need to keep tracking the task, so it would be nice to be able to mark the task as 'done' somehow. We could just create a variable to indicate the state, but this is such a common problem that edoras gear provides a standard solution.
Every work object has a state and a sub-state field that can be used
to manage state information, both of which contain instances of the State
class. The sub-state is left for use by the application, and can be set to any State
values that the application wants to define. For example we could create the following sub-states for our travel request task:
public static final State OPEN = State.getInstance("open");
public static final State BOOKED = State.getInstance("booked");
public static final State REJECTED = State.getInstance("rejected");
The work object sub-state can be changed using the same service that we use for other modifications:
getTaskService().setSubState(openTaskId, OPEN, "travel is open");
getTaskService().setSubState(bookedTaskId, BOOKED, "travel has been booked");
getTaskService().setSubState(rejectedTaskId, REJECTED, "travel request was rejected");
The state field has a much more limited set of values and is managed by edoras gear instead of being set directly by the application code. When created,
a work object will be in the ACTIVE
state and can be moved to the COMPLETED
state using the completeTask()
method from the service interface:
Task activeTask = getTaskService().findTask(Task.ID.eq(taskId).and(Task.STATE.isActive()));
if (activeTask != null) {
getTaskService().setSubState(taskId, BOOKED, "change sub-state to 'booked'");
getTaskService().completeTask(taskId, "task completed");
}
Task completedTask = getTaskService().findTaskById(taskId);
The state behaviour may seem very limited, but it is possible to combine the sub-state and state in complex workflows, and completing a work object using the service interface may have other important side-effects that will be explained later.
For now we will assume that the completeTask()
method will be used to complete the tasks
once they have been processed, perhaps adding additional state information using the sub-state value.
This allows us to refine our queries, for example to show only the active tasks assigned to the "admin" group:
Predicate activePredicate = Task.STATE.eq(WorkObjectState.ACTIVE);
Predicate groupPredicate = Task.CANDIDATE_GROUP_IDS.containsAnyOf(this.adminId);
List<Task> tasks = getTaskService().findTasks(activePredicate.and(groupPredicate));
We can also provide the user with a more powerful search interface, allowing them to explore the tasks stored within the system in a much more flexible way than would ever have been possible with the original e-mail based process:
Putting together all that we have learnt so far, we now have the basis for a fairly rich task management application that can store, manage and retrieve single tasks to meet a wide variety of use cases.
1.4. Adding more tasks: case management
After using the first version of our travel request management application for a few weeks, Mark is very happy with the results but would like to make some more improvements to the process. He’s noticed that there are a few international trips being booked that he’s not comfortable with, so he would like to be able to approve them before they are booked. How can we implement this requirement?
1.4.1. Grouping tasks together using cases
The first idea is simply that the administrators should be able to create another ad-hoc task for Dave to approve the travel request whenever an international trip is requested. We now have two types of tasks in the system:
The question is, how do we solve the following problems:
-
when Mark receives the approval request, how does he find the travel request details?
-
how can an administrator check whether an approval request has already been created?
One solution would be to add the ID of the original travel request to the new approval task (and vice-versa) but it seems likely that more tasks will be needed later on, so this approach won’t scale. We would also have to write all the code to manage these relationships which is time-consuming and error-prone. What we really need is an easy way to group related tasks.
edoras gear provides such a grouping mechanism via the Case
work object type.
A case provides a context for other work objects and serves as a container, with the related work objects being added as
children in a hierarchy. In our case we can create a case for each travel request and use it to group all of the related tasks.
First we need to update our application configuration to include the case management functionality:
<!-- Case Management definition -->
<gear:case-management id="caseManagement"/>
…
and include a reference to the case service into our application:
@Inject
private CaseService caseService;
protected CaseService getCaseService() {
return this.caseService;
}
Now we can create a travel request case, with the booking and approval tasks as child work objects. The relationship between the case and tasks is established by passing the case ID as a parameter when we store the task using the task service:
Case travelCase = Case.builder().name("Travel request for " + destination).build();
CaseId travelCaseId = getCaseService().addCase(travelCase, "create new travel request case");
Task bookingTask = Task.builder()
.name("Book travel")
.addCandidateGroupId(this.adminId)
.putVariable(DESTINATION, destination)
.putVariable(IS_INTERNATIONAL, true)
.putVariable(DEPART_DATE, getDepartureDate())
.putVariable(RETURN_DATE, getReturnDate())
.subState(OPEN)
.build();
TaskId bookingTaskId = getTaskService().addTask(bookingTask, travelCaseId, "add booking task");
Task approvalTask = Task.builder()
.name("Approve travel")
.addCandidateGroupId(this.managementId)
.build();
TaskId approvalTaskId = getTaskService().addTask(approvalTask, travelCaseId, "add approval task");
It is not just cases that can be used as containers; any work object may be used to supply the 'context' for other work objects. So a task may also be a container for related sub-tasks, etc. The context may also be changed by moving a work object to another parent as required. In this way detailed and adaptable work object hierarchies may be constructed. |
In this way, we represent the relationship between the individual tasks by grouping them under a common parent:
1.4.2. Searching for related work objects
We have now created a little hierarchy of a case and its related tasks, but how can we reload this hierarchy again at a later date? First we can locate the case using a normal search. This works in exactly the same way that it did for tasks, we just use the case service instead of the task service:
List<Case> travelCases = getCaseService().findCases(Case.NAME.like("Travel request*"));
To load the corresponding child tasks we can simply use a task search with a suitable hierarchy predicate:
List<Task> tasks = getTaskService().findTasks(Task.HIERARCHY.childOf(caseId));
As we will see later, hierarchies may be more than one level deep, and so there are several different hierarchy predicates that can be used to retrieve different parts of the hierarchy (only the direct children, both direct and indirect children etc.). More details can be found in the edoras gear user guide. |
1.4.3. Variables in a hierarchy
We have now grouped our tasks and we know how to find all of the tasks that belong together. What we would now like to do is update our search GUI to show the list of open travel requests (at the case level):
By clicking on the Details
button, the user can open a detailed view showing the all of the corresponding tasks and their status:
If we try to drill down one level more, however, we notice a small problem. The details of the travel request are stored in the original booking task, so for this task we can easily create a details GUI:
But the travel details will also be needed by the manager when approving the travel request, so we also need to show the details in the corresponding approval task GUI. Of course we could copy the information across to the new task, but this either requires time-consuming and error-prone manual work when the approval task is created, or some additional use-case specific code in the application.
In fact, the travel details are part of a global context that is interesting to all related tasks, so it would be good if this information could be kept in one place and shared with any tasks that we may want to create.
When loading a work object such as our booking task, edoras gear includes not only the variables from the work object that we requested, but also the variables from the parent work object if there is one (and its parent, and so on). This allows us to solve our problem very simply: we just place the travel information in the parent case instead of the booking task. At this point we can also take the opportunity to place the creation code in utility methods to make it easier to reuse:
public CaseId createTravelRequestCase(String destination, boolean isInternational, Date departDate, Date returnDate) {
Case travelCase = Case.builder()
.name("Travel request for " + destination)
.putVariable(DESTINATION, destination)
.putVariable(IS_INTERNATIONAL, isInternational)
.putVariable(DEPART_DATE, departDate)
.putVariable(RETURN_DATE, returnDate)
.build();
return getCaseService().addCase(travelCase, "create new travel request case");
}
public TaskId createApprovalTask(CaseId travelCaseId) {
Task newApprovalTask = Task.builder()
.name("Approve travel")
.candidateGroupIds(newHashSet(this.managementId))
.state(WorkObjectState.ACTIVE)
.build();
return getTaskService().addTask(newApprovalTask, travelCaseId, "add approval task");
}
public TaskId createBookingTask(CaseId travelCaseId) {
Task newBookingTask = Task.builder()
.name("Book travel")
.candidateGroupIds(newHashSet(this.adminId))
.state(WorkObjectState.ACTIVE)
.build();
return getTaskService().addTask(newBookingTask, travelCaseId, "add booking task");
}
We can now create the sub-tasks and directly see the travel detail variables:
CaseId travelCaseId = createTravelRequestCase(destination, true, getDepartureDate(), getReturnDate());
TaskId approvalTaskId = createApprovalTask(travelCaseId);
TaskQuery query = TaskQuery.builder().predicate(Task.ID.eq(approvalTaskId)).hints(TaskQuery.Hint.INCLUDE_PARENT_VARIABLES).build();
Task approvalTask = getTaskService().findTask(query);
String approvalDestination = approvalTask.getVariableValue(DESTINATION);
TaskId bookingTaskId = createBookingTask(travelCaseId);
query = TaskQuery.builder().predicate(Task.ID.eq(bookingTaskId)).hints(TaskQuery.Hint.INCLUDE_PARENT_VARIABLES).build();
Task bookingTask = getTaskService().findTask(query);
String bookingDestination = bookingTask.getVariableValue(DESTINATION);
When working with variables in a hierarchy, there are several useful features that you should be aware of:
More details can be found in the edoras gear user guide. |
1.5. Process automation: process management with BPMN 2.0
Using a case as a container for the different tasks in our travel request allows us to represent the fact that some tasks are related to each other. This is a big step, but there is another important dimension that we have to deal with: tasks generally aren’t completely independent of each other, they often have more interesting relationships. For example Mark currently only wants to approve international travel requests, so the approval task only needs to be created some of the time. If an approval task has been created, then it probably only makes sense to create the booking task once the travel has been approved.
One solution is for the various participants to create the required tasks at the appropriate time. The administrators could receive a simple travel request as a case object and manually create an ad-hoc approval task when needed. When an approval task is not required they can create a booking task directly. The manager could also create a booking task once he has completed an approval task. The problem with this approach is that the "responsibility" for the process execution is widely scattered throughout the team. It is hard to ensure that everyone has everyone has the same understanding of the process and remembers to execute the correct steps at the right time.
A better alternative is to implement the process logic in the application code:
public CaseId submitHardcodedTravelRequest(String destination, boolean isInternational, Date departDate, Date returnDate) {
CaseId travelCaseId = createTravelRequestCase(destination, isInternational, departDate, returnDate);
// ==== begin process logic ====
if (isInternational) {
createApprovalTask(travelCaseId);
} else {
createBookingTask(travelCaseId);
}
// ==== end process logic ====
return travelCaseId;
}
public void completeHardcodedApprovalTask(TaskId taskId, boolean approved, String comments) {
Task task = getTaskService().findTaskById(taskId);
CaseId caseId = task.getParentCaseId();
VariableMap variableMap = VariableMap.builder()
.put(IS_APPROVED, approved)
.put(APPROVAL_COMMENTS, comments)
.build();
getCaseService().putVariables(caseId, variableMap, "completed approval task");
getTaskService().completeTask(taskId, "completed approval task");
// ==== begin process logic ====
if (approved) {
createBookingTask(caseId);
}
// ==== end process logic ====
}
public void completeHardcodedBookingTask(TaskId taskId, String bookingDetails) {
Task task = getTaskService().findTaskById(taskId);
CaseId caseId = task.getParentCaseId();
getCaseService().putVariable(caseId, BOOKING_DETAILS, bookingDetails, "completed booking task");
getTaskService().completeTask(taskId, "completed booking task");
}
This is a big improvement, as the correct process tasks are now created automatically:
-
there are less manual steps, so users can work more efficiently.
-
users no longer have to know the whole process and remember which tasks have to be created at each step. We can therefore change the process without having to retrain all of the users.
-
there is much less chance that mistakes will be made.
However there are still some problems. Although the code now implements the process, the process logic is probably scattered over several methods or files. Understanding how the current process works or making changes will prove difficult as we have to find all of the places in the code that might be involved. As the code has to support more and more complex processes this may become a significant problem. It is also hard to communicate and discuss the details of the process without a concise and up-to-date description.
Wouldn’t it be great if we could describe the process in one place and use that description to automate the workflow! Wouldn’t it be even better if we could do that without having to throw away all of the process improvements that we have already made! Luckily we can…
1.5.1. Processes
edoras gear provides seamless integration with process automation engines to provide exactly the functionality that we need. Process automation engines take a description of a business process (usually in a specialized process description language such as BPMN 2.0) and allow the rules defined in that process description to be executed in the context of an application.
Explaining how to create business process descriptions using BPMN 2.0 notation is beyond the scope of this documentation, but the process can be simplified by using a graphical business process designer such as edoras vis. This approach also has the advantage of producing a graphical representation of the process which can be used for process documentation.
For now, we will assume that we have a BPMN description of our current process available. The graphical representation of such a process may look something like this:
The small symbols with an 'X' in them are conditional gateways, at which point a decision is made about which ongoing
path should be followed in the process. The gateway on the left decides whether to create an approval task or go
straight to the booking task. Expressions like #{isInternational}
are condition expressions that will be
evaluated at runtime when the execution flow reaches the corresponding gateway, and will resolve to the value of the
corresponding variable (in this case the boolean variable that we defined in our travel request).
As a first step we need to add the process engine support to our application configuration:
<!-- JUL-SLF4J logging rerouting -->
<bean id="julReroute" class="com.edorasware.commons.core.util.logging.JulToSlf4jBridgeHandlerInstaller" init-method="init"/>
<!-- Task Management definition -->
<gear:task-management id="taskManagement">
<gear:activiti-task-provider process-engine="processEngine"/>
</gear:task-management>
<!-- Process Management definition -->
<gear:process-management id="processManagement">
<gear:activiti-process-provider process-engine="processEngine"/>
</gear:process-management>
<!-- Process Engine definition -->
<gear:activiti-process-engine id="processEngine">
<gear:process-engine-configuration>
<gear:property name="expressionManager" ref="expressionManager"/>
</gear:process-engine-configuration>
<gear:process-definitions>
<gear:resource location="classpath:com/edorasware/gear/documentation/introduction/TravelRequestProcess.bpmn20.xml"/>
</gear:process-definitions>
</gear:activiti-process-engine>
<bean id="expressionManager" class="com.edorasware.gear.core.engine.support.activiti.ActivitiGearExpressionManager"/>
The expression manager used in these examples can resolve Spring beans and edoras gear hierarchy variables. It can also invoke Spring bean methods. The process engine can also be configured with custom expression resolvers to resolve other value types. Please refer to the edoras gear user guide for more details. |
In this example the process definition TravelRequestProcess.bpmn20.xml
will automatically be loaded when we start the process engine.
Note that we also updated the task management configuration to allow us to access any tasks that are created by the process engine via the standard task service API.
Now that we have configured the process engine and our process definition, the code to start the process is straightforward. Following the pattern that should be familiar by now, we can simply use a reference to the process service to start a process instance. In this case, however, we will need a reference to the process definition for the process to be started, so we also need a reference to the process definition service:
@Inject
protected ProcessDefinitionService processDefinitionService;
@Inject
protected ProcessService processService;
Using these two services,we simply create a new travel request case (exactly as before) and then locate the process definition (usually by searching for the process key). Once we have the process definition then we can start a new process in the context of the case (similar to the ad-hoc tasks that were used previously):
public CaseId submitAutomatedTravelRequest(String destination, boolean isInternational, Date departDate, Date returnDate) {
CaseId travelCaseId = createTravelRequestCase(destination, isInternational, departDate, returnDate);
ProcessDefinition processDefinition =
this.processDefinitionService.findProcessDefinition(ProcessDefinition.KEY.eq("travel-process"));
this.processService.startProcess(processDefinition.getId(), travelCaseId);
return travelCaseId;
}
The |
At this point the process engine will start to execute the process, checking in the first gateway whether the travel booking is for an international trip and creating a task of the appropriate type. The tasks can be searched for and processed exactly as before, but now there is no need to hard-code any process logic into the completion method as all of the process logic is managed by the process engine:
public void completeAutomatedApprovalTask(TaskId taskId, boolean approved, String comments) {
Task task = getTaskService().findTaskById(taskId);
CaseId caseId = task.getParentCaseId();
VariableMap variableMap = VariableMap.builder()
.put(IS_APPROVED, approved)
.put(APPROVAL_COMMENTS, comments)
.build();
getCaseService().putVariables(caseId, variableMap, "completed approval task");
getTaskService().completeTask(taskId, "completed approval task");
}
public void completeAutomatedBookingTask(TaskId taskId, String bookingDetails) {
Task task = getTaskService().findTaskById(taskId);
CaseId caseId = task.getParentCaseId();
getCaseService().putVariable(caseId, BOOKING_DETAILS, bookingDetails, "completed booking task");
getTaskService().completeTask(taskId, "completed booking task");
}
We now come back to the important side effects of changing the work object state to COMPLETED
.
Whenever a user task of the process is marked as 'completed', the process engine will apply the process rules and create
new tasks or perform other actions as required based on the process flow. When an end event is
reached (the heavy circle on the right hand side of our process diagram) then the process state is changed to COMPLETE
and process execution stops. No more tasks will be created.
1.5.2. Process hierarchy
When we created the tasks manually within the case then the case was the direct parent of the task:
When we create tasks using a process then this is no longer true. The direct child of the case is the process itself. A process may also create sub-processes, which in turn may create tasks. Thus the work object hierarchy for any given task may look something like the following:
Retrieving the process information works in exactly the same way as retrieving tasks or cases. We just use the process service together with an appropriate predicate or query definition:
Process process = this.processService.findProcess(Process.HIERARCHY.childOf(caseId));
Case variables will still be visible in the task, even when the direct parent is a process instance but we now have more places where variable values may be stored. As for Java programming, it’s a good idea to store each variable at the lowest level where the sharing requirements for that variable can be met. Some variables may be shared across all tasks and processes involved in a particular process (such as the destination in our travel request example). Other variables may only be needed for the duration of a single process or sub-process and are no longer interesting once the process has been completed, and these can therefore be stored in the appropriate process instance. It is also possible to store variables in the tasks themselves. Although tasks typically have a short lifetime, this can still be useful, for example to save incomplete information temporarily while a task is in-progress. The information can then be copied to the correct place when all of the information is available and the task can be completed. A task may still be retrieved even after it has been completed, so the information stored can also be used for history and/or audit purposes. |
1.5.3. Changing the process definition
In the process management configuration that we used earlier, the process BPMN file to be used was specified explicitly. This is convenient for stable processes or perhaps for applications that are frequently restarted, but in some cases we may want to load a process dynamically, or load a different version of a process within a running application. This can also be done using the process definition service:
Resource resource = new ClassPathResource(
"com/edorasware/gear/documentation/introduction/TravelRequestProcess.bpmn20.xml");
this.processDefinitionService.deployProcessDefinitions(ImmutableList.of(resource), ProcessProviderId.UNDEFINED);
Once deployed, a process definition cannot be modified or deleted (there may be process instances still running in
the system that use the old definition), instead a new deployment will create a new version
of the process definition. The new process definition will have the same key, but the version number will be incremented.
When we look up a process definition we should therefore use the LATEST
predicate to locate the latest deployed version (i.e. the version with the highest version number):
Predicate latestVersionPredicate = ProcessDefinition.LATEST_VERSION.withKey(key);
ProcessDefinition definition = this.processDefinitionService.findProcessDefinition(latestVersionPredicate);
int version = definition.getVersion();
Because it’s not possible to modify or delete process definitions once they have been deployed, the application needs to provide an alternative mechanism to simulate process definition deletion. One possible approach to this problem is described in the edoras gear FAQ. |
1.6. Extending the functionality of edoras gear
1.6.1. Service tasks
An application that creates, manages and schedules tasks, cases etc. using a BPMN process definition is useful, but in a reality a process often has to interact with other applications or send messages/reminders to users. To support this, we can include a service task in our process description. A service task allows us to include custom processing code directly into the process execution.
In the context of our travel request process, Mark would like an e-mail to be sent back to the originator when the travel request has been processed, avoiding the need to repeatedly check the state of the request. To do this we will need a service that can send e-mails. For now we just define a dummy service to print a message to the console and save the addresses:
@ExpressionBean
public class MailService {
private final List<String> mailedAddresses = newLinkedList();
public void sendMail(String mailAddress) {
System.out.println("A mail has been sent to " + mailAddress);
this.mailedAddresses.add(mailAddress);
}
public void sendMail(String mailAddress, String text) {
System.out.println("A mail has been sent to " + mailAddress + " with text " + text);
this.mailedAddresses.add(mailAddress);
}
public List<String> getMailedAddresses() {
return this.mailedAddresses;
}
public void reset() {
this.mailedAddresses.clear();
}
}
This service now needs to be made available as a Spring bean with a known ID:
<bean id="mailService" class="com.edorasware.gear.documentation.introduction.MailService"/>
We can now adjust our process to include a service task as the last step:
The "Send Notification Mail" service task is configured with the expression
#{mailService.sendMail(mailAddress, approvalComments)}
. When the task is executed the expression will be used by the process engine to look up a bean with the id
mailService
and invoke the method sendMail
with the value of the mailAddress
and approvalComments
variables as parameters.
This is another example of the expression resolution algorithm, which is also used by the conditional expressions that select valid process paths. Please refer to the edoras gear user guide for more information. |
All that is now missing is to set the mailAddress
variable when we create the travel request so
that the address is available for the mail service when the notification task is executed:
CaseId caseId = createTravelRequestCase("Travel request for Basel", false, departDate, returnDate);
getCaseService().putVariable(caseId, "mailAddress", "test@test.com", null);
Now we can test our process with the dummy service, and when everything is working as expected we can extend the mail service to send real e-mails.
1.6.2. Work object listeners
After a while using the travel request system, Mark receives a number of complaints from the administrators that lots of travel requests simply aren’t filled out properly. Is there a way to validate the incoming travel requests and prevent them entering the system? Of course we could add more checking in the GUI to make sure that the details are correct, but if there was a bug in the GUI code then incorrect travel requests could still enter the system. In this case that may not be too bad, but there may be circumstances where this would cause more significant problems. What we would really like to do is to implement those business rules for new travel request cases in such a way that:
-
it’s simply not possible to enter an incorrect travel request into the system, regardless of how buggy the GUI code may be
-
we can see that the validation has been performed.
A good way to implement this would be to use a work object listener. A work object listener is registered with the relevant work object service and is called whenever an interesting event happens (i.e. a work object of the relevant type is modified in some way). The listener is called:
-
before the action is performed (so that it can make adjustments as required)
-
after the action has been completed (so that it can use the final results for further processing)
In our case we can define a case listener to check the travel request details before the case is entered into the system:
public class TravelCaseListener implements CaseActionListener {
@Override
public void actionWillBePerformed(CaseActionEvent event) {
if (event.isCreationEvent()) {
Case newCase = event.getNewCase();
String destination = newCase.getVariableValue(TravelTaskConstants.DESTINATION);
Date departDate = newCase.getVariableValue(TravelTaskConstants.DEPART_DATE);
Date returnDate = newCase.getVariableValue(TravelTaskConstants.RETURN_DATE);
if ((destination == null) || (departDate == null) || (returnDate == null)) {
throw new RuntimeException("Travel details are incomplete");
}
if (departDate.compareTo(returnDate) > 0) {
throw new RuntimeException("Departure date must be before return date");
}
CaseModification.Builder modificationBuilder = event.getCaseModificationBuilder();
modificationBuilder.putVariable(TravelTaskConstants.IS_VALIDATED, true);
}
}
@Override
public void actionPerformed(CaseActionEvent event) {
}
}
We also need to register this listener with the case service in our application configuration. Note that any number of listeners can be registered in this way, and they will be called in the order that they are listed:
<gear:case-management id="caseManagement">
<gear:case-service-configuration id="caseService">
<gear:case-listeners>
<gear:action-listener class="com.edorasware.gear.documentation.introduction.TravelCaseListener"/>
</gear:case-listeners>
</gear:case-service-configuration>
</gear:case-management>
This listener checks the values before the case is created (in the actionWillBePerformed()
method). Before doing the validation it also checks that the event is of the correct type.
Listeners will receive several different event types (e.g. object creation, variable changes, state changes) and so it is important that listeners handle the correct events by including such an event type check. |
The listener then validates the variable values from the (prospective) new case instance, and throws an exception if the values are invalid. This exception will be passed back to the caller that was trying to create the case object, as the listeners are called synchronously. The original operation will also be aborted in this case.
If all of the validations are passed then the modification builder is used to set a flag variable in the new case to indicate that the validation has been performed.
Modification builders can be used by a listener to add new values to the work object (as we are doing here) or to override other modifications that are being requested. |
1.6.3. Timers
Another problem that comes up in the travel process from time to time is that Mark sometimes forgets to approve the travel requests. When this happens then the process is blocked, so Mark would like the system to send him a reminder if an approval task is left open for a while.
To implement this requirement we can simply add a timer to the approval task:
When the approval task is created then the timer will be initialized. The timer can be configured to fire after a given interval, possibly repeatedly, and when the timer fires then the attached part of the process will be executed. In this case, we can simply reuse our mail service to send a reminder e-mail.
If the approval task is completed before the timer has expired then the timer will be cancelled, so no reminder will be sent.
There are actually two types of timers. Both start a new execution path when the timer is triggered, but they differ in the way that the 'base' task is treated:
Note that the timer processing will execute in a new thread, so the caller’s context will no longer be available. In some circumstances this may mean that some additional code is required to re-establish a suitable execution context for further processing. |
1.7. Conclusions
1.7.1. Add structure where and when it is needed
We have seen that edoras gear makes it easy to start with simple ad-hoc task management and incrementally improve the way that these tasks are managed, adding more structure and detail as required until the real workflow requirements are clearly visible. At this point we can use case management and business process descriptions to document and automate the workflow in a clean and maintainable way.
1.7.2. Don’t try to structure an unstructured world
One of the great advantages of edoras gear is that even when we have formalized parts of our workflow, we can still combine the automated system behaviour and ad-hoc functionality. This allows the application to support 'exceptional' events that were not foreseen when the main workflow was defined. If exceptions happen often enough then we can update the application to take the exceptions into account, or if they are really exceptions then we can just continue to handle them as ad-hoc tasks. This avoids cluttering up a clean workflow description with lots of exceptions for infrequent events.
2. Setup
2.1. Overview
2.1.1. Goals
This guide will explain the basic setup of edoras gear and provide all of the code and configuration needed to set up a simple unit test. Once you have this example running you will be in a good position to start exploring the features of edoras gear by following the edoras gear introduction.
2.1.2. Requirements
To use edoras gear you will need to include the component and its dependencies into your project, for example using a dependency management tool such as Gradle or Maven. You will also need a valid edoras gear license.
Gradle
edoras gear can be integrated into an application project via Gradle in two steps.
First, define a Maven repository:
repositories {
maven {
url 'https://repo.edorasware.com/edoras-repo-public'
credentials {
username <put_username_here> // omit angle brackets
password <put_password_here> // omit angle brackets
}
}
Then add a dependency to the core module of edoras gear:
dependencies { compile 'com.edorasware.gear:edoras-gear-core:1.5.0.S124' }
Maven
edoras gear can be integrated into an application project via Maven in two steps.
First, define a Maven repository:
<repositories>
<repository>
        <id>edorasware.com</id>
        <url>https://repo.edorasware.com/edoras-repo-public</url>
</repository>
</repositories>
Then add a dependency to the core module of edoras gear:
<dependency> <groupId>com.edorasware.gear</groupId> <artifactId>edoras-gear-core</artifactId> <version>1.5.0.S124</version> <scope>compile</scope> </dependency>
License
edoras gear requires a valid license. You can a request a trial license by contacting us at support@edorasware.com.
2.1.3. edoras gear Bootstrap
A minimal project setup of edoras gear is provided via edoras gear bootstrap project. The edoras gear bootstrap project demonstrates how to configure edoras gear and run a simple unit test that executes a BPMN 2.0 process. After downloading and extracting the edoras gear bootstrap project, please follow the steps described in the Readme.txt file.
The following chapters explain the setup of edoras gear in more detail.
2.2. Basic edoras gear configuration
2.2.1. XML Configuration
edoras gear consists of a number of services that can be configured in XML using an application container such as Spring. The services are configured using the edoras gear namespace, which is backed by a fully documented schema. Traditional Spring beans can also be referenced in the standard way.
To get started we need to create the XML container, including the relevant namespaces:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:gear="http://www.edorasware.com/schema/gear"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.edorasware.com/schema/gear
http://www.edorasware.com/schema/gear/edoras-gear-3.0.2.S66.xsd">
</beans>
2.2.2. License management
Before edoras gear will run we need to configure the license management. By default, edoras gear will look for the file edorasware.license
in the classpath,
but it is also possible to configure a specific resource path. For this example we will simply use the default:
<!-- License Management definition-->
<gear:license-management id="licenseManagement"/>
By following the standard naming convention for
edoras gear
components they can
automatically be located and there is no need to explicitly wire them together in the XML configuration. The naming
convention is to use an ID with the same name as the component, but converted to camel case. For example the
|
2.2.3. Persistence management
The persistence management component provides the services used to persist and retrieve data. It uses a data source and transaction manager that we will define here as standard
Spring beans with the IDs dataSource
and transactionManager
. For our test environment we will use the in-memory H2 database:
<!-- Persistence bean definitions -->
<bean id="dataSource"
class="org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseFactoryBean">
<property name="databaseType" value="H2"/>
<property name="databaseName" value="SimpleProcessTest"/>
</bean>
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- configure edoras database schema service/manager -->
<bean id="databaseSchemaService" class="com.edorasware.commons.core.persistence.schema.internal.DefaultDatabaseSchemaService">
<constructor-arg name="dataSource" ref="dataSource"/>
<constructor-arg name="migrationsLocation" value="com/edorasware/commons/core/persistence/schema"/>
<constructor-arg name="transactionManager" ref="transactionManager"/>
</bean>
<bean id="databaseSchemaManager" class="com.edorasware.commons.core.persistence.schema.internal.StrategyBasedDatabaseSchemaManager" init-method="initialize">
<constructor-arg name="databaseSchemaService" ref="databaseSchemaService"/>
<constructor-arg name="strategy" value="CREATE_DROP"/>
</bean>
<bean id="databaseSchemaManagerLifecycleBean" class="com.edorasware.gear.core.persistence.schema.DatabaseSchemaManagerLifecycleBean">
<constructor-arg name="databaseSchemaManager" ref="databaseSchemaManager"/>
</bean>
The persistence management component can now be defined, and will automatically locate the beans that we have just defined:
<!-- Persistence Management definition -->
<gear:persistence-management id="persistenceManagement"
database-schema-creation-strategy="CREATE_DROP"/>
A number of different database types are supported. If not explicitly set in the definition of the persistence management component then the database type will automatically be derived from the current data source. As we are only going to write a unit test at this stage, we can create a new database schema every time the test runs.
For this reason we have chosen the For the complete set of persistence management options please refer to the edoras gear user guide. |
2.2.4. Identity management
The identity management component provides the current user and current tenant services:
<!-- Identity Management definition -->
<gear:identity-management id="identityManagement"/>
Our example will not be using any user or tenant management facilities, so we can just rely on the defaults.
2.2.5. A simple test process
The process definition that we will use for testing consists of one task:
The process description is contained in a BPMN file,SimpleProcess.bpmn20.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright (c) 2013. edorasware ag.
-->
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
targetNamespace="http://www.edorasware.com"
xsi:schemaLocation="http://www.omg.org/spec/BPMN/20100524/MODEL
http://www.omg.org/spec/BPMN/2.0/20100501/BPMN20.xsd">
<process id="simple-process" name="SimpleProcess">
<startEvent id="startEvent"/>
<userTask id="task1" name="SimpleTask"/>
<endEvent id="endEvent"/>
<sequenceFlow id="sequenceflow1" sourceRef="startEvent" targetRef="task1"/>
<sequenceFlow id="sequenceflow2" sourceRef="task1" targetRef="endEvent"/>
</process>
</definitions>
2.2.6. Task and process management
For our simple JUnit test we will use the Activiti process engine pre-configured with our simple process definition:
<!-- Process Engine definition -->
<gear:activiti-process-engine id="processEngine">
<gear:process-definitions>
<gear:resource location="classpath:com/edorasware/gear/documentation/setup/SimpleProcess.bpmn20.xml"/>
</gear:process-definitions>
</gear:activiti-process-engine>
We will also need the task and process management services from edoras gear. Both services must be connected to the process engine to allow access to the relevant work objects:
<!-- Task Management definition -->
<gear:task-management id="taskManagement">
<gear:activiti-task-provider process-engine="processEngine"/>
</gear:task-management>
<!-- Process Management definition -->
<gear:process-management id="processManagement">
<gear:activiti-process-provider process-engine="processEngine"/>
</gear:process-management>
<!-- Generic WorkObject Management definition -->
<gear:work-object-management id="workObjectManagement"/>
2.2.7. Complete application configuration
When we put all the parts together, we have the complete application configuration,
applicationConfig.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright (c) 2013. edorasware ag.
-->
<!-- tag::snippet0[] -->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:gear="http://www.edorasware.com/schema/gear"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.edorasware.com/schema/gear
http://www.edorasware.com/schema/gear/edoras-gear-3.0.2.S66.xsd">
<!-- end::snippet0[] -->
<!-- JUL-SLF4J logging rerouting -->
<bean id="julReroute" class="com.edorasware.commons.core.util.logging.JulToSlf4jBridgeHandlerInstaller" init-method="init"/>
<!-- tag::snippet2[] -->
<!-- License Management definition-->
<gear:license-management id="licenseManagement"/>
<!-- end::snippet2[] -->
<!-- tag::snippet3[] -->
<!-- Persistence bean definitions -->
<bean id="dataSource"
class="org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseFactoryBean">
<property name="databaseType" value="H2"/>
<property name="databaseName" value="SimpleProcessTest"/>
</bean>
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- configure edoras database schema service/manager -->
<bean id="databaseSchemaService" class="com.edorasware.commons.core.persistence.schema.internal.DefaultDatabaseSchemaService">
<constructor-arg name="dataSource" ref="dataSource"/>
<constructor-arg name="migrationsLocation" value="com/edorasware/commons/core/persistence/schema"/>
<constructor-arg name="transactionManager" ref="transactionManager"/>
</bean>
<bean id="databaseSchemaManager" class="com.edorasware.commons.core.persistence.schema.internal.StrategyBasedDatabaseSchemaManager" init-method="initialize">
<constructor-arg name="databaseSchemaService" ref="databaseSchemaService"/>
<constructor-arg name="strategy" value="CREATE_DROP"/>
</bean>
<bean id="databaseSchemaManagerLifecycleBean" class="com.edorasware.gear.core.persistence.schema.DatabaseSchemaManagerLifecycleBean">
<constructor-arg name="databaseSchemaManager" ref="databaseSchemaManager"/>
</bean>
<!-- end::snippet3[] -->
<!-- tag::snippet4[] -->
<!-- Persistence Management definition -->
<gear:persistence-management id="persistenceManagement"
database-schema-creation-strategy="CREATE_DROP"/>
<!-- end::snippet4[] -->
<!-- tag::snippet5[] -->
<!-- Identity Management definition -->
<gear:identity-management id="identityManagement"/>
<!-- end::snippet5[] -->
<!-- tag::snippet6[] -->
<!-- Process Engine definition -->
<gear:activiti-process-engine id="processEngine">
<gear:process-definitions>
<gear:resource location="classpath:com/edorasware/gear/documentation/setup/SimpleProcess.bpmn20.xml"/>
</gear:process-definitions>
</gear:activiti-process-engine>
<!-- end::snippet6[] -->
<!-- tag::snippet7[] -->
<!-- Task Management definition -->
<gear:task-management id="taskManagement">
<gear:activiti-task-provider process-engine="processEngine"/>
</gear:task-management>
<!-- Process Management definition -->
<gear:process-management id="processManagement">
<gear:activiti-process-provider process-engine="processEngine"/>
</gear:process-management>
<!-- Generic WorkObject Management definition -->
<gear:work-object-management id="workObjectManagement"/>
<!-- end::snippet7[] -->
<!-- tag::snippet1[] -->
</beans>
<!-- end::snippet1[] -->
2.2.8. Caching
edoras gear does not provide any kind of caching by default. If you want to enable caching of the immutable definitions (or any other service calls) then you are able to use the Spring caching abstraction which is based on interceptors.
To setup the caching for definitions you first need to add the aop
and cache
XML namespace declarations to your Spring configuration files:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:cache="http://www.springframework.org/schema/cache"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:gear="http://www.edorasware.com/schema/gear"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.springframework.org/schema/cache
http://www.springframework.org/schema/cache/spring-cache-3.2.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.2.xsd
http://www.edorasware.com/schema/gear
http://www.edorasware.com/schema/gear/edoras-gear-3.0.2.S66.xsd">
Next you need to define the cache manager which is easily replaceable by other implementations
provided by Spring. In this example we use a
SimpleCacheManager
which holds a
ConcurrentMapCache
as cache implementation.
<bean id="simpleCacheManager" class="org.springframework.cache.support.SimpleCacheManager">
<property name="caches">
<set>
<bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" name="findDefinitionById"/>
</set>
</property>
</bean>
If you want that your cache only stores the values in the cache after the transaction was
successfull, then you need to add a
TransactionAwareCacheManagerProxy
which wraps the
SimpleCacheManager
and ensures the transactional behavior.
<bean id="cacheManager" class="org.springframework.cache.transaction.TransactionAwareCacheManagerProxy">
<constructor-arg ref="simpleCacheManager"/>
</bean>
After we defined the cache managers we need to declare aspects for the public methods which
needs to be cached. In our example we will define a cache for the
findDefinitionById
public method of the`DomainObjectDefinitionService`. Please have a look at the Spring
documentation for further configuration options.
<aop:config>
<aop:advisor advice-ref="findDefinitionByIdAdvice" pointcut="execution(* com.edorasware.commons.core.service.entity.DomainObjectDefinitionService+.find*ById(..))"/>
</aop:config>
<cache:advice id="findDefinitionByIdAdvice" cache-manager="cacheManager">
<cache:caching>
<cache:cacheable method="find*ById" cache="findDefinitionById"/>
</cache:caching>
</cache:advice>
Now the definitions are being cached and with this way you are able to configure the caching for all definitions and other services you want to cache.
2.3. A simple unit test
To test that our edoras gear configuration and environment are correctly set up, we can write a simple unit test to start the test process, validate that a task is created, and then complete both the task and the process:
/*
* Copyright (c) 2013. edorasware ag.
*/
package com.edorasware.gear.documentation.setup;
import com.edorasware.gear.core.process.Process;
import com.edorasware.gear.core.process.ProcessDefinition;
import com.edorasware.gear.core.process.ProcessDefinitionService;
import com.edorasware.gear.core.process.ProcessId;
import com.edorasware.gear.core.process.ProcessService;
import com.edorasware.gear.core.task.Task;
import com.edorasware.gear.core.task.TaskId;
import com.edorasware.gear.core.task.TaskService;
import com.edorasware.commons.core.entity.State;
import com.edorasware.commons.core.entity.WorkObjectState;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.annotation.Transactional;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@ContextConfiguration("applicationContext.xml")
@RunWith(SpringJUnit4ClassRunner.class)
@DirtiesContext
@Transactional
public class SimpleProcessTest {
@Autowired
protected ProcessDefinitionService processDefinitionService;
@Autowired
protected ProcessService processService;
@Autowired
protected TaskService taskService;
@Test
public void runSimpleProcess() {
ProcessDefinition processDefinition =
this.processDefinitionService.findProcessDefinition(ProcessDefinition.KEY.eq("simple-process"));
// start the process
ProcessId processId = this.processService.startProcess(processDefinition.getId());
validateProcess(processId, WorkObjectState.ACTIVE);
// look up the task that the process created
Task task = this.taskService.findTask(Task.HIERARCHY.childOf(processId));
validateTask(task.getId(), WorkObjectState.ACTIVE);
// complete the task and the process
this.taskService.completeTask(task.getId(), "completed");
validateTask(task.getId(), WorkObjectState.COMPLETED);
validateProcess(processId, WorkObjectState.COMPLETED);
}
private void validateProcess(ProcessId processId, State expectedState) {
Process process = this.processService.findProcessById(processId);
assertNotNull(process);
assertEquals(expectedState, process.getState());
}
private void validateTask(TaskId taskId, State expectedState) {
Task task = this.taskService.findTaskById(taskId);
assertNotNull(task);
assertEquals(expectedState, task.getState());
}
}
2.4. Further information
A quick tour of the main features can be found in the edoras gear introduction. Complete information on additional options and features is available in the edoras gear user guide.
3. User Guide
3.1. Introduction
edoras gear provides the runtime environment to manage cases, processes, tasks, documents and timers through dedicated edoras gear Case Management, edoras gear Process Management, edoras gear Task Management, edoras gear Document Management, and edoras gear Timer Management components. All components are based on a provider architecture, which allows to integrate with any workflow or business process management system of choice. edoras gear ships with default providers which integrate against the Activiti workflow engine. In addition, the edoras gear Persistence Management component centrally defines all persistence and transaction aspects.
3.1.1. Architecture Overview
edoras gear offers dedicated services to manage and interact with all different workflow entities (cases, processes, tasks, documents, timers) and their definitions. Each service provides APIs for a specific entity. However, all services share the same architectural patterns. The following diagram gives an overview of all common service aspects:
① Manipulation & Ad-Hoc Creation
Entities are typically passed to edoras gear via dedicated provider implementations (see ⑥). Each service supports explicit APIs to modify existing entities. In scenarios where new entities need to be added ad-hoc, the service represents an entry point to add them programmatically.
② Queries
A dedicated Query API allows to find and count entities and their definitions based on arbitrary criteria. Query criteria are expressed in the form of predicates, which can be matched against all possible entity fields. Multiple predicates can be combined through arbitrary AND/OR operators. Please refer to section Query API for more details.
③ Life-Cycle Listeners
A dedicated listener infrastructure allows to observe an entity’s life-cycle transitions. For example, listener notifications can be used to log to an external system for auditing or statistical reasons. Beyond simple notification, certain listeners serve as hooks with the ability to modify or overrule the behavior of an executing action.
④ Workbasket Actions
Each service offers APIs which are particularly suited for workbasket operations. Functions like claiming, prioritizing, or escalating an entity represent common scenarios in any workflow application. These methods are especially useful when called through a higher-level management service which e.g. exposes bulk operations for a group of entities, or possibly decorates each call with appropriate permission management.
⑤ Variable-based Conversation
Each entity supports a variable-based data context which can be used for conversation purposes. Variables are either passed in along with the entity (through one or more providers, see ⑥), are configured as part of the Spring-based XML (see ⑨), or are programmatically added to an entity via a dedicated service method.
⑥ Providers
A provider infrastructure abstracts the integration of external workflow components. One or more providers can supply entities for a given service. For example, integration with the Activiti process engine is achieved in the form of ActivitiProcessProvider, ActivitiTaskProvider, and ActivitiTimerProvider implementations. The edoras gear Process Engine component (see ⑦) represents a further abstraction of these providers.
⑦ Process Engine
The edoras gear Process Engine component provides an abstraction over the Activiti workflow engine.
⑧ Persistence
All service operations are backed by a transactional, JDBC-based persistence layer. Please refer to section Persistence Management for more details.
⑨ Spring-based Configuration
Each service is configured via XML. Providers, listeners, and conversation variables are specified in a custom Spring namespace, which is backed by a fully documented schema. Traditional Spring beans can be referenced in a standard manner.
The fact that all services share the same architectural patterns also manifests itself in their XML configuration. Wherever possible, the configuration elements are structurally identical and only differ with respect to their entity-specific names. The following XML snippet nicely illustrates this similarity:
<gear:case-management id="caseManagement">
<gear:case-service-configuration id="caseService">
<gear:case-listeners>
<gear:action-listener ref="myCaseActionListener1"/>
<gear:action-listener ref="myCaseActionListener2"/>
</gear:case-listeners>
</gear:case-service-configuration>
<gear:default-case-provider/>
</gear:case-management>
<gear:process-management id="processManagement">
<gear:process-service-configuration id="processService">
<gear:process-listeners>
<gear:action-listener ref="myProcessActionListener1"/>
<gear:action-listener ref="myProcessActionListener2"/>
</gear:process-listeners>
</gear:process-service-configuration>
<gear:activiti-process-provider process-engine="processEngine"/>
</gear:process-management>
<gear:task-management id="taskManagement">
<gear:task-service-configuration id="taskService">
<gear:task-listeners>
<gear:action-listener ref="myTaskActionListener1"/>
<gear:action-listener ref="myTaskActionListener2"/>
</gear:task-listeners>
</gear:task-service-configuration>
<gear:activiti-task-provider process-engine="processEngine"/>
</gear:task-management>
<gear:document-management id="documentManagement">
<gear:document-service-configuration id="documentService">
<gear:document-listeners>
<gear:action-listener ref="myDocumentActionListener1"/>
<gear:action-listener ref="myDocumentActionListener2"/>
</gear:document-listeners>
</gear:document-service-configuration>
<gear:default-document-provider/>
</gear:document-management>
<gear:timer-management id="timerManagement">
<gear:timer-service-configuration id="timerService">
<gear:timer-listeners>
<gear:action-listener ref="myTimerActionListener1"/>
<gear:action-listener ref="myTimerActionListener2"/>
</gear:timer-listeners>
</gear:timer-service-configuration>
<gear:activiti-timer-provider process-engine="processEngine"/>
</gear:timer-management>
3.2. edoras gear Case Management
The edoras gear Case Management component exposes its case management functionality through the case service and the case definition service. Both services internally interact with one or more case providers to be notified about new case definitions being added, case instances being created, and existing cases being updated. In return, the services also notify the providers about any case changes that occur inside edoras gear.
The separation between services and providers makes it possible to hook in different kinds of system that are in charge of managing cases. Section Case Providers gives more details on the provider architecture.
The main elements and services of the edoras gear Case Management component can be accessed through the com.edorasware.gear.core.caze.CaseManagementConfiguration bean available in the bean registry:
CaseManagementConfiguration caseManagement = this.applicationContext.getBean(CaseManagementConfiguration.class);
PersistenceManagementConfiguration persistenceManagement = caseManagement.getPersistenceManagementConfiguration();
CaseService caseService = caseManagement.getCaseService();
CaseDefinitionService caseDefinitionService = caseManagement.getCaseDefinitionService();
3.2.1. Case Definition Service
The case definition service allows to read and query all deployed case definitions. It is of type com.edorasware.gear.core.caze.CaseDefinitionService.
The configured case definition service can be injected into a Spring bean or looked up from the application context either "by type" (type com.edorasware.gear.core.caze.CaseDefinitionService) or "by name" (based on the id specified in the case-definition-service-configuration element).
Case Definition Queries
Deployed case definitions can be queried from the case definition service by passing in a com.edorasware.gear.core.caze.CaseDefinitionQuery instance:
{
// find a specific case definition by id
CaseDefinition caseDefinition = this.caseDefinitionService.findCaseDefinitionById(CASE_DEFINITION_ID);
// retrieve its attributes
CaseDefinitionId id = caseDefinition.getId();
CaseDefinitionId externalId = caseDefinition.getExternalId();
CaseProviderId providerId = caseDefinition.getProviderId();
String key = caseDefinition.getKey();
String name = caseDefinition.getName();
Collection<Property> localProperties = caseDefinition.getLocalProperties();
Collection<Property> properties = caseDefinition.getProperties();
Property localPropertyShortNote = caseDefinition.getLocalProperty("shortNote");
Property propertyShortNote = caseDefinition.getProperty("shortNote");
String shortNote = caseDefinition.getLocalPropertyValue("shortNote");
}
{
// find all case definitions with a given key
Predicate matchesKey = CaseDefinition.KEY.eq("myCaseKey");
List<CaseDefinition> caseDefinitionsByKey = this.caseDefinitionService.findCaseDefinitions(matchesKey);
}
{
// find all case definitions with a given name
Predicate matchesName = CaseDefinition.NAME.eq("myCaseName");
List<CaseDefinition> caseDefinitionsByKey = this.caseDefinitionService.findCaseDefinitions(matchesName);
}
{
// find all case definitions with a given property
Predicate matchesPropertyName = CaseDefinition.PROPERTY.name().eq("shortNote");
Predicate matchesPropertyValue = CaseDefinition.PROPERTY.value().eq("simpleShortNote");
Predicate matchesProperty = Predicates.and(matchesPropertyName, matchesPropertyValue);
List<CaseDefinition> caseDefinitionsByProperty = this.caseDefinitionService.findCaseDefinitions(matchesProperty);
}
More advanced queries can be expressed through the Query API.
3.2.2. Case Service
The case service provides APIs to query for cases, execute workbasket actions, and to manually add cases (so called ad-hoc cases). The case service is of type com.edorasware.gear.core.caze.CaseService.
The case service can be injected into a Spring bean or looked up from the application context either "by type" (type com.edorasware.gear.core.caze.CaseService) or "by name" (based on the id specified in the case-service-configuration element).
Case Queries
Cases can be queried from the case service by passing in a com.edorasware.gear.core.case.CaseQuery instance:
{
// find a specific case by id
Case caze = this.caseService.findCaseById(CASE_ID);
// retrieve its attributes
CaseId id = caze.getId();
CaseId externalId = caze.getExternalId();
CaseDefinitionId definitionId = caze.getDefinitionId();
CaseProviderId providerId = caze.getProviderId();
String name = caze.getName();
UserId ownerId = caze.getOwnerId();
UserId assigneeId = caze.getAssigneeId();
UserId initialAssigneeId = caze.getInitialAssigneeId();
UserId previousAssigneeId = caze.getPreviousAssigneeId();
Set<UserId> candidateUserIds = caze.getCandidateUserIds();
Set<GroupId> candidateGroupIds = caze.getCandidateGroupIds();
Collection<Variable> caseVariables = caze.getVariables();
Variable variableCustomerId = caze.getVariable("customerId");
Variable localVariableCustomerId = caze.getLocalVariable("customerId");
Id customerId = caze.getVariableValue("customerId", Id.class);
State state = caze.getState();
Integer priority = caze.getPriority();
Date resubmissionTime = caze.getResubmissionTime();
Date dueTime = caze.getDueTime();
Date creationTime = caze.getCreationTime();
Date updateTime = caze.getUpdateTime();
Date assigneeIdUpdateTime = caze.getAssigneeIdUpdateTime();
Date stateUpdateTime = caze.getStateUpdateTime();
}
{
// find all cases for a case definition
Predicate matchesDefinitionId = Case.DEFINITION_ID.eq(CASE_DEFINITION_ID);
List<Case> casesByDefinitionId = this.caseService.findCases(matchesDefinitionId);
}
{
// find all cases with a given name
Predicate matchesName = Case.NAME.eq("Human Resources");
List<Case> casesByName = this.caseService.findCases(matchesName);
}
{
// find all open cases owned by user "anna"
Predicate isActive = Case.STATE.isActive();
Predicate isOwnedByAnna = Case.OWNER_ID.eq(UserId.get("anna"));
Predicate predicate = Predicates.and(isActive, isOwnedByAnna);
List<Case> casesByOwner = this.caseService.findCases(predicate);
}
{
// find all open cases assigned to user "bob" (personal workbasket)
Predicate isActive = Case.STATE.isActive();
Predicate isAssignedToBob = Case.ASSIGNEE_ID.eq(UserId.get("bob"));
Predicate predicate = Predicates.and(isActive, isAssignedToBob);
List<Case> personalWorkBasket = this.caseService.findCases(predicate);
}
{
// find all open cases for which user "jane" is a candidate (personal potential workbasket)
Predicate isActive = Case.STATE.isActive();
Predicate matchesCandidateUserJane = Case.CANDIDATE_USER_IDS.containsAnyOf(UserId.get("jane"));
Predicate predicate = Predicates.and(isActive, matchesCandidateUserJane);
List<Case> personalPotentialWorkBasket = this.caseService.findCases(predicate);
}
{
// find all open cases for which users in group "managers" are a candidate (group workbasket)
Predicate isActive = Case.STATE.isActive();
Predicate matchesGroupManagers = Case.CANDIDATE_GROUP_IDS.containsAnyOf(GroupId.get("managers"));
Predicate predicate = Predicates.and(isActive, matchesGroupManagers);
List<Case> casesByCandidateGroup = this.caseService.findCases(predicate);
}
{
// find all open cases for which users in groups "managers" and "employees" are candidates (union)
Predicate isActive = Case.STATE.isActive();
Predicate matchesGroupIds = Case.CANDIDATE_GROUP_IDS.containsAnyOf(GroupId.get("managers"), GroupId.get("employees"));
Predicate predicate = Predicates.and(isActive, matchesGroupIds);
List<Case> casesByCandidateGroups = this.caseService.findCases(predicate);
}
{
// find all open cases that have a specific variable set
Predicate isActive = Case.STATE.isActive();
Predicate matchesVariableName = Case.VARIABLE.name().eq("myVariableName");
Predicate matchesVariableValue = Case.VARIABLE.stringValue().eq("myVariableValue");
Predicate matchesVariable = Predicates.and(matchesVariableName, matchesVariableValue);
Predicate predicate = Predicates.and(isActive, matchesVariable);
List<Case> casesByVariable = this.caseService.findCases(predicate);
}
{
// find all cases that have been completed
Predicate isCompleted = Case.STATE.isCompleted();
List<Case> completedCases = this.caseService.findCases(isCompleted);
}
{
// find all open cases that have high priority
Predicate isActive = Case.STATE.isActive();
Predicate matchesPriority = Case.PRIORITY.eq(100);
Predicate predicate = Predicates.and(isActive, matchesPriority);
List<Case> highPriorityCases = this.caseService.findCases(predicate);
}
{
// find all open cases that need to be resubmitted tomorrow
Predicate isActive = Case.STATE.isActive();
Predicate matchesResubmissionTime = Case.RESUBMISSION_TIME.eq(tomorrow);
Predicate predicate = Predicates.and(isActive, matchesResubmissionTime);
List<Case> caseToBeResubmittedTomorrow = this.caseService.findCases(predicate);
}
{
// find all open cases that are due tomorrow
Predicate isActive = Case.STATE.isActive();
Predicate matchesDueTime = Case.DUE_TIME.eq(tomorrow);
Predicate predicate = Predicates.and(isActive, matchesDueTime);
List<Case> casesDueTomorrow = this.caseService.findCases(predicate);
}
More advanced queries can be expressed through the Query API.
Case Variable Modifications
During the entire life-time of a case, the data context of the case-level conversation can be modified by applying a set of variables. These variables are merged into the existing set of variables of the case-level data context. Existing variables are overwritten with the ones passed in. New variables contained in the set of passed-in variables are added to the data context. Variables that exist in the data context but that are not passed in are not modified in any way.
// define the case variables to update
Map<String, Object> variables = ImmutableMap.<String, Object>of(
"accepted", true,
"queue", 5);
// put the variables into the data context of the case-level conversation
this.caseService.putVariables(CASE_ID, variables, NO_DESCRIPTION);
Updating a variable with the same name as an already existing case variable will replace that variable, regardless of the previous or new scope.
Supported Variable Data Types are documented in the appendix.
Workbasket Actions
The case service supports the following workbasket actions:
-
own case: give an unowned case to a specific user, thus changing the case to owned
-
oust from case: remove the owner from an owned case, thus changing the case to unowned
-
claim case: assign an unassigned case to a specific user, thus changing the case to assigned
-
delegate case: delegate an assigned case to another assignee
-
revoke case: remove the assignee from an assigned case, and thus changing the case to unassigned
-
reserve case users: reserve a case for candidate users
-
cancel case users: cancel the candidate users from a case
-
reserve case groups: reserve a case for candidate groups
-
cancel case groups: cancel the candidate groups from a case
-
put variables: store variables on a case
-
set priority: set priority for a case
-
set resubmission time: set the date a case needs to be resubmitted
-
set due time: set the date by which a case is due for completion
-
complete case: mark an assigned case as completed, and thus remove it from the active cases
For each workbasket action that is called, an optional comment can be specified. The comment is currently not persisted but passed on to the registered case action listeners through case action events.
The following code examples demonstrate the various workbasket actions supported by the case service:
// users and groups
UserId bob = UserId.get("bob");
UserId ria = UserId.get("ria");
UserId anna = UserId.get("anna");
GroupId managers = GroupId.get("managers");
GroupId accountants = GroupId.get("accountants");
// make anna own the case
this.caseService.setOwner(CASE_ID, anna, NO_DESCRIPTION);
// transfer ownership of the case from anna to ria
this.caseService.setOwner(CASE_ID, ria, NO_DESCRIPTION);
// oust case from ria, changing the case to unowned
this.caseService.setOwner(CASE_ID, null, NO_DESCRIPTION);
// assign case to bob
this.caseService.setAssignedUser(CASE_ID, bob, NO_DESCRIPTION);
// delegate case from bob to anna
this.caseService.setAssignedUser(CASE_ID, anna, "requires special expertise");
// revoke case, changing the case to unassigned
this.caseService.setAssignedUser(CASE_ID, null, "going on vacation");
// reserve case for french speaking users
this.caseService.setCandidateUsers(CASE_ID, ImmutableSet.of(bob, anna), "french speaking");
// reserve case for groups of users that are domain experts
this.caseService.setCandidateGroups(CASE_ID, ImmutableSet.of(accountants, managers), "domain experts");
// cancel case for candidate users
this.caseService.setCandidateUsers(CASE_ID, ImmutableSet.<UserId>of(), NO_DESCRIPTION);
// cancel case for candidate users
this.caseService.setCandidateGroups(CASE_ID, ImmutableSet.<GroupId>of(), NO_DESCRIPTION);
// store case variable that signals request has been approved
this.caseService.putVariable(CASE_ID, "approved", true, NO_DESCRIPTION);
// set priority
this.caseService.setPriority(CASE_ID, 100, NO_DESCRIPTION);
// set resubmission time
this.caseService.setResubmissionTime(CASE_ID, oneWeekFromNow, NO_DESCRIPTION);
// set due time
this.caseService.setDueTime(CASE_ID, twoWeeksFromNow, NO_DESCRIPTION);
// complete case
this.caseService.completeCase(CASE_ID, "finally done");
The case service does not apply any checks on which user is calling the corresponding workbasket action. However, each workbasket operation validates certain constraints (e.g. whether the case is currently assigned or not). For more details on those constraints, please refer to the Javadoc of the corresponding methods.
Ad-Hoc Case Creation
Cases are typically passed to the edoras gear Case Management component via case providers. In scenarios where new cases need to be added ad-hoc, the case service offers an API to add them programmatically.
// add an ad-hoc case
Case caze = Case.builder().name("ad-hoc case").build();
this.caseService.addCase(caze, NO_DESCRIPTION);
Each case contributed to the case service, either via provider or via ad-hoc creation, needs to have a unique case id. In case of ad-hoc cases, the case id is generated by the edoras gear Persistence Management component and guaranteed to be unique. It is possible to explicitly provide a case id. In that case the contributor of the case has to ensure that the case id is actually unique.
In addition to the case id, each case has an external case id. This external case id needs to be either null or unique. It is not referenced anywhere by edoras gear. Typically, the external case id is either null or it contains a unique identifier that unambiguously maps the case back to its origin.
// add an ad-hoc case and let the persistence component of the system generate the case id
Case caze = Case.builder().externalId(CaseId.get("case-1")).build();
this.caseService.addCase(caze, NO_DESCRIPTION);
// add an ad-hoc case and apply the set case id
caze = Case.builder(CaseId.get("123456789")).externalId(CaseId.get("case-2")).build();
this.caseService.addCase(caze, NO_DESCRIPTION);
Listeners
The case service enables the developer to be notified about case actions via a listener mechanism.
Case Action Listener
The case action listeners are invoked whenever a high-level action is executed on a case, e.g. a case is created, claimed, completed, etc. The case action listener is invoked right before the action is performed and again after the action has been performed. During the invocation before the action is performed, the case action listener has the chance to modify, revert, and enhance the planned changes, e.g. to assign the case to another user than was planned:
private static class MyCaseActionListener extends BaseCaseActionListener {
private static final UserId DEFAULT_INITIAL_ASSIGNEE = UserId.get("anna");
@Override
public void actionWillBePerformed(CaseActionEvent caseActionEvent) {
if (caseActionEvent.isCreationEvent()) {
UserId assigneeId = caseActionEvent.getNewCase().getAssigneeId();
if (assigneeId == null) {
caseActionEvent.getCaseModificationBuilder().assigneeId(DEFAULT_INITIAL_ASSIGNEE);
}
}
}
}
Case-level Conversation
Each running case instance maintains a case-level conversation. This case-level conversation holds a data context which consists of a set of case variables of type com.edorasware.commons.core.entity.Variable. All activities within a case instance have access to and may manipulate the same set of case variables. The set of case variables is not a conclusive enumeration, but is created and modified as the case instance is executing.
The variables of a case can be accessed via the com.edorasware.gear.core.caze.Case class:
// case instance
Case caze = this.caseService.findCase(caseQuery);
// access all variables of the case
Collection<Variable> caseVariables = caze.getVariables();
// access a specific case variable
Variable caseVariableLastName = caze.getVariable("myVariableName");
// get the value of a case variable (assuming a String value type)
String myVariableValue = caseVariableLastName.getValue(String.class);
The case variables always reflect a snapshot taken at the time the com.edorasware.gear.core.caze.Case instance has been retrieved, e.g. via a case query. They are not updated automatically. In order to refresh the case variables, the corresponding case instance needs to be retrieved again via a case query.
3.2.3. Case Providers
A case provider acts as an adapter to the underlying system that is responsible for creating and completing cases. All case providers implement the com.edorasware.gear.core.caze.support.CaseProvider interface. In order to publish case life-cycle changes, a case provider needs to accept listeners and notify them when a case is created, updated, completed, or times out. In return, the case provider is notified when a case is completed. The case service knows how to interpret the case notifications sent by the case provider and it informs the case provider when a case gets completed through a workbasket action.
3.2.5. Configuration
This section describes how to configure the edoras gear Case Management component within an existing application.
Overview
The edoras gear Case Management component is configured via a custom Spring namespace. The custom Spring namespace can be used in any standard Spring configuration file. The following configuration settings are supported:
Setting | Description | Default Value |
---|---|---|
Id | The mandatory id of the case management configuration. The id can be used to inject the case management configuration into any other Spring bean "by name" or to get it from the application context. | (none) |
Persistence Management | A reference to the edoras gear Persistence Management bean that is used to get the persistence related configuration. The referenced bean must be of type com.edorasware.commons.core.persistence.PersistenceManagementConfiguration. | persistenceManagement |
Minimal Configuration
The following example shows a minimal Spring configuration of the edoras gear Case Management component:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:gear="http://www.edorasware.com/schema/gear"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.edorasware.com/schema/gear
http://www.edorasware.com/schema/gear/edoras-gear-3.0.2.S66.xsd">
<!-- definition of dataSource and transactionManager -->
<import resource="classpath:/com/edorasware/gear/documentation/test-license-config.xml"/>
<import resource="classpath:/test-persistence-config.xml"/>
<import resource="classpath:/com/edorasware/gear/documentation/identity-management-config.xml"/>
<import resource="classpath:/com/edorasware/gear/documentation/work-object-management-config.xml"/>
<gear:persistence-management id="persistenceManagement" database-schema-creation-strategy="${databaseSchemaCreationStrategy}"/>
<gear:case-management id="caseManagement"/>
</beans>
Custom Persistence Management Bean Name
The following example shows a Spring configuration that registers a case definition service and a case service, and that sets the persistence management component with the custom bean name myPersistenceManagement.
<gear:persistence-management id="myPersistenceManagement" database-schema-creation-strategy="${databaseSchemaCreationStrategy}"/>
<gear:case-management id="caseManagement" persistence-management="myPersistenceManagement"/>
Case Definition Service Configuration
The case definition service provides APIs to query for existing case definitions and add new definitions in an ad-hoc manner.
The case definition service is exposed in the application context and can be injected into any other Spring bean or retrieved from the application context "by type", using com.edorasware.gear.core.caze.CaseDefinitionService as the expected type. If access to the case definition service is required "by name", an id for the case definition service can be specified using the nested case-definition-service-configuration element within the case-management element:
<gear:case-management id="caseManagement">
<gear:case-definition-service-configuration id="myCaseDefinitionService"/>
</gear:case-management>
Case Service Configuration
The case service provides APIs to query for existing cases, execute workbasket actions, and to manually add cases (so called ad-hoc cases).
The case service is exposed in the application context and can be injected into any other Spring bean or retrieved from the application context "by type", using com.edorasware.gear.core.caze.CaseService as the expected type. If access to the case service is required "by name", an id for the case service can be specified using the nested case-service-configuration element within the case-management element:
<gear:case-management id="caseManagement">
<gear:case-service-configuration id="myCaseService"/>
</gear:case-management>
Listeners can be registered with the case service: a case action listener of type com.edorasware.gear.core.caze.support.CaseActionListener.
<bean id="myCaseActionListener1" class="com.edorasware.gear.documentation.MyCaseActionListener"/>
<gear:case-management id="caseManagement1">
<gear:case-service-configuration id="myCaseService1"
action-listener-ref="myCaseActionListener1"/>
</gear:case-management>
Alternatively, both listener types also support bulk registration. Multiple listener configurations can be nested in a case-listeners element:
<bean id="myCaseActionListener2" class="com.edorasware.gear.documentation.MyCaseActionListener"/>
<gear:case-management id="caseManagement2">
<gear:case-service-configuration id="myCaseService2">
<gear:case-listeners>
<gear:action-listener ref="myCaseActionListener2"/>
<gear:action-listener class="com.edorasware.gear.documentation.MyCaseActionListener"/>
</gear:case-listeners>
</gear:case-service-configuration>
</gear:case-management>
Case Provider Configuration
The case providers are responsible for feeding new cases to the case service and to complete cases passed down by the case service. One or more case providers must be specified. edoras gear comes with a default, but empty case provider.
<gear:case-management id="caseManagement1">
<gear:default-case-provider/>
</gear:case-management>
In all other cases, the case provider configuration references a bean of type com.edorasware.gear.core.caze.support.CaseProvider.
<bean id="customCaseProvider" class="com.edorasware.gear.documentation.MyCaseProvider">
<constructor-arg name="timeProvider" ref="timeProvider"/>
</bean>
<gear:case-management id="caseManagement2">
<gear:case-provider ref="customCaseProvider"/>
</gear:case-management>
Multiple case providers can be configured if there is more than one system that provides cases to the edoras gear Case Management component.
<bean id="firstCustomCaseProvider" class="com.edorasware.gear.documentation.MyCaseProvider1">
<constructor-arg name="timeProvider" ref="timeProvider"/>
</bean>
<bean id="secondCustomCaseProvider" class="com.edorasware.gear.documentation.MyCaseProvider2">
<constructor-arg name="timeProvider" ref="timeProvider"/>
</bean>
<gear:case-management id="caseManagement3">
<gear:case-providers>
<gear:case-provider ref="firstCustomCaseProvider"/>
<gear:case-provider ref="secondCustomCaseProvider"/>
<gear:case-provider ref="customCaseProvider"/>
<gear:default-case-provider/>
</gear:case-providers>
</gear:case-management>
3.3. edoras gear Process Management
The edoras gear Process Management component exposes its process management functionality through the process service and the process definition service. Both services internally interact with one or more process providers to be notified about new process definitions being added, process instances being created, and existing processes being updated. In return, the services also notify the providers about any process changes that occur inside edoras gear.
The separation between services and providers makes it possible to hook in different kinds of system that are in charge of managing processes. Section Process Providers gives more details on the provider architecture as a whole and on default provider implementations in particular.
The main elements and services of the edoras gear Process Management component can be accessed through the com.edorasware.gear.core.process.ProcessManagementConfiguration bean available in the bean registry:
ProcessManagementConfiguration processManagement = this.applicationContext.getBean(ProcessManagementConfiguration.class);
PersistenceManagementConfiguration persistenceManagement = processManagement.getPersistenceManagementConfiguration();
ProcessDefinitionService processDefinitionService = processManagement.getProcessDefinitionService();
ProcessService processService = processManagement.getProcessService();
3.3.1. Process Definition Service
The process definition service allows to read and query all deployed process definitions. It is of type com.edorasware.gear.core.process.ProcessDefinitionService.
The configured process definition service can be injected into a Spring bean or looked up from the application context either "by type" (type com.edorasware.gear.core.process.ProcessDefinitionService) or "by name" (based on the process definition service id specified in the process-definition-service element).
Process Definition Queries
Deployed process definitions can be queried from the process definition service by passing in a com.edorasware.gear.core.process.ProcessDefinitionQuery instance:
// find a specific process definition by id
ProcessDefinition processDefinition = processDefinitionService.findProcessDefinitionById(PROCESS_DEFINITION_ID);
// retrieve its attributes
ProcessDefinitionId id = processDefinition.getId();
ProcessDefinitionId externalId = processDefinition.getExternalId();
ProcessProviderId providerId = processDefinition.getProviderId();
String key = processDefinition.getKey();
String name = processDefinition.getName();
int version = processDefinition.getVersion();
Collection<Property> localProperties = processDefinition.getLocalProperties();
Collection<Property> properties = processDefinition.getProperties();
Property localPropertyShortNote = processDefinition.getLocalProperty("shortNote");
Property propertyShortNote = processDefinition.getProperty("shortNote");
String shortNote = processDefinition.getLocalPropertyValue("shortNote");
// find the deployed process definition in version 1 of the "order" process
ProcessDefinition firstProcessDefinition = this.processDefinitionService.findProcessDefinition(
Predicates.and(ProcessDefinition.KEY.eq("order"), ProcessDefinition.VERSION.eq(1)));
// find the latest deployed process definition of the "order" process
ProcessDefinition latestProcessDefinition = this.processDefinitionService.findProcessDefinition(
ProcessDefinition.LATEST_VERSION.withKey("order"));
// find the latest deployed process definition of the "order" process via convenience API
ProcessDefinition latestProcessDefinition2 = ProcessServiceUtils.findLatestVersionOfProcessDefinitionWithKey(
"order", this.processDefinitionService);
// find all deployed process definitions of the "order" process
List<ProcessDefinition> allOrderProcessDefinitions = processDefinitionService.findProcessDefinitions(
ProcessDefinition.KEY.eq("order"));
// find all known process definitions
List<ProcessDefinition> allProcessDefinitions = processDefinitionService.findProcessDefinitions(
Predicates.EMPTY);
More advanced queries can be expressed through the Query API.
3.3.2. Process Service
The process service provides APIs to query for processes, execute workbasket actions, and to manually add processes (so called ad-hoc processes). The process service is of type com.edorasware.gear.core.process.ProcessService.
The process service can be injected into a Spring bean or looked up from the application context either "by type" (type com.edorasware.gear.core.process.ProcessService) or "by name" (based on the process service id specified in the process-service element).
Starting a Process
A process instance is started by specifying the corresponding process definition to apply. Optionally, a list of variables can be provided. These variables are used to build the initial data context of the process-level conversation and are immediately available to all activities of the process instance, e.g. to user tasks and service tasks.
// first, find the latest process definition of the "myProcess" process
ProcessDefinitionId processDefinitionId = this.processDefinitionService.findProcessDefinition(
ProcessDefinition.LATEST_VERSION.withKey("myProcess")).getId();
// then, start a new process instance with initial variables for the given process definition
ImmutableMap<String, Object> variables = ImmutableMap.<String, Object>builder().
put("customer", "12345").
put("article", "123-45678-9").
build();
ProcessId processId = this.processService.startProcess(processDefinitionId, variables);
// alternatively, start a new process instance for the latest version of the "myProcess" process via convenience API
ProcessId otherProcessId = ProcessServiceUtils.startProcessForLatestVersionOfProcessDefinitionWithKey("myProcess", variables,
this.processDefinitionService, this.processService);
Process Queries
Running process instances can be queried from the process service by passing in a com.edorasware.gear.core.process.ProcessQuery instance:
// find a specific process instance by id
Process process = this.processService.findProcessById(PROCESS_ID);
// retrieve its attributes
ProcessId id = process.getId();
ProcessDefinitionId processDefinitionId = process.getDefinitionId();
String processName = process.getName();
Collection<Variable> processVariables = process.getVariables();
Variable variableReviewDate = process.getVariable("reviewDate");
Variable localVariableReviewDate = process.getLocalVariable("reviewDate");
Date reviewDate = process.getVariableValue("reviewDate", Date.class);
// find all running process instances with a given definition, regardless of their process definition version
List<Process> processesByDefinitionId = this.processService.findProcesses(
Process.DEFINITION_ID.eq(processDefinitionId));
// find all running process instances with a given definition that have a specific process variable set
List<Process> processesByVariable = this.processService.findProcesses(Predicates.and(
Process.DEFINITION_ID.eq(processDefinitionId),
Process.VARIABLE.name().eq("article"),
Process.VARIABLE.stringValue().eq("123-4"))
);
// find all running process instances that have a specific parent process
ProcessId parentProcessId = ProcessId.get("myParentProcess");
List<Process> subProcesses = this.processService.findProcesses(
Process.HIERARCHY.descendantOf(parentProcessId));
More advanced queries can be expressed through the Query API.
Process Variable Modifications
While a process instance is running, the data context of the process-level conversation can be modified by applying a set of variables. These variables are merged into the existing set of variables of the process-level data context. Existing variables are overwritten with the ones passed in. New variables contained in the set of passed-in variables are added to the data context. Variables that exist in the data context but that are not passed in are not modified in any way.
// define the process variables to update
ImmutableMap<String, Object> variables = ImmutableMap.<String, Object>builder().
put("article", "987-65432-1").
put("count", 5).build();
// put the variables into the data context of the process-level conversation
this.processService.putVariables(processId, variables, null);
Updating a variable with the same name as an already existing process variable will replace that variable, regardless of the previous or new scope.
Supported Variable Data Types are documented in the appendix.
Workbasket Actions
The process service supports the following workbasket actions:
-
own process: give an unowned process to a specific user, thus changing the process to owned
-
oust from process: remove the owner from an owned process, thus changing the process to unowned
-
claim process: assign an unassigned process to a specific user, thus changing the process to assigned
-
delegate process: delegate an assigned process to another assignee
-
revoke process: remove the assignee from an assigned process, and thus changing the process to unassigned
-
reserve process users: reserve a process for candidate users
-
cancel process users: cancel the candidate users from a process
-
reserve process groups: reserve a process for candidate groups
-
cancel process groups: cancel the candidate groups from a process
-
put variables: store variables on a process
-
set priority: set priority for a process
-
set resubmission time: set the date a process needs to be resubmitted
-
set due time: set the date by which a process is due for completion
-
complete process: mark an assigned process as completed, and thus remove it from the active processes
For each workbasket action that is called, an optional comment can be specified. The comment is currently not persisted but passed on to the registered process action listeners through process action events.
The following code examples demonstrate the various workbasket actions supported by the process service:
// users and groups
UserId bob = UserId.get("bob");
UserId ria = UserId.get("ria");
UserId anna = UserId.get("anna");
GroupId managers = GroupId.get("managers");
GroupId accountants = GroupId.get("accountants");
// make anna own the process
this.processService.setOwner(PROCESS_ID, anna, NO_DESCRIPTION);
// transfer ownership of the process from anna to ria
this.processService.setOwner(PROCESS_ID, ria, NO_DESCRIPTION);
// oust process from ria, changing the process to unowned
this.processService.setOwner(PROCESS_ID, null, NO_DESCRIPTION);
// assign process to bob
this.processService.setAssignedUser(PROCESS_ID, bob, NO_DESCRIPTION);
// delegate process from bob to anna
this.processService.setAssignedUser(PROCESS_ID, anna, "requires special expertise");
// revoke process, changing the process to unassigned
this.processService.setAssignedUser(PROCESS_ID, null, "going on vacation");
// reserve process for french speaking users
this.processService.setCandidateUsers(PROCESS_ID, ImmutableSet.of(bob, anna), "french speaking");
// reserve process for groups of users that are domain experts
this.processService.setCandidateGroups(PROCESS_ID, ImmutableSet.of(accountants, managers), "domain experts");
// cancel process for candidate users
this.processService.setCandidateUsers(PROCESS_ID, ImmutableSet.<UserId>of(), NO_DESCRIPTION);
// cancel process for candidate users
this.processService.setCandidateGroups(PROCESS_ID, ImmutableSet.<GroupId>of(), NO_DESCRIPTION);
// store process variable that signals request has been approved
this.processService.putVariable(PROCESS_ID, "approved", true, NO_DESCRIPTION);
// set priority
this.processService.setPriority(PROCESS_ID, 100, NO_DESCRIPTION);
// set resubmission time
this.processService.setResubmissionTime(PROCESS_ID, oneWeekFromNow, NO_DESCRIPTION);
// set due time
this.processService.setDueTime(PROCESS_ID, twoWeeksFromNow, NO_DESCRIPTION);
// complete process
this.processService.completeProcess(PROCESS_ID, "finally done");
The process service does not apply any checks on which user is calling the corresponding workbasket action. However, each workbasket operation validates certain constraints (e.g. whether the process is currently assigned or not). For more details on those constraints, please refer to the Javadoc of the corresponding methods.
Ad-Hoc Process Creation
Processes are typically passed to the edoras gear Process Management component via process providers. In scenarios where new processes need to be added ad-hoc, the process service offers an API to add them programmatically.
// add an ad-hoc process
Process process = Process.builder().name("ad-hoc process").build();
this.processService.addProcess(process, "new example process");
Each process contributed to the process service, either via provider or via ad-hoc creation, needs to have a unique process id. In case of ad-hoc processes, the process id is generated by the edoras gear Persistence Management component and guaranteed to be unique. It is possible to explicitly provide a process id. In that case the contributor of the process has to ensure that the process id is actually unique.
In addition to the process id, each process has an external process id. This external process id needs to be either null or unique. It is not referenced anywhere by edoras gear. Typically, the external process id is either null or it contains a unique identifier that unambiguously maps the process back to its origin.
// add an ad-hoc process and let the persistence component of the system generate the process id
process = Process.builder().externalId(ProcessId.get("process-1")).build();
this.processService.addProcess(process, NO_DESCRIPTION);
// add an ad-hoc process and apply the set process id
process = Process.builder(ProcessId.get("123456789")).externalId(ProcessId.get("process-2")).build();
this.processService.addProcess(process, NO_DESCRIPTION);
Listeners
The process service enables the developer to be notified about process actions via a listener mechanism.
Process Action Listener
The process action listeners are invoked whenever a high-level action is executed on a process, e.g. a process is created, claimed, completed, etc. The process action listener is invoked right before the action is performed and again after the action has been performed. During the invocation before the action is performed, the process action listener has the chance to modify, revert, and enhance the planned changes, e.g. to assign the process to another user than was planned:
private static class MyProcessActionListener extends BaseProcessActionListener {
private static final UserId DEFAULT_INITIAL_ASSIGNEE = UserId.get("anna");
@Override
public void actionWillBePerformed(ProcessActionEvent processActionEvent) {
if (processActionEvent.isCreationEvent()) {
UserId assigneeId = processActionEvent.getNewProcess().getAssigneeId();
if (assigneeId == null) {
processActionEvent.getProcessModificationBuilder().assigneeId(DEFAULT_INITIAL_ASSIGNEE);
}
}
}
}
Process Messages
The process service allows to send a message to running processes. The message consists of a message key and a message value. The recipient is typically a task of a process and can be further narrowed down by providing a qualifier when sending the message. How the signal reaches its recipient is implementation-specific. The default implementation only sends messages to active receive tasks.
boolean received = this.processService.sendMessage(processId, qualifier, messageKey, messageValue);
The default implementation of the process service supports two types of qualifiers which are explained in the following table.
Qualifier | Behavior | Return Value |
---|---|---|
null | The message is sent to all receive tasks of the process. | true if at least one receive task receives the message. |
List<TaskId> | The message is sent to all matching receive tasks. | true if at least one matching receive task receives the message. |
A task that receives a message is automatically completed, regardless of the message content. The process then continues.
Process-level Conversation
Each running process instance maintains a process-level conversation. This process-level conversation holds a data context which consists of a set of process variables of type com.edorasware.commons.core.entity.Variable. All activities within a process instance have access to and may manipulate the same set of process variables. The set of process variables is not a conclusive enumeration, but is created and modified as the process instance is started and executed.
The variables of a process can be accessed via the com.edorasware.gear.core.process.Process class:
// process instance
Process process = this.processService.findProcessById(PROCESS_ID);
// access all process variables of the process instance
Collection<Variable> processVariables = process.getVariables();
// access a specific process variable
Variable processVariableArticle = process.getVariable("article");
// get the value of a process variable (assuming a String value type)
String article = processVariableArticle.getValue(String.class);
The process variables always reflect a snapshot taken at the time the com.edorasware.gear.core.process.Process instance has been retrieved, e.g. via a process query. They are not updated automatically. In order to refresh the process variables, the corresponding process instance needs to be retrieved again via a process query.
3.3.3. Process Providers
A process provider acts as an adapter to the underlying system that is responsible for managing processes. All process providers implement the com.edorasware.gear.core.process.support.ProcessProvider interface. In order to publish process life-cycle changes, a process provider needs to accept listeners and notify them when a process is created, completed, or updated. In return, the providers are themselves notified about any process changes that occur inside edoras gear.
Default Process Provider
edoras gear comes with a default process provider implementation that adapts to the edoras gear Process Engine component.
3.3.5. Configuration
This section describes how to configure the edoras gear Process Management component.
Overview
The edoras gear Process Management component is configured via a custom Spring namespace. The custom Spring namespace can be used in any standard Spring configuration file. The following configuration settings are supported:
Setting | Description | Default Value |
---|---|---|
Id | The mandatory id of the process management configuration. The id can be used to inject the process management configuration into any other Spring bean "by name" or to get it from the application context. | (none) |
Persistence Management | A reference to the edoras gear Persistence Management bean that is used to get the persistence related configuration. The referenced bean must be of type com.edorasware.commons.core.persistence.PersistenceManagementConfiguration. | persistenceManagement |
Minimal Configuration
The following example shows a minimal Spring configuration of the edoras gear Process Management component used in conjunction with the edoras gear Process Engine:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:gear="http://www.edorasware.com/schema/gear"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.edorasware.com/schema/gear
http://www.edorasware.com/schema/gear/edoras-gear-3.0.2.S66.xsd">
<import resource="classpath:/com/edorasware/gear/documentation/logging-config.xml"/>
<import resource="classpath:/com/edorasware/gear/documentation/test-license-config.xml"/>
<import resource="classpath:/test-persistence-config.xml"/>
<import resource="classpath:/com/edorasware/gear/documentation/identity-management-config.xml"/>
<import resource="classpath:/com/edorasware/gear/documentation/work-object-management-config.xml"/>
<gear:persistence-management id="persistenceManagement" database-schema-creation-strategy="${databaseSchemaCreationStrategy}"/>
<gear:activiti-process-engine id="processEngine"/>
<gear:process-management id="processManagement">
<gear:activiti-process-provider process-engine="processEngine"/>
</gear:process-management>
</beans>
Custom Persistence Management Bean Name
The following example shows a Spring configuration that registers a process definition service and a process service that are backed by the edoras gear Process Engine component, and that sets the persistence management component with the custom bean name myPersistenceManagement.
<gear:persistence-management id="myPersistenceManagement" database-schema-creation-strategy="${databaseSchemaCreationStrategy}"/>
<gear:process-management id="processManagement" persistence-management="myPersistenceManagement">
<gear:activiti-process-provider process-engine="processEngine"/>
</gear:process-management>
<gear:activiti-process-engine id="processEngine" persistence-management="myPersistenceManagement"/>
Process Definition Service Configuration
The process definition service provides APIs to query for existing process definitions and add new definitions in an ad-hoc manner.
If the edoras gear Process Engine is used, the service can be configured to reference a default provider implementation which reads the process definition information from the standard Activiti database. This setup can be achieved via the process-engine attribute of the activiti-process-provider element.
The process definition service is exposed in the application context and can be injected into any other Spring bean or retrieved from the application context "by type", using com.edorasware.gear.core.process.ProcessDefinitionService as the expected type. If access to the process definition service is required "by name", an id for the process definition service can be specified using the nested process-definition-service-configuration element within the process-management element:
<gear:process-management id="processManagement">
<gear:process-definition-service-configuration id="myProcessDefinitionService"/>
<gear:activiti-process-provider process-engine="processEngine"/>
</gear:process-management>
Process Service Configuration
The process service provides APIs to query for processes, execute workbasket actions, and to manually add processes (so called ad-hoc processes).
If the edoras gear Process Engine is used, the service can be configured to reference a default provider implementation which reads the process information from the standard Activiti database. This setup can be achieved via the process-engine attribute of the activiti-process-provider element.
The process service is exposed in the application context and can be injected into any other Spring bean or retrieved from the application context "by type", using com.edorasware.gear.core.process.ProcessService as the expected type. If access to the process service is required "by name", an id for the process service can be specified using the nested process-service-configuration element within the process-management element:
<gear:process-management id="processManagement">
<gear:process-service-configuration id="myProcessService"/>
<gear:activiti-process-provider process-engine="processEngine"/>
</gear:process-management>
Listeners can be registered with the process service: a process action listener of type com.edorasware.gear.core.process.support.ProcessActionListener.
<bean id="myProcessActionListener1" class="com.edorasware.gear.documentation.MyProcessActionListener"/>
<gear:process-management id="processManagement1">
<gear:process-service-configuration id="myProcessService1"
action-listener-ref="myProcessActionListener1"/>
<gear:activiti-process-provider process-engine="processEngine"/>
</gear:process-management>
Alternatively, both listener types also support bulk registration. Multiple listener configurations can be nested in a process-listeners element:
<bean id="myProcessActionListener2" class="com.edorasware.gear.documentation.MyProcessActionListener"/>
<gear:process-management id="processManagement2">
<gear:process-service-configuration id="myProcessService2">
<gear:process-listeners>
<gear:action-listener ref="myProcessActionListener2"/>
<gear:action-listener class="com.edorasware.gear.documentation.MyProcessActionListener"/>
</gear:process-listeners>
</gear:process-service-configuration>
<gear:activiti-process-provider process-engine="processEngine"/>
</gear:process-management>
Process Provider Configuration
The process providers are responsible for feeding process definitions and processes to the process service and to manage changes thereof (as passed down by the service). One or more process providers must be specified.
If the edoras gear Process Engine is used, there is a default process provider implementation available. The default process provider references the process engine through the process-engine attribute of the activiti-process-provider element.
<gear:activiti-process-engine id="processEngine"/>
<gear:process-management id="processManagement1">
<gear:activiti-process-provider process-engine="processEngine"/>
</gear:process-management>
In all other cases, the process provider configuration references a bean of type com.edorasware.gear.core.process.support.ProcessProvider.
<bean id="customProcessProvider" class="com.edorasware.gear.documentation.MyProcessProvider">
<constructor-arg name="timeProvider" ref="timeProvider"/>
</bean>
<gear:process-management id="processManagement2">
<gear:process-provider ref="customProcessProvider"/>
</gear:process-management>
Multiple process providers can be configured if there is more than one system that provides processes to the edoras gear Process Management component.
<bean id="firstCustomProcessProvider" class="com.edorasware.gear.documentation.MyProcessProvider1">
<constructor-arg name="timeProvider" ref="timeProvider"/>
</bean>
<bean id="secondCustomProcessProvider" class="com.edorasware.gear.documentation.MyProcessProvider2">
<constructor-arg name="timeProvider" ref="timeProvider"/>
</bean>
<gear:process-management id="processManagement3">
<gear:process-providers>
<gear:process-provider ref="firstCustomProcessProvider"/>
<gear:process-provider ref="secondCustomProcessProvider"/>
<gear:process-provider ref="customProcessProvider"/>
<gear:activiti-process-provider process-engine="processEngine"/>
</gear:process-providers>
</gear:process-management>
3.4. edoras gear Task Management
The edoras gear Task Management component exposes its task management functionality through the task service and the task definition service. Both services internally interact with one or more task providers to be notified about new task definitions being added, task instances being created, and existing tasks being updated. In return, the services also notify the providers about any task changes that occur inside edoras gear.
The separation between services and providers makes it possible to hook in different kinds of system that are in charge of managing tasks. Section Task Providers gives more details on the provider architecture as a whole and on default provider implementations in particular.
The main elements and services of the edoras gear Task Management component can be accessed through the com.edorasware.gear.core.task.TaskManagementConfiguration bean available in the bean registry:
TaskManagementConfiguration taskManagement = this.applicationContext.getBean(TaskManagementConfiguration.class);
PersistenceManagementConfiguration persistenceManagement = taskManagement.getPersistenceManagementConfiguration();
TaskService taskService = taskManagement.getTaskService();
TaskDefinitionService taskDefinitionService = taskManagement.getTaskDefinitionService();
3.4.1. Task Definition Service
The task definition service allows to read and query all deployed task definitions. It is of type com.edorasware.gear.core.task.TaskDefinitionService.
The configured task definition service can be injected into a Spring bean or looked up from the application context either "by type" (type com.edorasware.gear.core.task.TaskDefinitionService) or "by name" (based on the id specified in the task-definition-service-configuration element).
Task Definition Queries
Deployed task definitions can be queried from the task definition service by passing in a com.edorasware.gear.core.task.TaskDefinitionQuery instance:
{
// find a specific task definition by id
TaskDefinition taskDefinition = this.taskDefinitionService.findTaskDefinitionById(TASK_DEFINITION_ID);
// retrieve its attributes
TaskDefinitionId id = taskDefinition.getId();
TaskDefinitionId externalId = taskDefinition.getExternalId();
TaskProviderId providerId = taskDefinition.getProviderId();
String key = taskDefinition.getKey();
String name = taskDefinition.getName();
Collection<Property> localProperties = taskDefinition.getLocalProperties();
Collection<Property> properties = taskDefinition.getProperties();
Property localPropertyShortNote = taskDefinition.getLocalProperty("shortNote");
Property propertyShortNote = taskDefinition.getProperty("shortNote");
String shortNote = taskDefinition.getLocalPropertyValue("shortNote");
}
{
// find all task definitions with a given key
Predicate matchesKey = TaskDefinition.KEY.eq("userTask");
List<TaskDefinition> taskDefinitionsByKey = this.taskDefinitionService.findTaskDefinitions(matchesKey);
}
{
// find all task definitions with a given name
Predicate matchesName = TaskDefinition.NAME.eq("simpleTask");
List<TaskDefinition> taskDefinitionsByName = this.taskDefinitionService.findTaskDefinitions(matchesName);
}
{
// find all task definitions with a given property
Predicate matchesPropertyName = TaskDefinition.PROPERTY.name().eq("shortNote");
Predicate matchesPropertyValue = TaskDefinition.PROPERTY.value().eq("simpleShortNote");
Predicate matchesProperty = Predicates.and(matchesPropertyName, matchesPropertyValue);
List<TaskDefinition> taskDefinitionsByProperty = this.taskDefinitionService.findTaskDefinitions(matchesProperty);
}
More advanced queries can be expressed through the Query API.
3.4.2. Task Service
The task service provides APIs to query for user tasks, execute workbasket actions, and to manually add tasks (so called ad-hoc tasks). The task service is of type com.edorasware.gear.core.task.TaskService.
The task service can be injected into a Spring bean or looked up from the application context either "by type" (type com.edorasware.gear.core.task.TaskService) or "by name" (based on the id specified in the task-service-configuration element).
Task Queries
User tasks can be queried from the task service by passing in a com.edorasware.gear.core.task.TaskQuery instance:
{
// find a specific task by id
Task task = this.taskService.findTaskById(TASK_ID);
// retrieve its attributes
TaskId id = task.getId();
TaskId externalId = task.getExternalId();
TaskDefinitionId definitionId = task.getDefinitionId();
TaskProviderId providerId = task.getProviderId();
String name = task.getName();
UserId ownerId = task.getOwnerId();
UserId assigneeId = task.getAssigneeId();
UserId initialAssigneeId = task.getInitialAssigneeId();
UserId previousAssigneeId = task.getPreviousAssigneeId();
Set<UserId> candidateUserIds = task.getCandidateUserIds();
Set<GroupId> candidateGroupIds = task.getCandidateGroupIds();
ProcessId processId = task.getParentProcessId();
Collection<Variable> taskVariables = task.getVariables();
Variable variableLastName = task.getVariable("lastName");
Variable localVariableLastName = task.getLocalVariable("lastName");
String lastName = task.getVariableValue("lastName", String.class);
State state = task.getState();
Integer priority = task.getPriority();
Date resubmissionTime = task.getResubmissionTime();
Date dueTime = task.getDueTime();
Date creationTime = task.getCreationTime();
Date updateTime = task.getUpdateTime();
Date assigneeIdUpdateTime = task.getAssigneeIdUpdateTime();
Date stateUpdateTime = task.getStateUpdateTime();
}
{
// find all tasks for task definition "signDocumentTask"
TaskDefinitionId definitionId = TaskDefinitionId.get("signDocumentTask");
Predicate matchesDefinitionId = Task.DEFINITION_ID.eq(definitionId);
List<Task> tasksByDefinitionId = this.taskService.findTasks(matchesDefinitionId);
}
{
// find all tasks with a given name
Predicate matchesName = Task.NAME.eq("Create Quarterly Report");
List<Task> tasksByName = this.taskService.findTasks(matchesName);
}
{
// find all open tasks owned by user "anna"
Predicate isActive = Task.STATE.isActive();
Predicate isOwnedByAnna = Task.OWNER_ID.eq(UserId.get("anna"));
Predicate predicate = Predicates.and(isActive, isOwnedByAnna);
List<Task> tasksByOwner = this.taskService.findTasks(predicate);
}
{
// find all open tasks assigned to user "bob" (personal workbasket)
Predicate isActive = Task.STATE.isActive();
Predicate isAssignedToBob = Task.ASSIGNEE_ID.eq(UserId.get("bob"));
Predicate predicate = Predicates.and(isActive, isAssignedToBob);
List<Task> personalWorkBasket = this.taskService.findTasks(predicate);
}
{
// find all open tasks for which user "jane" is a candidate (personal potential workbasket)
Predicate isActive = Task.STATE.isActive();
Predicate matchesCandidateUserJane = Task.CANDIDATE_USER_IDS.containsAnyOf(UserId.get("jane"));
Predicate predicate = Predicates.and(isActive, matchesCandidateUserJane);
List<Task> personalPotentialWorkBasket = this.taskService.findTasks(predicate);
}
{
// find all open tasks for which users in group "managers" are a candidate (group workbasket)
Predicate isActive = Task.STATE.isActive();
Predicate matchesGroupManagers = Task.CANDIDATE_GROUP_IDS.containsAnyOf(GroupId.get("managers"));
Predicate predicate = Predicates.and(isActive, matchesGroupManagers);
List<Task> tasksByCandidateGroup = this.taskService.findTasks(predicate);
}
{
// find all open tasks for which users in groups "managers" and "employees" are candidates (union)
Predicate isActive = Task.STATE.isActive();
Predicate matchesGroupIds = Task.CANDIDATE_GROUP_IDS.containsAnyOf(GroupId.get("managers"), GroupId.get("employees"));
Predicate predicate = Predicates.and(isActive, matchesGroupIds);
List<Task> tasksByCandidateGroups = this.taskService.findTasks(predicate);
}
{
// find all open tasks for a specific process instance
Predicate isActive = Task.STATE.isActive();
Predicate matchesProcessId = Task.HIERARCHY.descendantOf(PROCESS_ID);
Predicate predicate = Predicates.and(isActive, matchesProcessId);
List<Task> tasksByProcessId = this.taskService.findTasks(predicate);
}
{
// find all open tasks that have a specific task variable set
Predicate isActive = Task.STATE.isActive();
Predicate matchesVariableName = Task.VARIABLE.name().eq("lastName");
Predicate matchesVariableValue = Task.VARIABLE.stringValue().eq("Smith");
Predicate matchesVariable = Predicates.and(matchesVariableName, matchesVariableValue);
Predicate predicate = Predicates.and(isActive, matchesVariable);
List<Task> tasksByVariable = this.taskService.findTasks(predicate);
}
{
// find all tasks that have been completed
Predicate isCompleted = Task.STATE.isCompleted();
List<Task> completedTasks = this.taskService.findTasks(isCompleted);
}
{
// find all open tasks that have high priority
Predicate isActive = Task.STATE.isActive();
Predicate matchesPriority = Task.PRIORITY.eq(100);
Predicate predicate = Predicates.and(isActive, matchesPriority);
List<Task> highPriorityTasks = this.taskService.findTasks(predicate);
}
{
// find all open tasks that need to be resubmitted tomorrow
Predicate isActive = Task.STATE.isActive();
Predicate matchesResubmissionTime = Task.RESUBMISSION_TIME.eq(tomorrow);
Predicate predicate = Predicates.and(isActive, matchesResubmissionTime);
List<Task> taskToBeResubmittedTomorrow = this.taskService.findTasks(predicate);
}
{
// find all open tasks that are due tomorrow
Predicate isActive = Task.STATE.isActive();
Predicate matchesDueTime = Task.DUE_TIME.eq(tomorrow);
Predicate predicate = Predicates.and(isActive, matchesDueTime);
List<Task> tasksDueTomorrow = this.taskService.findTasks(predicate);
}
More advanced queries can be expressed through the Query API.
Task Variable Modifications
During the entire life-time of a task, the data context of the task-level conversation can be modified by applying a set of variables. These variables are merged into the existing set of variables of the task-level data context. Existing variables are overwritten with the ones passed in. New variables contained in the set of passed-in variables are added to the data context. Variables that exist in the data context but that are not passed in are not modified in any way.
// define the task variables to update
Map<String, Object> variables = ImmutableMap.<String, Object>of(
"accepted", true,
"queue", 5);
// put the variables into the data context of the task-level conversation
this.taskService.putVariables(TASK_ID, variables, NO_DESCRIPTION);
Updating a variable with the same name as an already existing task variable will replace that variable, regardless of the previous or new scope.
Supported Variable Data Types are documented in the appendix.
Workbasket Actions
The task service supports the following workbasket actions:
-
own task: give an unowned task to a specific user, thus changing the task to owned
-
oust from task: remove the owner from an owned task, thus changing the task to unowned
-
claim task: assign an unassigned task to a specific user, thus changing the task to assigned
-
delegate task: delegate an assigned task to another assignee
-
revoke task: remove the assignee from an assigned task, and thus changing the task to unassigned
-
reserve task users: reserve a task for candidate users
-
cancel task users: cancel the candidate users from a task
-
reserve task groups: reserve a task for candidate groups
-
cancel task groups: cancel the candidate groups from a task
-
put variables: store variables on a task
-
set priority: set priority for a task
-
set resubmission time: set the date a task needs to be resubmitted
-
set due time: set the date by which a task is due for completion
-
complete task: mark an assigned task as completed, and thus remove it from the active tasks
For each workbasket action that is called, an optional comment can be specified. The comment is currently not persisted but passed on to the registered task action listeners through task action events.
The following code examples demonstrate the various workbasket actions supported by the task service:
// users and groups
UserId bob = UserId.get("bob");
UserId ria = UserId.get("ria");
UserId anna = UserId.get("anna");
GroupId managers = GroupId.get("managers");
GroupId accountants = GroupId.get("accountants");
// make anna own the task
this.taskService.setOwner(TASK_ID, anna, NO_DESCRIPTION);
// transfer ownership of the task from anna to ria
this.taskService.setOwner(TASK_ID, ria, NO_DESCRIPTION);
// oust task from ria, changing the task to unowned
this.taskService.setOwner(TASK_ID, null, NO_DESCRIPTION);
// assign task to bob
this.taskService.setAssignedUser(TASK_ID, bob, NO_DESCRIPTION);
// delegate task from bob to anna
this.taskService.setAssignedUser(TASK_ID, anna, "requires special expertise");
// revoke task, changing the task to unassigned
this.taskService.setAssignedUser(TASK_ID, null, "going on vacation");
// reserve task for french speaking users
this.taskService.setCandidateUsers(TASK_ID, ImmutableSet.of(bob, anna), "french speaking");
// reserve task for groups of users that are domain experts
this.taskService.setCandidateGroups(TASK_ID, ImmutableSet.of(accountants, managers), "domain experts");
// cancel task for candidate users
this.taskService.setCandidateUsers(TASK_ID, ImmutableSet.<UserId>of(), NO_DESCRIPTION);
// cancel task for candidate users
this.taskService.setCandidateGroups(TASK_ID, ImmutableSet.<GroupId>of(), NO_DESCRIPTION);
// store task variable that signals request has been approved
this.taskService.putVariable(TASK_ID, "approved", true, NO_DESCRIPTION);
// set priority
this.taskService.setPriority(TASK_ID, 100, NO_DESCRIPTION);
// set resubmission time
this.taskService.setResubmissionTime(TASK_ID, oneWeekFromNow, NO_DESCRIPTION);
// set due time
this.taskService.setDueTime(TASK_ID, twoWeeksFromNow, NO_DESCRIPTION);
// complete task
this.taskService.completeTask(TASK_ID, "finally done");
The task service does not apply any checks on which user is calling the corresponding workbasket action. However, each workbasket operation validates certain constraints (e.g. whether the task is currently assigned or not). For more details on those constraints, please refer to the Javadoc of the corresponding methods.
Ad-Hoc Task Creation
Tasks are typically passed to the edoras gear Task Management component via task providers. In scenarios where new tasks need to be added ad-hoc, the task service offers an API to add them programmatically.
// add an ad-hoc task that is not bound to any process
Task task = Task.builder().name("unbound ad-hoc task").build();
this.taskService.addTask(task, "reminder task");
// add an ad-hoc task that is bound to the specified process
task = Task.builder().name("bound ad-hoc task").build();
this.taskService.addTask(task, PROCESS_ID, "phone call task");
Each task contributed to the task service, either via provider or via ad-hoc creation, needs to have a unique task id. In case of ad-hoc tasks, the task id is generated by the edoras gear Persistence Management component and guaranteed to be unique. It is possible to explicitly provide a task id. In that case the contributor of the task has to ensure that the task id is actually unique.
In addition to the task id, each task has an external task id. This external task id needs to be either null or unique. It is not referenced anywhere by edoras gear. Typically, the external task id is either null or it contains a unique identifier that unambiguously maps the task back to its origin.
// add an ad-hoc task and let the persistence component of the system generate the task id
task = Task.builder().externalId(TaskId.get("task-1")).build();
this.taskService.addTask(task, NO_DESCRIPTION);
// add an ad-hoc task and apply the set task id
task = Task.builder(TaskId.get("123456789")).externalId(TaskId.get("task-2")).build();
this.taskService.addTask(task, NO_DESCRIPTION);
Listeners
The tasks service enables the developer to be notified about task actions via a listener mechanism.
Task Action Listener
The task action listeners are invoked whenever a high-level action is executed on a task, e.g. a task is created, claimed, completed, etc. The task action listener is invoked right before the action is performed and again after the action has been performed. During the invocation before the action is performed, the task action listener has the chance to modify, revert, and enhance the planned changes, e.g. to assign the task to another user than was planned:
private static class MyTaskActionListener extends BaseTaskActionListener {
private static final UserId DEFAULT_INITIAL_ASSIGNEE = UserId.get("anna");
@Override
public void actionWillBePerformed(TaskActionEvent taskActionEvent) {
if (taskActionEvent.isCreationEvent()) {
UserId assigneeId = taskActionEvent.getNewTask().getAssigneeId();
if (assigneeId == null) {
taskActionEvent.getTaskModificationBuilder().assigneeId(DEFAULT_INITIAL_ASSIGNEE);
}
}
}
}
Task-level Conversation
Each user task maintains a task-level conversation. The task-level conversation is backed by a data context which consists of a set of task variables of type com.edorasware.commons.core.entity.Variable. The set of task variables is the aggregation of variables passed up by the task provider, the conversation variables as described by the conversation metadata, and the variables programmatically added to the task. Each task has access to its set of task variables.
Task variables passed up by the task provider are typically of scope PROCESS, meaning these variables apply to all tasks of a given process. Conversation variables of a task are always of scope TASK, meaning these variables may differ between different tasks of the same process. Variables programmatically added to the task are always of scope TASK.
The variables of a task can be accessed via the com.edorasware.gear.core.task.Task class:
// task instance
Task task = this.taskService.findTask(taskQuery);
// access all task variables of the task
Collection<Variable> taskVariables = task.getVariables();
// access a specific task variable
Variable taskVariableLastName = task.getVariable("lastName");
// get the value of a task variable (assuming a String value type)
String lastName = taskVariableLastName.getValue(String.class);
The task variables always reflect a snapshot taken at the time the com.edorasware.gear.core.task.Task instance has been retrieved, e.g. via a task query. They are not updated automatically. In order to refresh the task variables, the corresponding task instance needs to be retrieved again via a task query.
3.4.3. Task Providers
A task provider acts as an adapter to the underlying system that is responsible for creating and completing tasks. All task providers implement the com.edorasware.gear.core.task.support.TaskProvider interface. In order to publish task life-cycle changes, a task provider needs to accept listeners and notify them when a task is created, updated, completed, or times out. In return, the task provider is notified when a task is completed. The task service knows how to interpret the task notifications sent by the task provider and it informs the task provider when a task gets completed through a workbasket action.
Default Task Provider
edoras gear comes with a default task provider implementation that adapts to the edoras gear Process Engine component.
3.4.4. Conversation Metadata
For each task, a set of conversation variables is created whose values are calculated based on the conversation metadata definitions. Each conversation metadata definition consists of the names and expressions that make up its conversation variables. The expressions are resolved and calculated at runtime. All conversation variables are accessible on the task just like the variables contributed by the task providers and the variables added programmatically.
Conversation Metadata Lookup
By default, the concrete conversation metadata to apply for a given task is determined by the process definition of the process to which this task belongs: the process definition’s key and version are concatenated to a single key and used to find a mapping in the conversation metadata definitions. This lookup behavior is customizable by implementing a different lookup strategy. For example, a custom lookup strategy could ignore the process definition’s version, or it could first try to find a mapping for the name of the target task and if the look up fails it could fall back to the process definition, or the custom lookup strategy could even be based on existing task variables.
Conversation Variable Configuration
Each conversation variable has a name and a value. The value is calculated based on a configurable expression. The expression can reference Spring beans and variables provided by the task providers.
By default, the conversation metadata definitions happens via XML. The sample below shows how to declare the metadata definition for a conversation variable named customerName that will contain the first name and last name of the customer, assuming the customer object is stored in a variable by the name customer that is provided by a task provider.
The metadata definitions of the conversation variables are grouped and then associated with a conversation id. By default, the conversation id is the composition of a process definition key and a process definition version. The sample belows defines a group standardCustomer that contains the metadata definition of the conversation variable named customerName. This group is mapped to the process definition singleUserTaskProcess in version 1.
Conversation Variable Expressions
The expression that defines the value of a conversation variable can access Spring beans and other variables. Spring beans have precedence in the case of a name clash between a variable and a Spring bean with the same name. Expressions cannot reference other conversation variables. A conversation variable will override a variable with the same name that is provided by a task provider. Only conversation variables are searchable.
Supported Conversation Variable Data Types
The name of a calculated conversation variable is always of type java.lang.String. The value of a calculated conversation variable can be one of the following data types:
-
all basic Java data types ( boolean, int, …)
-
java.lang.String
-
java.util.Date
-
java.io.Serializable
-
null
3.4.5. Configuration
This section describes how to configure the edoras gear Task Management component within an existing application.
Overview
The edoras gear Task Management component is configured via a custom Spring namespace. The custom Spring namespace can be used in any standard Spring configuration file. The following configuration settings are supported:
Setting | Description | Default Value |
---|---|---|
Id | The mandatory id of the task management configuration. The id can be used to inject the task management configuration into any other Spring bean "by name" or to get it from the application context. | (none) |
Persistence Management | A reference to the edoras gear Persistence Management bean that is used to get the persistence related configuration. The referenced bean must be of type com.edorasware.commons.core.persistence.PersistenceManagementConfiguration. | persistenceManagement |
Minimal Configuration
The following example shows a minimal Spring configuration of the edoras gear Task Management component used in conjunction with the edoras gear Process Engine:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:gear="http://www.edorasware.com/schema/gear"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.edorasware.com/schema/gear
http://www.edorasware.com/schema/gear/edoras-gear-3.0.2.S66.xsd">
<import resource="classpath:/com/edorasware/gear/documentation/logging-config.xml"/>
<import resource="classpath:/com/edorasware/gear/documentation/test-license-config.xml"/>
<import resource="classpath:/test-persistence-config.xml"/>
<import resource="classpath:/com/edorasware/gear/documentation/identity-management-config.xml"/>
<import resource="classpath:/com/edorasware/gear/documentation/work-object-management-config.xml"/>
<gear:persistence-management id="persistenceManagement" database-schema-creation-strategy="${databaseSchemaCreationStrategy}"/>
<gear:activiti-process-engine id="processEngine"/>
<gear:task-management id="taskManagement">
<gear:activiti-task-provider process-engine="processEngine"/>
</gear:task-management>
</beans>
Custom Persistence Management Bean Name
The following example shows a Spring configuration that registers a task definition service and a task service that are backed by the edoras gear Process Engine component, and that sets the persistence management component with the custom bean name myPersistenceManagement.
<gear:persistence-management id="myPersistenceManagement" database-schema-creation-strategy="${databaseSchemaCreationStrategy}"/>
<gear:task-management id="taskManagement" persistence-management="myPersistenceManagement">
<gear:activiti-task-provider process-engine="processEngine"/>
</gear:task-management>
<gear:activiti-process-engine id="processEngine" persistence-management="myPersistenceManagement"/>
Task Definition Service Configuration
The task definition service provides APIs to query for existing task definitions and add new definitions in an ad-hoc manner.
If the edoras gear Process Engine is used, the service can be configured to reference a default provider implementation which reads the task definition information from the standard Activiti database. This setup can be achieved via the process-engine attribute of the default-task-provider element.
The task definition service is exposed in the application context and can be injected into any other Spring bean or retrieved from the application context "by type", using com.edorasware.gear.core.task.TaskDefinitionService as the expected type. If access to the task definition service is required "by name", an id for the task definition service can be specified using the nested task-definition-service-configuration element within the task-management element:
<gear:task-management id="taskManagement">
<gear:task-definition-service-configuration id="myTaskDefinitionService"/>
<gear:activiti-task-provider process-engine="processEngine"/>
</gear:task-management>
Task Service Configuration
The task service provides APIs to query for user tasks, execute workbasket actions, and to manually add tasks (so called ad-hoc tasks).
If the edoras gear Process Engine is used, the service can be configured to reference a default provider implementation which reads the task information from the standard Activiti database. This setup can be achieved via the process-engine attribute of the default-task-provider element.
The task service is exposed in the application context and can be injected into any other Spring bean or retrieved from the application context "by type", using com.edorasware.gear.core.task.TaskService as the expected type. If access to the task service is required "by name", an id for the task service can be specified using the nested task-service-configuration element within the task-management element:
<gear:task-management id="taskManagement">
<gear:task-service-configuration id="myTaskService"/>
<gear:activiti-task-provider process-engine="processEngine"/>
</gear:task-management>
Listeners can be registered with the task service: a task action listener of type com.edorasware.gear.core.task.support.TaskActionListener.
<bean id="myTaskActionListener1" class="com.edorasware.gear.documentation.MyTaskActionListener"/>
<gear:task-management id="taskManagement1">
<gear:task-service-configuration id="myTaskService1"
action-li