1. edoras gear - 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.

Tip

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.

Tip

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.1.1. Who should read this document?

This document is mainly intended for system architects, integrators and developers. Some technical knowledge is required as many code examples are given, and much of the discussion concerns technical and architectural issues.

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.:

company

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.

Tip

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>
  
      <!-- 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
  protected DesignCompanyUserService userService;
  
  @Inject
  protected TaskService 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;
  
  protected Date departureDate;
  protected Date returnDate;
  
  @Before
  public void initializeUsersAndGroups() {
      this.andyId = this.userService.lookupUserId("andy");
      this.annaId = this.userService.lookupUserId("anna");
      this.daveId = this.userService.lookupUserId("dave");
      this.adminId = this.userService.lookupGroupId("admin");
      this.managementId = this.userService.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 = this.taskService.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:

travel1

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 = this.taskService.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 = this.taskService.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 = Predicate.and(namePredicate, assigneePredicate);
  List<Task> tasks = this.taskService.findTasks(combined);
Tip

There are two ways to combine predicates. The example shown here uses the explicit Predicate.and() method. There is also a 'fluent' interface where predicates can be chained together (e.g.namePredicate.and(assigneePredicate). Which one to use is a matter of style, but typically the former shows the nesting better for complex predicate combinations, and the latter is more compact and readable for simple predicate combinations.

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 = this.taskService.countTasks(assigneePredicate);
Tip

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:

travel2

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:

  this.taskService.setName(taskId, "Travel request for Berlin: 13/3 - 15/3", "changed travel dates");
  
  Task updatedTask = this.taskService.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 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:

  this.taskService.setAssignedUser(taskId, this.annaId, "reassigned to Anna");
Tip

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 = this.taskService.findTasks(groupPredicate.and(unassignedPredicate));
  if (!tasks.isEmpty()) {
      this.taskService.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.

Tip

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:

travel3

By searching for all tasks with the "admin" candidate group we can also easily provide an overview of all administration tasks:

travel4

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 = this.taskService.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);
Tip

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, this.departureDate)
          .putVariable(RETURN_DATE, this.returnDate)
          .build();
  
  TaskId berlinRequestId = this.taskService.addTask(berlinRequest, null);

and also to read the values back out again:

  Task request = this.taskService.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:

  this.taskService.putVariable(berlinRequestId, DESTINATION, destination, "set request details");
  this.taskService.putVariable(berlinRequestId, IS_INTERNATIONAL, true, "set request details");
  this.taskService.putVariable(berlinRequestId, DEPART_DATE, departDate, "set request details");
  this.taskService.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();
  
  this.taskService.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:

travel5

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 = this.taskService.findTasks(namePredicate.and(valuePredicate));
Tip

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 and and or will apply all constraints to a single variable which may lead to unexpected results. This problem can be solved using multi-value predicates.

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 = this.taskService.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 = this.taskService.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:

  this.taskService.setSubState(openTaskId, OPEN, "travel is open");
  this.taskService.setSubState(bookedTaskId, BOOKED, "travel has been booked");
  this.taskService.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 = this.taskService.findTask(Task.ID.eq(taskId).and(Task.STATE.isActive()));
  if (activeTask != null) {
      this.taskService.setSubState(taskId, BOOKED, "change sub-state to 'booked'");
      this.taskService.completeTask(taskId, "task completed");
  }
  
  Task completedTask = this.taskService.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 = this.taskService.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:

travelsearch

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:

taskpool

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
  protected CaseService 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 = this.caseService.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, this.departureDate)
          .putVariable(RETURN_DATE, this.returnDate)
          .subState(OPEN)
          .build();
  
  TaskId bookingTaskId = this.taskService.addTask(bookingTask, travelCaseId, "add booking task");
  
  Task approvalTask = Task.builder()
          .name("Approve travel")
          .addCandidateGroupId(this.managementId)
          .build();
  
  TaskId approvalTaskId = this.taskService.addTask(approvalTask, travelCaseId, "add approval task");
Tip

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:

casestructure

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 = this.caseService.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 = this.taskService.findTasks(Task.HIERARCHY.childOf(caseId));
Tip

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):

casesearch

By clicking on the Details button, the user can open a detailed view showing the all of the corresponding tasks and their status:

tasklist

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:

taskdetails

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 this.caseService.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 this.taskService.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 this.taskService.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, this.departureDate, this.returnDate);
  
  TaskId approvalTaskId = createApprovalTask(travelCaseId);
  Task approvalTask = this.taskService.findTaskById(approvalTaskId);
  String approvalDestination = approvalTask.getVariableValue(DESTINATION);
  
  TaskId bookingTaskId = createBookingTask(travelCaseId);
  Task bookingTask = this.taskService.findTaskById(bookingTaskId);
  String bookingDestination = bookingTask.getVariableValue(DESTINATION);
Tip

When working with variables in a hierarchy, there are several useful features that you should be aware of:

  • variables contain a source ID indicating which work object is the variable 'owner'

  • you can explicitly ask for variables local to the current work object

  • a parent variable can be overwritten by a 'local' variable with the same name

  • you can use query hints to control the loading of hierarchy variables

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 = this.taskService.findTaskById(taskId);
      CaseId caseId = task.getParentCaseId();
  
      VariableMap variableMap = VariableMap.builder()
              .put(IS_APPROVED, approved)
              .put(APPROVAL_COMMENTS, comments)
              .build();
  
      this.caseService.putVariables(caseId, variableMap, "completed approval task");
      this.taskService.completeTask(taskId, "completed approval task");
  
      // ==== begin process logic ====
      if (approved) {
          createBookingTask(caseId);
      }
      // ==== end process logic ====
  }
  
  public void completeHardcodedBookingTask(TaskId taskId, String bookingDetails) {
      Task task = this.taskService.findTaskById(taskId);
      CaseId caseId = task.getParentCaseId();
  
      this.caseService.putVariable(caseId, BOOKING_DETAILS, bookingDetails, "completed booking task");
      this.taskService.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:

travel process

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"/>
Tip

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;
  }
Tip

The ServiceUtils class provides various utility methods to locate a process definition and start a process to create a new process instance. These are especially useful when several versions of the same process may be present in the system.

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 = this.taskService.findTaskById(taskId);
      CaseId caseId = task.getParentCaseId();
  
      VariableMap variableMap = VariableMap.builder()
              .put(IS_APPROVED, approved)
              .put(APPROVAL_COMMENTS, comments)
              .build();
  
      this.caseService.putVariables(caseId, variableMap, "completed approval task");
      this.taskService.completeTask(taskId, "completed approval task");
  }
  
  public void completeAutomatedBookingTask(TaskId taskId, String bookingDetails) {
      Task task = this.taskService.findTaskById(taskId);
      CaseId caseId = task.getParentCaseId();
  
      this.caseService.putVariable(caseId, BOOKING_DETAILS, bookingDetails, "completed booking task");
      this.taskService.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:

casehierarchy

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:

processhierarchy

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));
Tip

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();
Tip

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:

  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:

travel process mail

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.

Tip

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);
  this.caseService.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.

Tip

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.

Tip

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:

travel process timer

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.

Tip

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:

  • cancelling timers will interrupt the current task (indicating that it is no longer valid).

  • non-cancelling timers leave the current task unchanged.

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. edoras gear - 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.S89'
}
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.S89</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"/>
Tip

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 license-management component should have the ID licenseManagement.

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>

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"/>
Tip

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 create-drop schema creation strategy.

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:

simpleProcess

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>
      <!-- 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. edoras gear - 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:

Entity Service Architecture

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 = Predicate.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 = Predicate.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 = Predicate.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 = Predicate.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 = Predicate.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 = Predicate.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 = Predicate.and(matchesVariableName, matchesVariableValue);
      Predicate predicate = Predicate.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 = Predicate.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 = Predicate.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 = Predicate.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.

Default Case Provider

edoras gear comes with a default, but empty case provider.

3.2.4. Conversation Metadata

Work in progress.

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:/com/edorasware/gear/documentation/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"/>
  
  <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"/>
  <bean id="secondCustomCaseProvider" class="com.edorasware.gear.documentation.MyCaseProvider2"/>
  
  <gear:case-management id="caseManagement3">
      <gear:case-providers>
          <gear:case-provider ref="firstCustomCaseProvider"/>
          <gear:case-provider ref="secondCustomCaseProvider"/>
          <gear:case-provider class="com.edorasware.gear.documentation.MyCaseProvider"/>
          <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(
          Predicate.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(
          Predicate.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(Predicate.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.4. Conversation Metadata

Work in progress

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:/com/edorasware/gear/documentation/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"/>
  
  <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"/>
  <bean id="secondCustomProcessProvider" class="com.edorasware.gear.documentation.MyProcessProvider2"/>
  
  <gear:process-management id="processManagement3">
      <gear:process-providers>
          <gear:process-provider ref="firstCustomProcessProvider"/>
          <gear:process-provider ref="secondCustomProcessProvider"/>
          <gear:process-provider class="com.edorasware.gear.documentation.MyProcessProvider"/>
          <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 = Predicate.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 = Predicate.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 = Predicate.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 = Predicate.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 = Predicate.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 = Predicate.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 = Predicate.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 = Predicate.and(matchesVariableName, matchesVariableValue);
      Predicate predicate = Predicate.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 = Predicate.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 = Predicate.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 = Predicate.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:/com/edorasware/gear/documentation/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-listener-ref="myTaskActionListener1"/>
      <gear:activiti-task-provider process-engine="processEngine"/>
  </gear:task-management>

Alternatively, both listener types also support bulk registration. Multiple listener configurations can be nested in a task-listeners element:

  <bean id="myTaskActionListener2" class="com.edorasware.gear.documentation.MyTaskActionListener"/>
  
  <gear:task-management id="taskManagement2">
      <gear:task-service-configuration id="myTaskService2">
          <gear:task-listeners>
              <gear:action-listener ref="myTaskActionListener2"/>
              <gear:action-listener class="com.edorasware.gear.documentation.MyTaskActionListener"/>
          </gear:task-listeners>
      </gear:task-service-configuration>
      <gear:activiti-task-provider process-engine="processEngine"/>
  </gear:task-management>
Task Provider Configuration

The task providers are responsible for feeding new tasks to the task service and to complete tasks passed down by the task service. One or more task providers must be specified.

If the edoras gear Process Engine is used, there is a default task provider implementation available. The default task provider references the process engine through the process-engine attribute of the default-task-provider element.

  <gear:activiti-process-engine id="processEngine"/>
  
  <gear:task-management id="taskManagement1">
      <gear:activiti-task-provider process-engine="processEngine"/>
  </gear:task-management>

As an optional performance optimization, the default task provider can be configured to refrain from synchronizing its properties with the underlying Activiti workflow engine.

  <gear:task-management id="taskManagement2">
      <gear:activiti-task-provider process-engine="processEngine" suppressPropertySynchronization="true"/>
  </gear:task-management>

In all other cases, the task provider configuration references a bean of type com.edorasware.gear.core.task.support.TaskProvider.

  <bean id="customTaskProvider" class="com.edorasware.gear.documentation.MyTaskProvider"/>
  
  <gear:task-management id="taskManagement3">
      <gear:task-provider ref="customTaskProvider"/>
  </gear:task-management>

Multiple task providers can be configured if there is more than one system that provides tasks to the edoras gear Task Management component.

  <bean id="firstCustomTaskProvider" class="com.edorasware.gear.documentation.MyTaskProvider1"/>
  <bean id="secondCustomTaskProvider" class="com.edorasware.gear.documentation.MyTaskProvider2"/>
  
  <gear:task-management id="taskManagement4">
      <gear:task-providers>
          <gear:task-provider ref="firstCustomTaskProvider"/>
          <gear:task-provider ref="secondCustomTaskProvider"/>
          <gear:task-provider class="com.edorasware.gear.documentation.MyTaskProvider"/>
          <gear:activiti-task-provider process-engine="processEngine"/>
      </gear:task-providers>
  </gear:task-management>

3.5. edoras gear Document Management

The edoras gear Document Management component exposes its document management functionality through the document service and the document definition service. Both services internally interact with one or more document providers to be notified about new document definitions being added, document instances being created, and existing documents being updated. In return, the services also notify the providers about any document 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 documents. Section Document Providers gives more details on the provider architecture.

The main elements and services of the edoras gear Document Management component can be accessed through the com.edorasware.gear.core.document.DocumentManagementConfiguration bean available in the bean registry:

  DocumentManagementConfiguration documentManagement = this.applicationContext.getBean(DocumentManagementConfiguration.class);
  
  PersistenceManagementConfiguration persistenceManagement = documentManagement.getPersistenceManagementConfiguration();
  DocumentService documentService = documentManagement.getDocumentService();
  DocumentDefinitionService documentDefinitionService = documentManagement.getDocumentDefinitionService();

3.5.1. Document Definition Service

The document definition service allows to read and query all deployed document definitions. It is of type com.edorasware.gear.core.document.DocumentDefinitionService.

The configured document definition service can be injected into a Spring bean or looked up from the application context either "by type" (type com.edorasware.gear.core.document.DocumentDefinitionService) or "by name" (based on the id specified in the document-definition-service-configuration element).

Document Definition Queries

Deployed document definitions can be queried from the document definition service by passing in a com.edorasware.gear.core.document.DocumentDefinitionQuery instance:

  {
      // find a specific document definition by id
      DocumentDefinition documentDefinition = this.documentDefinitionService.findDocumentDefinitionById(DOCUMENT_DEFINITION_ID);
  
      // retrieve its attributes
      DocumentDefinitionId id = documentDefinition.getId();
      DocumentDefinitionId externalId = documentDefinition.getExternalId();
      DocumentProviderId providerId = documentDefinition.getProviderId();
      String key = documentDefinition.getKey();
      String name = documentDefinition.getName();
      Collection<Property> localProperties = documentDefinition.getLocalProperties();
      Collection<Property> properties = documentDefinition.getProperties();
      Property localPropertyShortNote = documentDefinition.getLocalProperty("shortNote");
      Property propertyShortNote = documentDefinition.getProperty("shortNote");
      String shortNote = documentDefinition.getLocalPropertyValue("shortNote");
  }
  
  {
      // find all document definitions with a given key
      Predicate matchesKey = DocumentDefinition.KEY.eq("myDocumentKey");
      List<DocumentDefinition> documentDefinitionsByKey = this.documentDefinitionService.findDocumentDefinitions(matchesKey);
  }
  
  {
      // find all document definitions with a given name
      Predicate matchesName = DocumentDefinition.NAME.eq("myDocumentName");
      List<DocumentDefinition> documentDefinitionsByKey = this.documentDefinitionService.findDocumentDefinitions(matchesName);
  }
  
  {
      // find all document definitions with a given property
      Predicate matchesPropertyName = DocumentDefinition.PROPERTY.name().eq("shortNote");
      Predicate matchesPropertyValue = DocumentDefinition.PROPERTY.value().eq("simpleShortNote");
      Predicate matchesProperty = Predicate.and(matchesPropertyName, matchesPropertyValue);
      List<DocumentDefinition> documentDefinitionsByProperty = this.documentDefinitionService.findDocumentDefinitions(matchesProperty);
  }

More advanced queries can be expressed through the Query API.

3.5.2. Document Service

The document service provides APIs to query for documents, execute workbasket actions, and to manually add documents (so called ad-hoc documents). The document service is of type com.edorasware.gear.core.document.DocumentService.

The document service can be injected into a Spring bean or looked up from the application context either "by type" (type com.edorasware.gear.core.document.DocumentService) or "by name" (based on the id specified in the document-service-configuration element).

Document Queries

Documents can be queried from the document service by passing in a com.edorasware.gear.core.document.DocumentQuery instance:

  {
      // find a specific document by id
      Document document = this.documentService.findDocumentById(DOCUMENT_ID);
  
      // retrieve its attributes
      DocumentId id = document.getId();
      DocumentId externalId = document.getExternalId();
      DocumentDefinitionId definitionId = document.getDefinitionId();
      DocumentProviderId providerId = document.getProviderId();
      String name = document.getName();
      UserId ownerId = document.getOwnerId();
      UserId assigneeId = document.getAssigneeId();
      UserId initialAssigneeId = document.getInitialAssigneeId();
      UserId previousAssigneeId = document.getPreviousAssigneeId();
      Set<UserId> candidateUserIds = document.getCandidateUserIds();
      Set<GroupId> candidateGroupIds = document.getCandidateGroupIds();
      Collection<Variable> documentVariables = document.getVariables();
      Variable variableLastModifier = document.getVariable("lastModifier");
      Variable localVariableLastModifier = document.getLocalVariable("lastModifier");
      String lastModifier = document.getVariableValue("lastModifier", String.class);
      State state = document.getState();
      Integer priority = document.getPriority();
      Date resubmissionTime = document.getResubmissionTime();
      Date dueTime = document.getDueTime();
      Date creationTime = document.getCreationTime();
      Date updateTime = document.getUpdateTime();
      Date assigneeIdUpdateTime = document.getAssigneeIdUpdateTime();
      Date stateUpdateTime = document.getStateUpdateTime();
  }
  
  {
      // find all documents for a document definition
      Predicate matchesDefinitionId = Document.DEFINITION_ID.eq(DOCUMENT_DEFINITION_ID);
      List<Document> documentsByDefinitionId = this.documentService.findDocuments(matchesDefinitionId);
  }
  
  {
      // find all documents with a given name
      Predicate matchesName = Document.NAME.eq("Human Resources");
      List<Document> documentsByName = this.documentService.findDocuments(matchesName);
  }
  
  {
      // find all open documents owned by user "anna"
      Predicate isActive = Document.STATE.isActive();
      Predicate isOwnedByAnna = Document.OWNER_ID.eq(UserId.get("anna"));
      Predicate predicate = Predicate.and(isActive, isOwnedByAnna);
      List<Document> documentsByOwner = this.documentService.findDocuments(predicate);
  }
  
  {
      // find all open documents assigned to user "bob" (personal workbasket)
      Predicate isActive = Document.STATE.isActive();
      Predicate isAssignedToBob = Document.ASSIGNEE_ID.eq(UserId.get("bob"));
      Predicate predicate = Predicate.and(isActive, isAssignedToBob);
      List<Document> personalWorkBasket = this.documentService.findDocuments(predicate);
  }
  
  {
      // find all open documents for which user "jane" is a candidate (personal potential workbasket)
      Predicate isActive = Document.STATE.isActive();
      Predicate matchesCandidateUserJane = Document.CANDIDATE_USER_IDS.containsAnyOf(UserId.get("jane"));
      Predicate predicate = Predicate.and(isActive, matchesCandidateUserJane);
      List<Document> personalPotentialWorkBasket = this.documentService.findDocuments(predicate);
  }
  
  {
      // find all open documents for which users in group "managers" are a candidate (group workbasket)
      Predicate isActive = Document.STATE.isActive();
      Predicate matchesGroupManagers = Document.CANDIDATE_GROUP_IDS.containsAnyOf(GroupId.get("managers"));
      Predicate predicate = Predicate.and(isActive, matchesGroupManagers);
      List<Document> documentsByCandidateGroup = this.documentService.findDocuments(predicate);
  }
  
  {
      // find all open documents for which users in groups "managers" and "employees" are candidates (union)
      Predicate isActive = Document.STATE.isActive();
      Predicate matchesGroupIds = Document.CANDIDATE_GROUP_IDS.containsAnyOf(GroupId.get("managers"), GroupId.get("employees"));
      Predicate predicate = Predicate.and(isActive, matchesGroupIds);
      List<Document> documentsByCandidateGroups = this.documentService.findDocuments(predicate);
  }
  
  {
      // find all open documents that have a specific variable set
      Predicate isActive = Document.STATE.isActive();
      Predicate matchesVariableName = Document.VARIABLE.name().eq("myVariableName");
      Predicate matchesVariableValue = Document.VARIABLE.stringValue().eq("myVariableValue");
      Predicate matchesVariable = Predicate.and(matchesVariableName, matchesVariableValue);
      Predicate predicate = Predicate.and(isActive, matchesVariable);
      List<Document> documentsByVariable = this.documentService.findDocuments(predicate);
  }
  
  {
      // find all documents that have been completed
      Predicate isCompleted = Document.STATE.isCompleted();
      List<Document> completedDocuments = this.documentService.findDocuments(isCompleted);
  }
  
  {
      // find all open documents that have high priority
      Predicate isActive = Document.STATE.isActive();
      Predicate matchesPriority = Document.PRIORITY.eq(100);
      Predicate predicate = Predicate.and(isActive, matchesPriority);
      List<Document> highPriorityDocuments = this.documentService.findDocuments(predicate);
  }
  
  {
      // find all open documents that need to be resubmitted tomorrow
      Predicate isActive = Document.STATE.isActive();
      Predicate matchesResubmissionTime = Document.RESUBMISSION_TIME.eq(tomorrow);
      Predicate predicate = Predicate.and(isActive, matchesResubmissionTime);
      List<Document> documentToBeResubmittedTomorrow = this.documentService.findDocuments(predicate);
  }
  
  {
      // find all open documents that are due tomorrow
      Predicate isActive = Document.STATE.isActive();
      Predicate matchesDueTime = Document.DUE_TIME.eq(tomorrow);
      Predicate predicate = Predicate.and(isActive, matchesDueTime);
      List<Document> documentsDueTomorrow = this.documentService.findDocuments(predicate);
  }

More advanced queries can be expressed through the Query API.

Document Variable Modifications

During the entire life-time of a document, the data context of the document-level conversation can be modified by applying a set of variables. These variables are merged into the existing set of variables of the document-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 document variables to update
  Map<String, Object> variables = ImmutableMap.<String, Object>of(
          "accepted", true,
          "queue", 5);
  
  // put the variables into the data context of the document-level conversation
  this.documentService.putVariables(DOCUMENT_ID, variables, NO_DESCRIPTION);

Updating a variable with the same name as an already existing document variable will replace that variable, regardless of the previous or new scope.

Supported Variable Data Types are documented in the appendix.

Workbasket Actions

The document service supports the following workbasket actions:

  • own document: give an unowned document to a specific user, thus changing the document to owned

  • oust from document: remove the owner from an owned document, thus changing the document to unowned

  • claim document: assign an unassigned document to a specific user, thus changing the document to assigned

  • delegate document: delegate an assigned document to another assignee

  • revoke document: remove the assignee from an assigned document, and thus changing the document to unassigned

  • reserve document users: reserve a document for candidate users

  • cancel document users: cancel the candidate users from a document

  • reserve document groups: reserve a document for candidate groups

  • cancel document groups: cancel the candidate groups from a document

  • put variables: store variables on a document

  • set priority: set priority for a document

  • set resubmission time: set the date a document needs to be resubmitted

  • set due time: set the date by which a document is due for completion

  • complete document: mark an assigned document as completed, and thus remove it from the active documents

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 document action listeners through document action events.

The following code examples demonstrate the various workbasket actions supported by the document 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 document
  this.documentService.setOwner(DOCUMENT_ID, anna, NO_DESCRIPTION);
  
  // transfer ownership of the document from anna to ria
  this.documentService.setOwner(DOCUMENT_ID, ria, NO_DESCRIPTION);
  
  // oust document from ria, changing the document to unowned
  this.documentService.setOwner(DOCUMENT_ID, null, NO_DESCRIPTION);
  
  // assign document to bob
  this.documentService.setAssignedUser(DOCUMENT_ID, bob, NO_DESCRIPTION);
  
  // delegate document from bob to anna
  this.documentService.setAssignedUser(DOCUMENT_ID, anna, "requires special expertise");
  
  // revoke document, changing the document to unassigned
  this.documentService.setAssignedUser(DOCUMENT_ID, null, "going on vacation");
  
  // reserve document for french speaking users
  this.documentService.setCandidateUsers(DOCUMENT_ID, ImmutableSet.of(bob, anna), "french speaking");
  
  // reserve document for groups of users that are domain experts
  this.documentService.setCandidateGroups(DOCUMENT_ID, ImmutableSet.of(accountants, managers), "domain experts");
  
  // cancel document for candidate users
  this.documentService.setCandidateUsers(DOCUMENT_ID, ImmutableSet.<UserId>of(), NO_DESCRIPTION);
  
  // cancel document for candidate users
  this.documentService.setCandidateGroups(DOCUMENT_ID, ImmutableSet.<GroupId>of(), NO_DESCRIPTION);
  
  // store document variable that signals request has been approved
  this.documentService.putVariable(DOCUMENT_ID, "approved", true, NO_DESCRIPTION);
  
  // set priority
  this.documentService.setPriority(DOCUMENT_ID, 100, NO_DESCRIPTION);
  
  // set resubmission time
  this.documentService.setResubmissionTime(DOCUMENT_ID, oneWeekFromNow, NO_DESCRIPTION);
  
  // set due time
  this.documentService.setDueTime(DOCUMENT_ID, twoWeeksFromNow, NO_DESCRIPTION);
  
  // complete document
  this.documentService.completeDocument(DOCUMENT_ID, "finally done");

The document 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 document is currently assigned or not). For more details on those constraints, please refer to the Javadoc of the corresponding methods.

Ad-Hoc Document Creation

Documents are typically passed to the edoras gear Document Management component via document providers. In scenarios where new documents need to be added ad-hoc, the document service offers an API to add them programmatically.

  // add an ad-hoc document
  Document document = Document.builder().name("ad-hoc document").build();
  this.documentService.addDocument(document, NO_DESCRIPTION);

Each document contributed to the document service, either via provider or via ad-hoc creation, needs to have a unique document id. In case of ad-hoc documents, the document id is generated by the edoras gear Persistence Management component and guaranteed to be unique. It is possible to explicitly provide a document id. In that case the contributor of the document has to ensure that the document id is actually unique.

In addition to the document id, each document has an external document id. This external document id needs to be either null or unique. It is not referenced anywhere by edoras gear. Typically, the external document id is either null or it contains a unique identifier that unambiguously maps the document back to its origin.

  // add an ad-hoc document and let the persistence component of the system generate the document id
  Document document = Document.builder().externalId(DocumentId.get("document-1")).build();
  this.documentService.addDocument(document, NO_DESCRIPTION);
  
  // add an ad-hoc document and apply the set document id
  document = Document.builder(DocumentId.get("123456789")).externalId(DocumentId.get("document-2")).build();
  this.documentService.addDocument(document, NO_DESCRIPTION);
Listeners

The document service enables the developer to be notified about document actions via a listener mechanism.

Document Action Listener

The document action listeners are invoked whenever a high-level action is executed on a document, e.g. a document is created, claimed, completed, etc. The document 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 document action listener has the chance to modify, revert, and enhance the planned changes, e.g. to assign the document to another user than was planned:

  private static class MyDocumentActionListener extends BaseDocumentActionListener {
  
      private static final UserId DEFAULT_INITIAL_ASSIGNEE = UserId.get("anna");
  
      @Override
      public void actionWillBePerformed(DocumentActionEvent documentActionEvent) {
          if (documentActionEvent.isCreationEvent()) {
              UserId assigneeId = documentActionEvent.getNewDocument().getAssigneeId();
              if (assigneeId == null) {
                  documentActionEvent.getDocumentModificationBuilder().assigneeId(DEFAULT_INITIAL_ASSIGNEE);
              }
          }
      }
  
  }
Document-level Conversation

Each running document instance maintains a document-level conversation. This document-level conversation holds a data context which consists of a set of document variables of type com.edorasware.commons.core.entity.Variable. All activities within a document instance have access to and may manipulate the same set of document variables. The set of document variables is not a conclusive enumeration, but is created and modified as the document instance is executing.

The variables of a document can be accessed via the com.edorasware.gear.core.document.Document class:

  // document instance
  Document document = this.documentService.findDocument(documentQuery);
  
  // access all variables of the document
  Collection<Variable> documentVariables = document.getVariables();
  
  // access a specific document variable
  Variable documentVariableLastName = document.getVariable("myVariableName");
  
  // get the value of a document variable (assuming a String value type)
  String myVariableValue = documentVariableLastName.getValue(String.class);

The document variables always reflect a snapshot taken at the time the com.edorasware.gear.core.document.Document instance has been retrieved, e.g. via a document query. They are not updated automatically. In order to refresh the document variables, the corresponding document instance needs to be retrieved again via a document query.

3.5.3. Document Providers

A document provider acts as an adapter to the underlying system that is responsible for creating and completing documents. All document providers implement the com.edorasware.gear.core.document.support.DocumentProvider interface. In order to publish document life-cycle changes, a document provider needs to accept listeners and notify them when a document is created, updated, completed, or times out. In return, the document provider is notified when a document is completed. The document service knows how to interpret the document notifications sent by the document provider and it informs the document provider when a document gets completed through a workbasket action.

Default Document Provider

edoras gear comes with a default, but empty document provider.

3.5.4. Conversation Metadata

Work in progress

3.5.5. Configuration

This section describes how to configure the edoras gear Document Management component within an existing application.

Overview

The edoras gear Document 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 document management configuration. The id can be used to inject the document 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 Document 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:/com/edorasware/gear/documentation/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:document-management id="documentManagement"/>
  
  </beans>
Custom Persistence Management Bean Name

The following example shows a Spring configuration that registers a document definition service and a document 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:document-management id="documentManagement" persistence-management="myPersistenceManagement"/>
Document Definition Service Configuration

The document definition service provides APIs to query for existing document definitions and add new definitions in an ad-hoc manner.

The document 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.document.DocumentDefinitionService as the expected type. If access to the document definition service is required "by name", an id for the document definition service can be specified using the nested document-definition-service-configuration element within the document-management element:

  <gear:document-management id="documentManagement">
      <gear:document-definition-service-configuration id="myDocumentDefinitionService"/>
  </gear:document-management>
Document Service Configuration

The document service provides APIs to query for existing documents, execute workbasket actions, and to manually add documents (so called ad-hoc documents).

The document 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.document.DocumentService as the expected type. If access to the document service is required "by name", an id for the document service can be specified using the nested document-service-configuration element within the document-management element:

  <gear:document-management id="documentManagement">
      <gear:document-service-configuration id="myDocumentService"/>
  </gear:document-management>

Listeners can be registered with the document service: a document action listener of type com.edorasware.gear.core.document.support.DocumentActionListener.

  <bean id="myDocumentActionListener1" class="com.edorasware.gear.documentation.MyDocumentActionListener"/>
  
  <gear:document-management id="documentManagement1">
      <gear:document-service-configuration id="myDocumentService1"
                                           action-listener-ref="myDocumentActionListener1"/>
  </gear:document-management>

Alternatively, both listener types also support bulk registration. Multiple listener configurations can be nested in a document-listeners element:

  <bean id="myDocumentActionListener2" class="com.edorasware.gear.documentation.MyDocumentActionListener"/>
  
  <gear:document-management id="documentManagement2">
      <gear:document-service-configuration id="myDocumentService2">
          <gear:document-listeners>
              <gear:action-listener ref="myDocumentActionListener2"/>
              <gear:action-listener class="com.edorasware.gear.documentation.MyDocumentActionListener"/>
          </gear:document-listeners>
      </gear:document-service-configuration>
  </gear:document-management>
Document Provider Configuration

The document providers are responsible for feeding new documents to the document service and to complete documents passed down by the document service. One or more document providers must be specified. edoras gear comes with a default, but empty document provider.

  <gear:document-management id="documentManagement1">
      <gear:default-document-provider/>
  </gear:document-management>

In all other cases, the document provider configuration references a bean of type com.edorasware.gear.core.document.support.DocumentProvider.

  <bean id="customDocumentProvider" class="com.edorasware.gear.documentation.MyDocumentProvider"/>
  
  <gear:document-management id="documentManagement2">
      <gear:document-provider ref="customDocumentProvider"/>
  </gear:document-management>

Multiple document providers can be configured if there is more than one system that provides documents to the edoras gear Document Management component.

  <bean id="firstCustomDocumentProvider" class="com.edorasware.gear.documentation.MyDocumentProvider1"/>
  <bean id="secondCustomDocumentProvider" class="com.edorasware.gear.documentation.MyDocumentProvider2"/>
  
  <gear:document-management id="documentManagement3">
      <gear:document-providers>
          <gear:document-provider ref="firstCustomDocumentProvider"/>
          <gear:document-provider ref="secondCustomDocumentProvider"/>
          <gear:document-provider class="com.edorasware.gear.documentation.MyDocumentProvider"/>
          <gear:default-document-provider/>
      </gear:document-providers>
  </gear:document-management>

3.6. edoras gear Timer Management

The edoras gear Timer Management component exposes its timer management functionality through the timer service and the timer definition service. Both services internally interact with one or more timer providers to be notified about new timer definitions being added, timer instances being created, and existing timers being updated. In return, the services also notify the providers about any timer 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 timers. Section Timer 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 Timer Management component can be accessed through the com.edorasware.gear.core.timer.TimerManagementConfiguration bean available in the bean registry:

  TimerManagementConfiguration timerManagement = this.applicationContext.getBean(TimerManagementConfiguration.class);
  
  PersistenceManagementConfiguration persistenceManagement = timerManagement.getPersistenceManagementConfiguration();
  TimerDefinitionService timerDefinitionService = timerManagement.getTimerDefinitionService();
  TimerService timerService = timerManagement.getTimerService();

3.6.1. Timer Definition Service

The timer definition service allows to read and query all deployed timer definitions. It is of type com.edorasware.gear.core.timer.TimerDefinitionService.

The configured timer definition service can be injected into a Spring bean or looked up from the application context either "by type" (type com.edorasware.gear.core.timer.TimerDefinitionService) or "by name" (based on the timer definition service id specified in the timer-definition-service element).

Timer Definition Queries

Deployed timer definitions can be queried from the timer definition service by passing in a com.edorasware.gear.core.timer.TimerDefinitionQuery instance:

  {
      // find a specific timer definition by id
      TimerDefinition timerDefinition = this.timerDefinitionService.findTimerDefinitionById(TIMER_DEFINITION_ID);
  
      // retrieve its attributes
      TimerDefinitionId id = timerDefinition.getId();
      TimerDefinitionId externalId = timerDefinition.getExternalId();
      TimerProviderId providerId = timerDefinition.getProviderId();
      TenantId tenantId = timerDefinition.getTenantId();
      String name = timerDefinition.getName();
      String description = timerDefinition.getDescription();
      Date creationTime = timerDefinition.getCreationTime();
      Date updateTime = timerDefinition.getUpdateTime();
      String key = timerDefinition.getKey();
      Collection<Property> properties = timerDefinition.getProperties();
      Property propertyShortNote = timerDefinition.getProperty("shortNote");
      String shortNote = timerDefinition.getPropertyValue("shortNote");
      Date validFrom = timerDefinition.getValidFrom();
      Date validTo = timerDefinition.getValidTo();
      int version = timerDefinition.getVersion();
      int externalVersion = timerDefinition.getExternalVersion();
      String resourceString = timerDefinition.getResourceString();
  }
  
  {
      // find all timer definitions with a given key
      Predicate matchesKey = TimerDefinition.KEY.eq("exampleTimerKey");
      List<TimerDefinition> timerDefinitionsByKey = this.timerDefinitionService.findTimerDefinitions(matchesKey);
  }
  
  {
      // find all timer definitions with a given name
      Predicate matchesName = TimerDefinition.NAME.eq("exampleTimerName");
      List<TimerDefinition> timerDefinitionsByName = this.timerDefinitionService.findTimerDefinitions(matchesName);
  }
  
  {
      // find all timer definitions with a given property
      Predicate matchesPropertyName = TimerDefinition.PROPERTY.name().eq("shortNote");
      Predicate matchesPropertyValue = TimerDefinition.PROPERTY.value().eq("simpleShortNote");
      Predicate matchesProperty = Predicate.and(matchesPropertyName, matchesPropertyValue);
      List<TimerDefinition> timerDefinitionsByProperty = this.timerDefinitionService.findTimerDefinitions(matchesProperty);
  }

More advanced queries can be expressed through the Query API.

3.6.2. Timer Service

The timer service provides APIs to query for timers, modify existing ones, and to manually add new timers (so called ad-hoc timers). The timer service is of type com.edorasware.gear.core.timer.TimerService.

The timer service can be injected into a Spring bean or looked up from the application context either "by type" (type com.edorasware.gear.core.timer.TimerService) or "by name" (based on the timer service id specified in the timer-service element).

Timer Queries

Running timer instances can be queried from the timer service by passing in a com.edorasware.gear.core.timer.TimerQuery instance:

  // find a specific timer instance by id
  Timer timer = this.timerService.findTimerById(TIMER_ID);
  
  // retrieve its attributes
  TimerId id = timer.getId();
  TimerId externalId = timer.getExternalId();
  TimerProviderId providerId = timer.getProviderId();
  TenantId tenantId = timer.getTenantId();
  String name = timer.getName();
  String description = timer.getDescription();
  Date creationTime = timer.getCreationTime();
  Date updateTime = timer.getUpdateTime();
  TimerDefinitionId timerDefinitionId = timer.getDefinitionId();
  State state = timer.getState();
  State subState = timer.getSubState();
  Collection<Variable> timerVariables = timer.getVariables();
  Variable timerVariableUrgency = timer.getVariable("urgency");
  Object urgency = timer.getVariableValue("urgency");
  Date stateUpdateTime = timer.getStateUpdateTime();
  Date subStateUpdateTime = timer.getSubStateUpdateTime();
  String expression = timer.getExpression();
  Date dueTime = timer.getDueTime();
  WorkObjectId targetWorkObjectId = timer.getTargetWorkObjectId();
  boolean managedExternally = timer.isManagedExternally();
  
  // find all timer instances with a given definition
  Predicate byDefinitionId = Timer.DEFINITION_ID.eq(timerDefinitionId);
  List<Timer> timersByDefinitionId = this.timerService.findTimers(byDefinitionId);
  
  // find all timer instances that have a specific due time
  Predicate byDueTimeRange = Timer.DUE_TIME.between(START_DATE, END_DATE);
  List<Timer> timersByDueTimeRange = this.timerService.findTimers(byDueTimeRange);

More advanced queries can be expressed through the Query API.

Timer Variable Modifications

While a timer instance is running, the data context of the timer-level conversation can be modified by applying a set of variables. These variables are merged into the existing set of variables of the timer-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 timer variables to update
  Map<String, Object> variables = ImmutableMap.<String, Object>of(
          "example", "exampleValue",
          "count", 5);
  
  // put the variables into the data context of the timer-level conversation
  this.timerService.putVariables(TIMER_ID, variables, null);

Updating a variable with the same name as an already existing timer variable will replace that variable, regardless of the previous or new scope.

Supported Variable Data Types are documented in the appendix.

Workbasket Actions

The timer service supports the following workbasket actions:

  • put variables: store variables on a timer

  • set expression: set the ISO-8601 expression that describes when the timer should fire

  • set due time: set the date by which a timer is due for completion

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 timer action listeners through timer action events.

The following code examples demonstrate the workbasket actions supported by the timer service:

  // store timer variable that signals approval
  this.timerService.putVariable(TIMER_ID, "approved", true, NO_DESCRIPTION);
  
  // set expression
  this.timerService.setExpression(TIMER_ID, tenHoursFromNow, NO_DESCRIPTION);
  
  // set due time
  this.timerService.setDueTime(TIMER_ID, twoWeeksFromNow, NO_DESCRIPTION);

The timer service does not apply any checks on which user is calling the corresponding workbasket action.

Ad-Hoc Timer Creation

Timers are typically passed to the edoras gear Timer Management component via timer providers. In scenarios where new timers need to be added ad-hoc, the timer service offers an API to add them programmatically.

  // add an ad-hoc timer and let the persistence component of the system generate both timer ids
  Timer timer = Timer.builder().build();
  this.timerService.addTimer(timer, "new example timer with no pre-set ids");

Each timer contributed to the timer service, either via provider or via ad-hoc creation, needs to have a unique timer id. In case of ad-hoc timers, the timer id is generated by the edoras gear Persistence Management component and guaranteed to be unique. It is possible to explicitly provide a timer id. In that case the contributor of the timer has to ensure that the timer id is actually unique.

In addition to the timer id, each timer has an external timer id. This external timer id needs to be either null or unique. It is not referenced anywhere by edoras gear. Typically, the external timer id is either null or it contains a unique identifier that unambiguously maps the timer back to its origin.

  // add an ad-hoc timer and let the persistence component of the system generate the primary timer id
  timer = Timer.builder().externalId(TimerId.get("timer-1")).build();
  this.timerService.addTimer(timer, "new example timer with external id already set");
  
  // add an ad-hoc timer and apply the set timer id
  timer = Timer.builder(TimerId.get("123456789")).externalId(TimerId.get("timer-2")).build();
  this.timerService.addTimer(timer, "new example timer with both ids already set");
Listeners

The timer service enables the developer to be notified about timer actions via a listener mechanism.

Timer Action Listener

The timer action listeners are invoked whenever a high-level action is executed on a timer, e.g. a timer is created, modified, removed etc. The timer 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 timer action listener has the chance to modify, revert, and enhance the planned changes, e.g. to set a default due date:

  private static class MyTimerActionListener extends BaseTimerActionListener {
  
      @Override
      public void actionWillBePerformed(TimerActionEvent timerActionEvent) {
          if (timerActionEvent.isCreationEvent()) {
              Date dueTime = timerActionEvent.getNewTimer().getDueTime();
              if (dueTime == null) {
                  timerActionEvent.getTimerModificationBuilder().dueTime(TOMORROW);
              }
          }
      }
  
  }
Timer-level Conversation

Each running timer instance maintains a timer-level conversation. This timer-level conversation holds a data context which consists of a set of timer variables of type com.edorasware.commons.core.entity.Variable. All activities within a timer instance have access to and may manipulate the same set of timer variables. The set of timer variables is not a conclusive enumeration, but is created and modified throughout the lifespan of a timer.

The variables of a timer can be accessed via the com.edorasware.gear.core.timer.Timer class:

  // timer instance
  Timer timer = this.timerService.findTimerById(TIMER_ID);
  
  // access all variables of the timer instance
  Collection<Variable> timerVariables = timer.getVariables();
  
  // access a specific timer variable
  Variable timerVariable = timer.getVariable("example");
  
  // get the value of a variable (assuming a String value type)
  String exampleValue = timerVariable.getValue(String.class);

The timer variables always reflect a snapshot taken at the time the com.edorasware.gear.core.timer.Timer instance has been retrieved, e.g. via a timer query. They are not updated automatically. In order to refresh the timer variables, the corresponding timer instance needs to be retrieved again via a timer query.

3.6.3. Timer Providers

A timer provider acts as an adapter to the underlying system that is responsible for managing timers. All timer providers implement the com.edorasware.gear.core.timer.support.TimerProvider interface. In order to publish timer life-cycle changes, a timer provider needs to accept listeners and notify them when a timer is created or removed. In return, the providers are themselves notified about any timer changes that occur inside edoras gear.

Default Timer Provider

edoras gear comes with a default timer provider implementation that adapts to the edoras gear Process Engine component.

3.6.4. Conversation Metadata

Work in progress

3.6.5. Configuration

This section describes how to configure the edoras gear Timer Management component.

Overview

The edoras gear Timer 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 timer management configuration. The id can be used to inject the timer 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 Timer 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:/com/edorasware/gear/documentation/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:timer-management id="timerManagement">
          <gear:activiti-timer-provider process-engine="processEngine"/>
      </gear:timer-management>
  
  </beans>
Custom Persistence Management Bean Name

The following example shows a Spring configuration that registers a timer definition service and a timer 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:timer-management id="timerManagement" persistence-management="myPersistenceManagement">
      <gear:activiti-timer-provider process-engine="processEngine"/>
  </gear:timer-management>
  
  <gear:activiti-process-engine id="processEngine" persistence-management="myPersistenceManagement"/>
Timer Definition Service Configuration

The timer definition service provides APIs to query for existing timer 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 timer definition information from the standard Activiti database. This setup can be achieved via the process-engine attribute of the default-timer-provider element.

The timer 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.timer.TimerDefinitionService as the expected type. If access to the timer definition service is required "by name", an id for the timer definition service can be specified using the nested timer-definition-service-configuration element within the timer-management element:

  <gear:timer-management id="timerManagement">
      <gear:timer-definition-service-configuration id="myTimerDefinitionService"/>
      <gear:activiti-timer-provider process-engine="processEngine"/>
  </gear:timer-management>
Timer Service Configuration

The timer service provides APIs to query for timers, execute workbasket actions, and to manually add timers (so called ad-hoc timers).

If the edoras gear Process Engine is used, the service can be configured to reference a default provider implementation which reads the timer information from the standard Activiti database. This setup can be achieved via the process-engine attribute of the default-timer-provider element.

The timer 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.timer.TimerService as the expected type. If access to the timer service is required "by name", an id for the timer service can be specified using the nested timer-service-configuration element within the timer-management element:

  <gear:timer-management id="timerManagement">
      <gear:timer-service-configuration id="myTimerService"/>
      <gear:activiti-timer-provider process-engine="processEngine"/>
  </gear:timer-management>

Listeners of type com.edorasware.gear.core.timer.support.TimerActionListener can be registered with the timer service. Multiple listener configurations can be nested in a timer-listeners element:

  <bean id="myTimerActionListener1" class="com.edorasware.gear.documentation.MyTimerActionListener"/>
  <bean id="myTimerActionListener2" class="com.edorasware.gear.documentation.MyTimerActionListener"/>
  
  <gear:timer-management id="timerManagement1">
      <gear:timer-service-configuration id="myTimerService1"
                                        action-listener-ref="myTimerActionListener1">
          <gear:timer-listeners>
              <gear:action-listener ref="myTimerActionListener2"/>
              <gear:action-listener class="com.edorasware.gear.documentation.MyTimerActionListener"/>
          </gear:timer-listeners>
      </gear:timer-service-configuration>
      <gear:activiti-timer-provider process-engine="processEngine"/>
  </gear:timer-management>
Timer Provider Configuration

The timer providers are responsible for feeding timer definitions and timers to the timer service and to manage changes thereof (as passed down by the service). One or more timer providers must be specified.

If the edoras gear Process Engine is used, there is a default timer provider implementation available. The default timer provider references the process engine through the process-engine attribute of the default-timer-provider element.

  <gear:activiti-process-engine id="processEngine"/>
  
  <gear:timer-management id="timerManagement1">
      <gear:activiti-timer-provider process-engine="processEngine"/>
  </gear:timer-management>

In all other cases, the timer provider configuration references a bean of type com.edorasware.gear.core.timer.support.TimerProvider. Multiple timer providers can be configured if there is more than one system that provides timers to the edoras gear Timer Management component.

3.7. edoras gear Persistence Management

The edoras gear Persistence Management component centrally defines all persistence and transaction aspects. Currently, these are the transaction manager, the data source, the database type, the strategy to apply regarding the life-cycle of the database upon startup and shutdown of edoras gear, and the primary key generator to query when persisting new elements to the database. The edoras gear Persistence Management component is referenced by all other components that involve persistence.

The main elements and services of the edoras gear Persistence Management component can be accessed through the com.edorasware.commons.core.persistence.PersistenceManagementConfiguration bean available in the bean registry:

  PersistenceManagementConfiguration persistenceManagement = this.applicationContext.getBean(
          PersistenceManagementConfiguration.class);
  
  PlatformTransactionManager transactionManager = persistenceManagement.getTransactionManager();
  DataSource dataSource = persistenceManagement.getDataSource();
  DatabaseType databaseType = persistenceManagement.getDatabaseType();
  DatabaseSchemaCreationStrategy schemaCreationStrategy = persistenceManagement.getDatabaseSchemaCreationStrategy();
  DatabaseMetadata databaseMetadata = persistenceManagement.getDatabaseMetadata();
  ConverterProvider converterProvider = persistenceManagement.getConverterProvider();
  PrimaryKeyGenerator primaryKeyGenerator = persistenceManagement.getPrimaryKeyGenerator();
  DdlHandler ddlHandler = persistenceManagement.getDdlHandler();

3.7.1. Transactions

The services provided by edoras gear involve database access and need to be run as part of a transaction. If a service is invoked as part of an already running transaction, the service will participate in that transaction. If no transaction is running at the time of a service invocation, the service starts a new transaction that is committed at the end of the service call.

When edoras gear is embedded into an application, the application must use the same transaction manager and data source instances in its internal services, repositories, etc. as the ones configured in the edoras gear Persistence Management component. This ensures atomic data modifications across both the application and the components of edoras gear.

3.7.2. Configuration

This section describes how to configure the edoras gear Persistence Management component.

Overview

The edoras gear Persistence 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 persistence management configuration. The id can be used to inject the persistence management configuration into any other Spring bean "by name" or to get it from the application context. (none)
Transaction Manager A reference to the Spring transaction manager bean used for all transactional persistence aspects. The referenced bean must be of type org.springframework.transaction.PlatformTransactionManager. transactionManager
Data Source A reference to the Spring data source bean used to persist data. The referenced bean must be of type javax.sql.DataSource. dataSource
Database Schema Creation Strategy The database schema creation strategy to apply upon startup and shutdown of edoras gear. create-or-validate
Database Type The database type for which edoras gear will apply the matching DDL scripts and SQL statements. If no value is specified, the edoras gear Persistence Management component attempts to infer the database type from the specified data source. (none)
Minimal Configuration

The following example shows the minimal Spring configuration to configure the persistence management component with a transaction manager, a data source, an optional specific database type, and a database schema creation strategy.

  <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">
  
      <bean id="myTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
          <property name="dataSource" ref="myDataSource"/>
      </bean>
  
      <bean id="myDataSource" class="org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseFactoryBean">
          <property name="databaseType" value="H2"/>
          <property name="databaseName" value="PersistenceManagementTest-minimalConfiguration"/>
      </bean>
  
      <gear:persistence-management
              id="myPersistenceManagement"
              transaction-manager="myTransactionManager"
              data-source="myDataSource"
              database-schema-creation-strategy="validate"
              database-type="h2"/>
  </beans>
Database Schema Creation Strategy

By default, the database schemas for the edoras gear components are created during application startup, if they do not yet exist, and are only validated for version compatibility if they already exist. By default, the database schemas are not dropped during application shutdown. The database schema creation strategy can be set using the database-schema-creation-strategy attribute on the persistence-management element:

  <gear:persistence-management
          id="myPersistenceManagement"
          database-schema-creation-strategy="validate"/>

The following values are supported:

Value Description
validate Validates the database schema (version compatibility).
create-or-validate Creates the database schema at startup, if it does not yet exist. Validates the schema, if it already exists.
create-drop Creates the database schema at startup, if it does not yet exist, and drops the database schema at shutdown. Useful for testing purposes only.
update-drop Updates the database schema, if it is not yet uptodate, and drops the database schema during shutdown. Useful for testing purposes only.
Database Type

The database type can be set using the database attribute on the persistence-management element:

  <gear:persistence-management
          id="myPersistenceManagement"
          database-type="h2"/>

The following values are supported:

Value Description
mssql Microsoft SQL Server (2008 or later).
oracle Oracle (11g or later).
db2 DB2 (9.x or later).
postgresql POSTGRESQL (9.x or later).
mysql MySQL (5.x or later).
derby Derby (10.6.1.0 or later).
h2 H2 (1.x or later).
Custom Converter Provider

In addition to the default converters provided by the persistence management component, additional new converters can be registered via a custom converter provider of type com.edorasware.commons.core.persistence.ConverterProvider. The custom converter provider is configured through the converter-provider attribute on the persistence-management element:

  <bean id="myConverterProvider" class="com.edorasware.gear.documentation.MyConverterProvider"/>
  
  <gear:persistence-management id="myPersistenceManagement"
                               converter-provider="myConverterProvider"/>

The converters contributed by the custom converter provider have precedence over the default converters.

Primary Key Generator

The primary key generator is queried to get the next available primary key when persisting a new element to the database. The default implementation is backed by a database table and generates sequential numeric primary numeric keys. The size of the block of primary keys to fetch is configurable:

  <gear:persistence-management id="persistenceManagement1">
      <!-- configure default -->
      <gear:default-primary-key-generator block-size="200"/>
  </gear:persistence-management>

This default implementation is cluster-safe.

A custom primary key generator can be configured via the primary-key-generator element:

  <gear:persistence-management id="persistenceManagement2">
      <!-- configure custom -->
      <gear:primary-key-generator ref="primaryKeyGenerator"/>
  </gear:persistence-management>

3.8. edoras gear Process Engine

The edoras gear Process Engine component provides an abstraction over the Activiti workflow engine.

3.8.1. Configuration

This section describes how to configure the edoras gear Process Engine component in an embedded scenario, i.e. integrated into an existing application.

Overview

The edoras gear Process Engine 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 engine. The id can be used to inject the process engine 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
Process Definitions The optional list of process definitions deployed by the process engine at startup. (none)
Minimal Configuration

The following example shows a minimal Spring configuration of 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-persistence-config.xml"/>
  
      <gear:persistence-management id="persistenceManagement" database-schema-creation-strategy="${databaseSchemaCreationStrategy}"/>
  
      <gear:activiti-process-engine id="processEngine"/>
  
  </beans>

The Spring convention names are used to find the persistence management bean that is injected into the process engine. The process engine is represented in the application context as an instance of type com.edorasware.gear.core.engine.ProcessEngineInfo and can be injected into any other Spring bean "by type". The specified process engine id is used as the bean name to get the com.edorasware.gear.core.engine.ProcessEngineInfo from the application context. Hence, the process engine can also be injected into other Spring beans "by name". Programmatically, the process engine id is available through the com.edorasware.gear.core.config.ProcessEngineInfo#getProcessEngineId() API.

This minimal configuration of the process engine creates the database schema, if it does not yet exist, but it does not deploy any process definitions at startup.

Note: currently, only one process engine is supported per application context. Declaring multiple process engines per application context results in an exception at application startup.

Custom Persistence Management Bean Name

If the bean name of the persistence management component does not match the Spring convention names, i.e. it is different from persistenceManagement, the bean name can be configured explicitly:

  <gear:persistence-management id="myPersistenceManagement" database-schema-creation-strategy="${databaseSchemaCreationStrategy}"/>
  
  <gear:activiti-process-engine id="processEngine" persistence-management="myPersistenceManagement"/>
Specifying Process Definitions

Process definitions can be deployed automatically at application startup. This is achieved by declaring a list of BPMN 2.0 XML process definition files within the activiti-process-engine configuration using the process-definitions element:

  <gear:activiti-process-engine id="processEngine">
      <gear:process-definitions>
          <gear:resource location="classpath:/com/edorasware/gear/documentation/order/orderProcess.bpmn20.xml"/>
          <gear:resource location="classpath:/com/edorasware/gear/documentation/expense/expenseProcess.bpmn20.xml"/>
      </gear:process-definitions>
  </gear:activiti-process-engine>

The value specified inside the resource elements supports all Spring resource patterns and schemes (classpath, file, http, …​).

Alternatively, process definitions can be declared in a top-level element. In that case, they can be referenced from the activiti-process-engine configuration element using the process-definitions attribute:

  <gear:process-definitions id="myProcessDefinitions">
      <gear:resource location="classpath:/com/edorasware/gear/documentation/order/orderProcess.bpmn20.xml"/>
      <gear:resource location="classpath:/com/edorasware/gear/documentation/expense/expenseProcess.bpmn20.xml"/>
  </gear:process-definitions>
  
  <gear:activiti-process-engine id="processEngine" process-definitions="myProcessDefinitions"/>

This makes it possible to override the process definitions in other Spring configuration files or in child application contexts.

Advanced Process Engine Configuration

In certain cases, more fine-grained control over the configuration of the underlying process engine is required. The edoras gear Process Engine component supports a generic mechanism for specifying advanced configuration settings that are delegated to the underlying process engine factory. Advanced configuration properties can be defined via the nested process-engine-configuration element:

  <gear:activiti-process-engine id="processEngine">
      <gear:process-engine-configuration>
          <!-- deactivate job executor -->
          <gear:property name="jobExecutorActivate" value="false"/>
  
          <!-- set reference to custom expression manager bean -->
          <gear:property name="expressionManager" ref="myExpressionManager"/>
      </gear:process-engine-configuration>
  </gear:activiti-process-engine>
  
  <bean id="myExpressionManager" class="com.edorasware.gear.documentation.MyExpressionManager"/>

Property values can be literals or references to Spring beans. The set of property names and values supported by the advanced process engine configuration mechanism depends on the underlying process engine factory implementation. By default, all properties available in the hierarchy of the com.edorasware.gear.core.engine.support.activiti.ActivitiProcessEngineConfiguration class are supported.

Properties configured via the advanced configuration settings override the properties set by the edoras gear Process Engine component.

Configuration properties are applied to the underlying process engine by the process engine factory. By default, a process engine factory that configures and creates an Activiti process engine is used. The default process engine factory supports the conversion of literal values to the required target type based on Spring’s default property editor support (e.g. boolean, int, …​). In case the way a specific property value is applied needs to be controlled more explicitly, a custom implementation of the com.edorasware.gear.core.engine.support.ProcessEngineFactory interface can be defined as a Spring bean and referenced via the process-engine-factory attribute of the process-engine-configuration element:

  <gear:activiti-process-engine id="processEngine">
      <gear:process-engine-configuration process-engine-factory="myProcessEngineFactory"/>
  </gear:activiti-process-engine>
  
  <bean id="myProcessEngineFactory" class="com.edorasware.gear.documentation.MyProcessEngineFactory"/>

The custom process engine factory implementation is responsible for applying the process engine configuration properties to the underlying process engine and for creating the actual instance of the underlying process engine.

The class com.edorasware.gear.core.engine.support.activiti.ActivitiProcessEngineFactory acts as a good starting point to extend the Activiti process engine configuration. It provides various hooks to extend or customize the configuration of the Activiti process engine.

Access to Activiti Process Engine

If the default com.edorasware.gear.core.engine.support.activiti.ActivitiProcessEngineFactory (or a custom sub-class) is used to create the underlying process engine, access to the Activiti process engine (of type org.activiti.engine.ProcessEngine) is possible via the com.edorasware.gear.core.engine.ProcessEngineInfo available from the application context:

  // injected by type / by name
  ProcessEngineInfo processEngineInfo = injectedProcessEngineInfo;
  
  // type org.activiti.engine.ProcessEngine
  ProcessEngine activitiProcessEngine = processEngineInfo.getNativeProcessEngine(ProcessEngine.class);

In case the Activiti process engine instance has to be exposed directly (and not only via the process engine info instance) in the application context, the com.edorasware.gear.core.engine.config.activiti.ActivitiProcessEngineFactoryBean can be used:

  <bean id="activitiProcessEngine"
        class="com.edorasware.gear.core.engine.config.activiti.ActivitiProcessEngineFactoryBean">
      <property name="processEngineInfo" ref="processEngine"/>
  </bean>

The factory bean exposes the Activiti process engine of type org.activiti.engine.ProcessEngine under the bean id activitiProcessEngine, which then can be injected into any other Spring bean (by name or by type).

Database Configuration

All database aspects are configured in the referenced edoras gear Persistence Management component.

Example configuration:

  <gear:persistence-management id="myPersistenceManagement"
                               database-schema-creation-strategy="validate"
                               database-type="mssql"/>
  
  <gear:activiti-process-engine id="processEngine" persistence-management="myPersistenceManagement"/>

3.8.2. BPMN 2.0 Support

The edoras gear Process Engine currently supports the following BPMN 2.0 elements:

  • none start event

  • none end event

  • user task

  • receive task

  • service task

  • script task

  • exclusive gateway

  • sequence flow / conditional sequence flow

  • sub process

  • call activity

Usage of other BPMN 2.0 elements (e.g. boundary events, etc.) is experimental.

Appendix A: Miscellaneous

3.A.1. Query API

Predicate Construction

Most edoras gear services allow to find and count domain objects through a dedicated Query API. For example, the com.edorasware.gear.core.task.TaskService allows to search for com.edorasware.gear.core.task.Tasks by com.edorasware.commons.core.query.Predicate or via configured com.edorasware.gear.core.task.TaskQuery.

Query criteria are expressed in the form of com.edorasware.commons.core.query.Predicates. Domain objects offer pre-defined operand constants which allow to build predicates for the object’s different fields. The following example uses operands available on the com.edorasware.gear.core.task.Task class. Similar constants are available on com.edorasware.gear.core.task.TaskDefinition, com.edorasware.gear.core.timer.Timer, and com.edorasware.gear.core.timer.TimerDefinition.

  Predicate isActive = Task.STATE.isActive();
  Predicate matchesCandidateUserJane = Task.CANDIDATE_USER_IDS.containsAnyOf(UserId.get("jane"));
  Predicate isDueTomorrow = Task.DUE_TIME.eq(tomorrow);

Each operand constant offers comparison methods specific to its value type. This allows to build complex, yet type-safe predicates:

  // range comparisons for date operands
  Predicate isDueThisMonth = Task.DUE_TIME.between(startOfMonth, endOfMonth);
  
  // relative comparisons for numeric operands
  Predicate isHighPriority = Task.PRIORITY.greaterThanOrEq(8);
  
  // wildcard comparisons for string operands
  Predicate matchesWildcardName = Task.NAME.like("Smi*");
  
  // set comparisons via "in" operator
  Set<TaskDefinitionId> definitionIds = ImmutableSet.of(TaskDefinitionId.get("id1"), TaskDefinitionId.get("id2"));
  Predicate matchesDefinitionsIds = Task.DEFINITION_ID.in(definitionIds);
  
  // dynamic comparisons for priority & version operands
  Predicate isLowestPriority = Task.PRIORITY.lowest();
  Predicate isHighestPriority = Task.PRIORITY.highest();
  Predicate isLatestVersion = TaskDefinition.VERSION.latest();
  
  // value aggregation underlying dynamic comparisons can be controlled by inner predicate
  // e.g. "highest value among all version values of definitions whose key equals 'taskDefKey'"
  Predicate predicate = TaskDefinition.VERSION.latest(TaskDefinition.KEY.eq("taskDefKey"));
  
  // type-specific comparisons for variable name/values
  Predicate matchesVariableName = Task.VARIABLE.name().like("na*");
  Predicate matchesVariableStringValue = Task.VARIABLE.stringValue().like("*val*");
  Predicate matchesVariableIntegerValue = Task.VARIABLE.integerValue().lessThan(42);
  Predicate matchesVariableDateValue = Task.VARIABLE.dateValue().after(startOfMonth);
  Predicate matchesVariableIdValue = Task.VARIABLE.idValue().eq(TaskId.get("myTaskId"));

Multiple predicates can be combined through arbitrary AND/OR operators, as well as negated via NOT operator:

  Predicate active = Task.STATE.isActive();
  Predicate assigneeJane = Task.ASSIGNEE_ID.eq(UserId.get("jane"));
  Predicate candidateUserJane = Task.CANDIDATE_USER_IDS.containsAnyOf(UserId.get("jane"));
  Predicate candidateGroupAdmin = Task.CANDIDATE_GROUP_IDS.containsAnyOf(GroupId.get("admin"));
  Predicate variableLastName = Task.VARIABLE.name().eq("lastName");
  
  // fluent API to construct advanced queries via AND/OR combinations of two predicates at a time
  Predicate predicate = active.and(assigneeJane.or(candidateUserJane));
  
  // alternative API to construct advanced queries via AND/OR combinations of any number of predicates
  Predicate otherPredicate = Predicate.and(active, Predicate.or(assigneeJane, candidateUserJane, candidateGroupAdmin), variableLastName);
  
  // fluent API to construct negation via NOT operator
  Predicate isNotCompleted = Task.STATE.isCompleted().not();
  
  // alternative API to construct negation via NOT operator
  Predicate isNotAssignedToJane = Predicate.not(Task.ASSIGNEE_ID.eq(UserId.get("jane")));

All consecutive variable (property) predicates are matched against a single variable entry. To match against different variables of the same entity, the variable (property) predicates can be isolated from each other by using the com.edorasware.commons.core.query.entity.MultipleNamedValuePredicate.

  Predicate matchesVariableOne = Task.VARIABLE.name().eq("lastName").and(Task.VARIABLE.stringValue().like("Smi*"));
  Predicate matchesVariableTwo = Task.VARIABLE.name().eq("count").and(Task.VARIABLE.integerValue().greaterThanOrEq(1));
  Predicate matchesMultipleVariables = MultipleNamedValuePredicate.matchAll(matchesVariableOne, matchesVariableTwo);
  List<Task> tasks = this.taskService.findTasks(matchesMultipleVariables);
Query Construction

In the simplest case, in which a query consists of a predicate exclusively, the predicate can be used directly:

  List<Task> tasks = this.taskService.findTasks(predicate);

Alternatively, a com.edorasware.commons.core.query.Query instance which wraps the configured predicate can be constructed:

  TaskQuery taskQuery = TaskQuery.byPredicate(predicate);
  List<Task> tasks = this.taskService.findTasks(taskQuery);

In order to create a fully configured com.edorasware.commons.core.query.Query instance, the query is built via com.edorasware.commons.core.query.entity.EntityQuery$EntityQueryBuilder and the predicate is set as one configuration aspect.

  List<Task> tasks = this.taskService.findTasks(predicate);

Note that the same builder-pattern not only works for task queries, but can be used to construct instances of all other entity-specific com.edorasware.commons.core.query.Query implementations, e.g. com.edorasware.gear.core.timer.TimerQuery, com.edorasware.gear.core.process.ProcessQuery, or also com.edorasware.gear.core.task.TaskDefinitionQuery instances.

Query Sorting & Pagination

The results of a query can be sorted based on arbitrary criteria. Ordering criteria can be built through the pre-defined operand constants which are available on any domain object:

  // find all tasks, ordered by assignee, last updated first
  List<Ordering> orderingCriteria = Arrays.asList(
          Task.ASSIGNEE_ID.orderAsc(), Task.UPDATE_TIME.orderDesc()
  );
  TaskQuery taskQuery = TaskQuery.builder().sorting(orderingCriteria).build();
  List<Task> sortedTasks = this.taskService.findTasks(taskQuery);

Additionally, the results of any query can be limited in size or set off by a specified amount (also known as "pagination"):

  // query for 50 tasks, from task 100 to task 149
  int PAGING_SIZE = 50;
  TaskQuery taskQuery = TaskQuery.builder().offset(100).limit(PAGING_SIZE).build();
  List<Task> pagedTasks = this.taskService.findTasks(taskQuery);
Query Optimization

As a means of optimization, queries allow for the specification of com.edorasware.commons.core.query.QueryHints. Hinted queries selectively omit loading of certain child entities:

  {
      // omit all identity links & all variables
      Set<QueryHint> taskQueryHints = ImmutableSet.of(TaskQuery.Hint.OMIT_IDENTITY_LINKS, TaskQuery.Hint.OMIT_VARIABLES);
      TaskQuery taskQuery = TaskQuery.builder().hints(taskQueryHints).build();
      Task task = this.taskService.findTask(taskQuery);
  }
  
  {
      // selectively restrict variables
      Predicate predicate = Task.VARIABLE.name().eq("myVariable");
      QueryHint taskQueryHint = TaskQuery.Hint.RESTRICT_VARIABLES.matching(predicate);
      TaskQuery taskQuery = TaskQuery.builder().hints(Collections.singleton(taskQueryHint)).build();
      Task task = this.taskService.findTask(taskQuery);
  }
  
  {
      // omit all properties
      Set<QueryHint> taskDefinitionQueryHints = Collections.singleton(TaskDefinitionQuery.Hint.OMIT_PROPERTIES);
      TaskDefinitionQuery taskDefinitionQuery = TaskDefinitionQuery.builder().hints(taskDefinitionQueryHints).build();
      TaskDefinition taskDefinition = this.taskDefinitionService.findTaskDefinition(taskDefinitionQuery);
  }

3.A.2. Supported Variable Data Types

The name of a variable is always of type java.lang.String. The value of a variable can be one of the following data types:

  • all basic Java data types ( boolean, int, …​)

  • java.lang.String

  • java.util.Date

  • com.edorasware.util.Id

  • java.io.Serializable

  • null

3.A.3. JEE Integration

The edoras gear services can be exposed as EJBs. The following code sample shows the header and a method of an EJB manager class which wraps the edoras gear case service. If you want to expose all edoras gear services as EJBs, you need to create similar EJB manager classes, implement the corresponding service interfaces, and add the same class annotations.

  @Stateless(name = "CaseServiceManager")
  @Interceptors(SpringBeanAutowiringInterceptor.class)
  public class CaseServiceManager implements CaseService {
  
      @Autowired
      private CaseService caseService;
  
      @Override
      public Case findCaseById(CaseId caseId) {
          return this.caseService.findCaseById(caseId);
      }

In order to resolve beans over JNDI in Activiti EL expressions, an EjbELResolver must be registered with the org.activiti.engine.impl.el.ExpressionManager. The EjbELResolver resolves the base part of an EL expression to an EJB that is looked up from the JNDI context by the given name.

  import org.springframework.jndi.JndiTemplate;
  import org.activiti.engine.impl.javax.el.ELContext;
  import org.activiti.engine.impl.javax.el.ELResolver;
  
  import javax.naming.NamingException;
  import java.beans.FeatureDescriptor;
  import java.util.Iterator;
  
  /**
   * This class implements an ELResolver which resolves the base part of an expression to a bean looked up via JNDI.
   */
  @SuppressWarnings("UnusedDeclaration")
  public final class ActivitiEjbELResolver extends ELResolver {
  
      private final JndiTemplate jndiTemplate;
  
      public ActivitiEjbELResolver() {
          this.jndiTemplate = new JndiTemplate();
      }
  
      @Override
      public Object getValue(ELContext context, Object base, Object property) {
          if (base == null) {
              try {
                  Object result = this.jndiTemplate.lookup("java:module/" + property);
                  context.setPropertyResolved(true);
                  return result;
              } catch (NamingException e) {
                  // do nothing
              }
          }
  
          return null;
      }
  
      @Override
      public boolean isReadOnly(ELContext context, Object base, Object property) {
          return true;
      }
  
      @Override
      public void setValue(ELContext context, Object base, Object property, Object value) {
      }
  
      @Override
      public Iterator<FeatureDescriptor> getFeatureDescriptors(ELContext context, Object base) {
          return null;
      }
  
      @Override
      public Class<?> getCommonPropertyType(ELContext context, Object base) {
          return Object.class;
      }
  
      @Override
      public Class<?> getType(ELContext context, Object base, Object property) {
          return Object.class;
      }
  
  }

Appendix B: Sample Application

The edorasware BPM release ships with a sample application. The sample application is a JSF application that demonstrates the usage of the task service and the other services. The sample application also displays the configuration of the edoras gear Task Management and the edoras gear Process Engine.

3.B.1. Project Structure

Some important files of the sample application are:

  • WEB-INF/web.xml: Servlet context configuration file that wires Spring and JSF into the web application.

  • WEB-INF/faces-config.xml: JSF configuration file that registers an ELResolver to resolve Spring bean names used in expressions. It also defines the navigation rules to apply.

  • WEB-INF/application-config.xml: Spring configuration file that declares the various components of edoras gear and their dependencies.

  • index.xhtml: The entry page of the sample application.

Note that all files of the sample application are bundled in the sample/edoras-gear-sample.war.

Appendix C: Developer Guidelines

This appendix provides information and guidelines that developers should consider when using edoras gear.

3.C.1. Code structure

Internal Packages

Classes and interfaces in packages called internal are not part of the public API. These classes provide implementation details and can change in future versions without notice. It is highly recommended to not make use of them.

Support Packages

Classes and interfaces in packages called support provide Service Provider Interfaces (SPI) are used to programmatically extend the behavior and configuration of edoras gear. They are not used to interact with application

3.C.2. Transactions

Transaction Boundaries

All services of edoras gear are run with transaction boundaries PROPAGATION_REQUIRED. This means that if no transaction is available at the time a service method is invoked, a new transaction is spawned based on the settings of the edoras gear Persistence Management component.

3.C.3. Logging

JDBC Prepared Statements

To enable the logging of the prepared statements' sql and of the applied values, set the log level to DEBUG on the com.edorasware.commons.core.persistence.jdbc.JdbcPersistence class. For example, when using log4j, add the following lines to the log4j.properties file:

  log4j.category.com.edorasware.gear.core.db.internal.JdbcPersistence=DEBUG

To enable Spring’s logging of the prepared statements' sql and of the applied values, set the log level to TRACE on the org.springframework.jdbc.core.StatementCreatorUtils and to DEBUG on the org.springframework.jdbc.core.JdbcTemplate class. For example, when using log4j, add the following lines to the log4j.properties file:

  log4j.category.org.springframework.jdbc.core.StatementCreatorUtils=TRACE
  log4j.category.org.springframework.jdbc.core.JdbcTemplate=DEBUG
Java Util Logging (JUL) Rerouting

While all of edoras gear logs through SLF4J, Activiti logs directly to Java Util Logging (JUL). In order to reroute the JUL logs through SLF4J as well, one possibility is to define a bean of type com.edorasware.commons.core.util.logging.JulToSlf4jBridgeHandlerInstaller at the top of the Spring configuration. This bean will install the SLF4J bridge handler for JUL.

  <bean id="julReroute"
  class="com.edorasware.commons.core.util.logging.JulToSlf4jBridgeHandlerInstaller"
  init-method="init"/>;

3.C.4. Build Integration

Maven Dependencies

edoras gear can be integrated into an application project via Maven in two steps.

Define a Maven repository in the distributionManagement element:

  <distributionManagement>
      <repository>
          <id>edorasware.com</id>
          <url>https://repo.edorasware.com/libs-release-public</url>
      </repository>
  </distributionManagement>

Add a dependency to the core module of edoras gear:

<dependency>
<groupId>com.edorasware.bpm</groupId>
<artifactId>edoras-gear-core</artifactId>
<version>1.5.0.S89</version>
</dependency>

3.C.5. Databases

Database Support

edoras gear provides out-of-the-box support for the databases listed under the Persistence Management component.

The database tables for each supported database are either created/updated automatically when starting up edoras gear, or they are created/updated manually by running the respective database scripts. In a typical enterprise scenario, the database scripts to manage the tables are run manually by the DBA. The sections below describe how to run the scripts and how to configure edoras gear to respect the manually created/updated tables.

Manual Database Schema Management

In order to manually manage the database tables used by edoras gear, dedicated scripts are shipped with the application:

  • When starting from an empty database, create scripts allow to set up all database tables from scratch. Please refer to section Creating a database schema from scratch for detailed instructions.

  • For an existing edoras gear database schema, update scripts allow to migrate the tables to the latest application version. Please refer to section Updating an existing database schema to a newer version for detailed instructions.

  • For matter of completeness, drop scripts allow to delete the complete edoras gear database schema. Please refer to section Dropping database tables for detailed instructions.

Creating a database schema from scratch

To create all database tables required by edoras gear from an empty database, run the create scripts bundled in the database/create folder. The scripts must be executed in the following order:

  • activiti.<databaseType>.create.engine.sql

  • activiti.<databaseType>.create.history.sql

  • activiti.<databaseType>.create.identity.sql

  • edw.bpm.<databaseType>.create.primaryKey.sql

  • edw.bpm.<databaseType>.create.domainObjectDefinition.sql

  • edw.bpm.<databaseType>.create.domainObject.sql

  • edw.bpm.<databaseType>.create.timerDefinition.sql

  • edw.bpm.<databaseType>.create.timer.sql

  • edw.bpm.<databaseType>.create.workObjectDefinition.sql

  • edw.bpm.<databaseType>.create.workObject.sql

  • edw.bpm.<databaseType>.create.caseDefinition.sql

  • edw.bpm.<databaseType>.create.case.sql

  • edw.bpm.<databaseType>.create.processDefinition.sql

  • edw.bpm.<databaseType>.create.process.sql

  • edw.bpm.<databaseType>.create.taskDefinition.sql

  • edw.bpm.<databaseType>.create.task.sql

  • edw.bpm.<databaseType>.create.documentDefinition.sql

  • edw.bpm.<databaseType>.create.document.sql

After executing the create scripts, the application must be run with the validate schema creation strategy.

Updating an existing database schema to a newer version

To migrate an existing database schema to a later application version, run the update scripts bundled in the database/update folder. Each script name includes the fromVersion and toVersion of the migration increment it covers. Per increment, the following scripts must be executed in order (if available):

  • activiti.<databaseType>.update.engine.<fromVersion>.to.<toVersion>.sql

  • activiti.<databaseType>.update.history.<fromVersion>.to.<toVersion>.sql

  • activiti.<databaseType>.update.identity.<fromVersion>.to.<toVersion>.sql

  • edw.bpm.<databaseType>.update.<fromVersion>.to.<toVersion>.sql

Application version numbers are included in the MANIFEST file of the shipped application jar file. When migrating across a range of versions, each intermediate increment must be included in the update. For example, to upgrade from version 2.1.0.S14 to version 2.1.0.S21, each increment must be run successively:

  • edw.bpm.<databaseType>.update.S14.to.S15.sql

  • activiti.<databaseType>.update.engine.S15.to.S16.sql

  • activiti.<databaseType>.update.history.S15.to.S16.sql

  • edw.bpm.<databaseType>.update.S15.to.S16.sql

  • etc. …​

  • edw.bpm.<databaseType>.update.S20.to.S21.sql

After executing the update scripts, the application must be run with the validate schema creation strategy.

Dropping database tables

In order to delete the complete edoras gear database schema, drop scripts are bundled in the database/drop folder:

  • activiti.<databaseType>.drop.identity.sql

  • activiti.<databaseType>.drop.history.sql

  • activiti.<databaseType>.drop.engine.sql

  • edw.bpm.<databaseType>.drop.document.sql

  • edw.bpm.<databaseType>.drop.documentDefinition.sql

  • edw.bpm.<databaseType>.drop.task.sql

  • edw.bpm.<databaseType>.drop.taskDefinition.sql

  • edw.bpm.<databaseType>.drop.process.sql

  • edw.bpm.<databaseType>.drop.processDefinition.sql

  • edw.bpm.<databaseType>.drop.case.sql

  • edw.bpm.<databaseType>.drop.caseDefinition.sql

  • edw.bpm.<databaseType>.drop.workObject.sql

  • edw.bpm.<databaseType>.drop.workObjectDefinition.sql

  • edw.bpm.<databaseType>.drop.timer.sql

  • edw.bpm.<databaseType>.drop.timerDefinition.sql

  • edw.bpm.<databaseType>.drop.domainObject.sql

  • edw.bpm.<databaseType>.drop.domainObjectDefinition.sql

  • edw.bpm.<databaseType>.drop.primaryKey.sql

Appendix D: Disclaimer

The edoras gear Process Engine is currently based on the Activiti workflow engine. Direct API calls to the underlying Activiti workflow engine are not officially supported unless explicitly stated otherwise.

4. edoras gear Process Tutorial

4.1. Start Events

StartEvents are used to create a process instance. A process instance start can be invoked by different type of events. (i.e. API call, time reached, message arrived) Start events are always catching. They wait until a certain trigger happens. Good habit is to put StartEvents to the left upper corner of the process definition.

4.1.1. None Start Event

A none start event means that engine can not recognize when this event occurs. None start event is used in the case when we plan to start process instance by process engine API call.

Note:

  • A subprocess always has none start event.

  • A none start event are usually without a name.

NoneStartEventIcon

BPMN Representation of a None Start Event

Real-World Use Case

There are plenty of use cases for none start event. Let’s take the most trivial one:

  • Start process instance with none start event to process invoice, application form.

Sample Use Case

The following diagram illustrates a simple process which uses a none start event:

NoneStartEventSample

XML snippet of the process sample:

  <startEvent id="startNoneEvent"/>

The following JUnit test snippet illustrates how to none start event works:

  ProcessDefinition processDefinition = this.processDefinitionService.findProcessDefinition(ProcessDefinition.KEY.eq(SAMPLE_PROCESS));
  this.processService.startProcess(processDefinition.getId());

In the diagram and snippet above, process instance is started by process engine api call. After the API call, process engine executes process instance till wait state (in this case user task) is not reached.

4.1.2. Message Start Event

Messages start event can be used to trigger process start based on named message. The none start event trigger needs to know process definition. In case of message start event, the only thing needed is to let process engine know, which event has occurred recently. Process engine will decide which process to run. Decision is based on currently deployed process definitions.

Notes:

  • The process definition can specify more than one message start event.

  • Process instance started by message can have input process variables.

  • The message start event name must be unique across all the process definitions. It is not possible to bind 2 processes to one message definition.

  • When new process version is uploaded all message subscriptions of previous version are canceled.

MessageStartEventIcon

BPMN Representation of a Message Start Event

Real-World Use Case

Message start events are useful in case of messaging systems. Messaging system message can be easily transformed into process engine call.

Sample Use Case

The following diagram illustrates a simple process which uses a message start event:

MessageStartEventProcess.bpmn20

XML snippet of the process sample: The message has to be defined first:

  <message id="newInvoice" name="newInvoiceMessage"/>

After that message start event can reference this message:

  <startEvent id="startMessageEvent">
      <messageEventDefinition messageRef="newInvoice"/>
  </startEvent>

The following JUnit test snippet illustrates how to message start event works:

  ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
  RuntimeService runtimeService = processEngine.getRuntimeService();
  runtimeService.startProcessInstanceByMessage("newInvoiceMessage");

4.1.3. Error Start Event

A error start event catches error event and continues in the process execution in the scope where the event was defined.

Notes:

  • Error start event cannot be used for starting a process instance. Execution only continues in the execution of the process instance which throws error.

  • Error start event interrupts current execution and continues in the error sub-process.

ErrorStartEventIcon

BPMN Representation of a Error Start Event

Real-World Use Case

The error start event is used as a logical error. It can use variables from the context in which process instance throwing an error was executed. In the case when an error end event creates event whole execution path is traversed back to find proper error start event, which can catch error according to error code or error reference.

Sample Use Case

The following diagram illustrates a simple process which uses a error start event:

ErrorEventProcess

XML snippet of the process sample: Error definition

  <error id="mainError" errorCode="13"/>

Start event specification

  <startEvent id="catchError">
      <errorEventDefinition errorRef="mainError"/>
  </startEvent>

In the diagram and xml snippets above, error sub-process process is started when main process ends in the end event. It means directly after the start event.

4.1.4. Timer Start Event

A timer start event creates process instance at given time (once or periodically).

Notes:

  • Timer start event is scheduled when process is deployed to repository. There is no need to start process instance explicitly by API call.

  • A subprocess cannot have a timer start event. You can put timer into the parent process, before sub-process call.

  • The consequence of new process version deployment is that previous version timers are deleted.

  • Timer start event does not allow to start process with process variables.

TimerStartEventIcon

BPMN Representation of a Timer Start Event

Real-World Use Case

Uses cases for timer start event:

  • Periodical processes. When we want to execute process periodically (e.g. quarterly, daily) timer start event can be the right choice.

  • Processes scheduled on given time.

Sample Use Case

The following diagram illustrates a simple process which uses a timer start event:

StartProcessbyTimer

XML snippet of the process sample:

  <startEvent id="mystarttimerevent1" isInterrupting="true">
     <timerEventDefinition id="timerEventDefinition1">
        <timeCycle id="sid-5c632c5d-de5e-42f4-aa57-fe2d0c83b957" xsi:type="tFormalExpression">R2/PT10S</timeCycle>
     </timerEventDefinition>
  </startEvent>

The following JUnit test snippet illustrates how to timer start event works.:

  public void startByTimerTwoRepetitionsAfter10Seconds() throws Exception {
      // There is no need to start process. Timers are activated automatically right after deployment
      Assert.assertEquals(0L, countActiveProcesses());
      // We wait 30 seconds as timer precision is OS dependent
      Thread.sleep(30000);
      Assert.assertEquals(2L, countActiveProcesses());
  }

In the diagram and snippet above, process is started twice. The first run is 10 seconds after the deployment and the second occurs 10 seconds after the first one.

4.2. End Events

The end events mean end of the path for process or sub-process. An end event is throwing and always throws a result. Result type depends on end event type. The end event type is declared in the sub-element. Good habit is to put end events to the right bottom corner of the process definition.

4.2.1. None End Event

A none end event’s result is unspecified. In the case of none end event the engine ends current path of execution.

Notes:

  • A none end events are usually without any specific name.

Graphical representation of none end event:

NoneEndEventIcon

BPMN Representation of a None End Event

Real-World Use Case

None end event is used in the cases when no result from process end is needed. In the most cases when process execution was finished without any exceptional state none end event is used for its end.

Sample Use Case

The following diagram illustrates a simple process which uses a none end event together with none start event:

NoneStartEventSample

XML snippet of the process sample:

  <endEvent id="endNoneEvent" name="An ordinary end"/>

In the diagram and snippet above, process instance is started by process engine api call. An user task is created. After the user task complete, a process engine executes none end event node and ends the process instance.

4.2.2. Error End Event

An error end event ends current path of execution and throws an error. The error can be caught by intermediate boundary error event or error start event. Process instance execution continues in the scope where the catching event was defined.

Note:

  • In case when error is thrown and no catching error event is found, an exception is thrown.

Graphical representation of error end event:

ErrorEndEventIcon

BPMN Representation of a Error End Event

Real-World Use Case

The error end event is used to throw the logical error. The logical error has to be handled by process instance.

Sample Use Case

The following diagram illustrates a simple process which uses a error end event:

ErrorEventProcess

XML snippet of the process sample: Error definition

  <error id="mainError" errorCode="13"/>

End event specification

  <endEvent id="theEnd">
      <errorEventDefinition errorRef="mainError"/>
  </endEvent>

If the errorRef does not match any defined error, then the errorRef is used as a shortcut for the errorCode.

4.3. Intermediate Catching Events

An intermediate catching event is used to wait in process execution on a specific event. The event can occur externally and process engine runtime is informed about it by API call. Possible events which are caught:

  • message event

  • timer event

  • signal event

4.3.1. Message Intermediate Catching Event

BPMN Feature

Message Intermediate Catching Events are used to model wait state for particular message event with a specified name. After message catching process instance continues in its execution. An event message is dispatched by API call.

MessageCatchEventIcon

BPMN Representation of a Message Intermediate Catching Event

Message Intermediate Catching Event style rules:

  • By convention, message intermediate catching events are named after the event they are waiting for. (e.g. "Additional data received")

Real-World Use Case

Use case for message intermediate catching event:

  • Let’s imagine process which execution depends on external resources which are not assigned to the process. To be more specific, let’s ask client for additional information about his income in the loan approval process. A client is asked by automatically generated e-mail. The next step should wait on the message from the client.

Sample Use Case

The following diagram illustrates a simple process which uses a message intermediate catching event:

MessageCatchingEventProcess.bpmn20

XML snippet of the process sample:

  <message id="additionalAppInfoReceivedMessage" name="infoReceived"/>
  <intermediateCatchEvent id="AdditionalInfoReceived" name="Additional information received">
      <messageEventDefinition messageRef="additionalAppInfoReceivedMessage"/>
  </intermediateCatchEvent>

In the diagram above, a client applies for a loan. After the user finishes user task, e-mail is sent automatically. After that, process instance has to wait on client’s response with additional data. When response arrives, message is send to the process engine through API.

The following JUnit test snippet illustrates how message can be sent to the process instance which is waiting for this message.

  ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
  RuntimeService runtimeService = processEngine.getRuntimeService();
  Execution execution = runtimeService.createExecutionQuery()
          .messageEventSubscriptionName("infoReceived")
          .singleResult();

In the case when there are several processes running our query could be more specific. We can distinguish between process instances according to their variables.

  execution = runtimeService.createExecutionQuery()
          .messageEventSubscriptionName("infoReceived")
          .processVariableValueEquals("identification", "IdCode")
          .singleResult();

And finally message event can be sent.

  runtimeService.messageEventReceived("infoReceived", execution.getId());

4.3.2. Timer Intermediate Catching Event

BPMN Feature

Timer Intermediate Catching Events are used to model wait state driven by a time.

TimerCatchEventIcon

BPMN Representation of a Timer Intermediate Catching Event

Timer Intermediate Catching Event style rules:

  • By convention, timer intermediate catching events are named after the event they are waiting for. (e.g. "Delivery deadline reached")

Real-World Use Case

Use case for timer intermediate catching event:

  • Continue in the process instance execution when time was reached.

Sample Use Case

The following diagram illustrates a simple process which uses a timer intermediate catching event:

TimerCatchingEventProcess

XML snippet of the process sample:

  <intermediateCatchEvent id="intermediateTimerEvent" name="Wait&#10;on the answer">
     <timerEventDefinition id="timerEventDefinition1">
        <timeDuration id="formalExpression" xsi:type="tFormalExpression">PT5S</timeDuration>
     </timerEventDefinition>
  </intermediateCatchEvent>

In the diagram above, timer waits until given period of time expires and continues in the process instance evaluation.

The following JUnit test snippet illustrates how timer intermediate catching event works

  startProcessByKey("timerCatchingEventProcess");
  
  Task activeTask = getActiveTask();
  assertEquals("Send a complain", activeTask.getName());
  this.taskService.completeTask(activeTask.getId(), NO_DESCRIPTION);
  // timer is waiting
  // there should be no active user task
  activeTask = getActiveTask();
  assertNull(activeTask);
  
  // wait on timer
  Thread.sleep(10000);
  
  activeTask = getActiveTask();
  assertEquals("Check the answer", activeTask.getName());
  this.taskService.completeTask(activeTask.getId(), NO_DESCRIPTION);

jUnit starts the process and complete the first user task. After that the test waits for 10 seconds in which timer event is fired and process instance continues in its execution. At the end the last user task from the process is executed.

4.3.3. Signal Intermediate Catching Event

BPMN Feature

Signal Intermediate Catching Events are used to model wait for particular signal. After catching the signal the process execution continues. The signal is not consumed after the catching. One signal can fire execution of several independent process instances in one step.

SignalCatchEventIcon

BPMN Representation of a Signal Intermediate Catching Event

Signal Intermediate Catching Event style rules:

  • By convention, signal intermediate catching events are named after the event they are waiting for. (e.g. "New customer arrived")

Real-World Use Case

Use case for signal intermediate catching event:

  • Signal event can trigger several process instances to continue.

Sample Use Case

The following diagram illustrates a simple process which uses a signal intermediate catching event:

SignalCatchingEventProcess.bpmn20

XML snippet of the process sample:

  <signal id="newCustomerArrived" name="New Customer Arrived"/>
  <intermediateCatchEvent id="carParkingCatchNewCustomer">
      <signalEventDefinition signalRef="newCustomerArrived"/>
  </intermediateCatchEvent>

In the diagram above, new customer comes to the shop. This is the signal for waiting process instances to park his car and open him a door.

The following JUnit test snippet illustrates how signal can be sent to the process engine. Another possibility is to throw signal from process definition.

  startProcessByKey("carParking");
  startProcessByKey("doorOpenning");
  
  List<Task> activeTasks = getActiveTasks();
  assertEquals(2, activeTasks.size());
  completeAllTasks(activeTasks);
  
  // process instances are waiting on the signal
  ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
  RuntimeService runtimeService = processEngine.getRuntimeService();
  runtimeService.signalEventReceived("New Customer Arrived");
  
  activeTasks = getActiveTasks();
  assertEquals(2, activeTasks.size());
  completeAllTasks(activeTasks);

In the first step two process instances are started. Internal staff has completed tasks, they are prepared to open the door and to park the cars and process instances are waiting for new customer arrival. After that signal is sent. Both independent process instances continues in their execution.

4.4. Gateways

A gateway is used to route the flow (token) of the process execution. On the basis of the different gateway types (represented by a diamond shape with different icons) it is possible to generate tokens (i.e. fork a process) or consume generated tokens (i.e. join multiple processes).

4.4.1. Exclusive Gateways (XOR)

BPMN Feature

Exclusive gateways (XOR) are used to model alternative paths in a process. For each path, one logical expression must be defined. Only one of the defined paths can be taken (hence the predicate "exclusive"). If multiple conditions apply, the first path defined in the XML file will be taken. A XOR gateway can have an arbitrary number of outgoing paths.

xor

BPMN Representation of an Exclusive Gateway (XOR)

Exclusive gateway style rules:

  • By convention, XOR gateways are named after the question they represent (e.g. "Order Permitted?"). As a consequence of the XOR logic, the possible answers exclude each other.

  • By convention, the sequence flows after a XOR gateway are named after the conditions they represent (e.g. "Yes", "No"). For each possible sequence flow (condition) one outgoing path is necessary.

  • Model a task before a XOR gateway which delivers the reason for a decision (e.g. "Permit Order Form").

  • XOR gateways should not be used to merge alternative paths, unless into another gateway. Alternatively, the sequence flows should be connected directly. [BPMN Method and Style, Bruce Silver]

  • Sub-processes followed by a XOR gateway should have two end states, one matching the name of the followed gateway. [BPMN Method and Style, Bruce Silver]

Real-World Use Case

Uses cases for exclusive gateways:

  • Sequential execution on the basis of decisions (particular tasks should be executed only in particular cases).

Sample Use Case

The following diagram illustrates a simple process which uses an exclusive gateway:

ExclusiveGatewaySample

XML snippet of the process sample:

  <exclusiveGateway gatewayDirection="Diverging" id="data-basedExclusiveXORGateway1" name="Order Permitted?"/>
  <sequenceFlow id="sequenceFlow1" name="Yes" sourceRef="data-basedExclusiveXORGateway1" targetRef="endEvent1">
      <conditionExpression xsi:type="tFormalExpression">#{orderPermitted}</conditionExpression>
  </sequenceFlow>
  <sequenceFlow id="sequenceFlow2" name="No" sourceRef="data-basedExclusiveXORGateway1" targetRef="serviceTask1">
      <conditionExpression xsi:type="tFormalExpression">#{!orderPermitted}</conditionExpression>
  </sequenceFlow>

In the diagram above, the upper path represents the 'Yes' decision, while the bottom path represents the 'No' decision. The gateway is modelled in a way that the order is only executed when it was permitted.

The following JUnit test snippet illustrates the 'Yes' path, where the order will be permitted because the corresponding process variable orderPermitted evaluates to true:

  List<Task> allTasks = this.taskService.findTasks(predicate);
  assertEquals(1, allTasks.size());
  
  Task task = allTasks.get(0);
  assertEquals("Permit Order Form", task.getName());
  this.taskService.setAssignedUser(task.getId(), UserId.get("user"), NO_DESCRIPTION);
  
  // set process variable 'orderPermitted' to value 'true'
  this.processService.putVariable(task.getParentProcessId(), "orderPermitted", true, NO_DESCRIPTION);
  this.taskService.completeTask(task.getId(), NO_DESCRIPTION);
  
  Mockito.verify(this.mailService, Mockito.times(0)).sendCancelMail();

On the other hand, the 'No' path is executed if orderPermitted evaluates to false:

  List<Task> allTasks = this.taskService.findTasks(predicate);
  assertEquals(1, allTasks.size());
  
  Task task = allTasks.get(0);
  assertEquals("Permit Order Form", task.getName());
  this.taskService.setAssignedUser(task.getId(), UserId.get("user"), NO_DESCRIPTION);
  
  // set process variable 'orderPermitted' to value 'false'
  this.processService.putVariable(task.getParentProcessId(), "orderPermitted", false, NO_DESCRIPTION);
  this.taskService.completeTask(task.getId(), NO_DESCRIPTION);
  
  Mockito.verify(this.mailService, Mockito.times(1)).sendCancelMail();

4.4.2. Parallel Gateways (AND)

BPMN Feature

Parallel gateways (AND) are used to model concurrent execution of activities. They can represent both, the forking of a single activity into multiple paths, as well as joining multiple paths back to a single activity. Even though AND gateways logically model parallel processes, their actual execution does not necessary happen concurrently. Activiti explicitly follows a non-concurrent execution strategy, i.e. multiple paths are executed sequentially (with no promises about execution order).

and

BPMN Representation of a Parallel Gateway (AND)

Parallel gateway style rules:

  • AND gateways are usually unnamed.

  • Sequence flows after AND gateways are usually unnamed.

  • A parallel join into an activity always requires one AND gateway. A process instance is only completed when all created tokens are consumed.

  • AND gateways should not be used to join parallel paths into 'None' end events, since such events already imply a join operation. [BPMN Method and Style, Bruce Silver]

Real-World Use Case

Use cases for parallel gateways:

  • Concurrent execution without decisions (fork, sequence of execution does not matter).

  • Used to model parallel joins, wait for all incoming sequences (join, mind deadlocks).

Sample Use Case

The following diagram illustrates a simple process which uses a parallel gateway:

ParallelGatewaySample

XML snippet of the process sample:

  <parallelGateway gatewayDirection="Diverging" id="parallelGateway1"/>
  <parallelGateway gatewayDirection="Converging" id="parallelGateway2"/>
  <sequenceFlow id="sequenceFlow1" sourceRef="parallelGateway1" targetRef="userTask1"/>
  <sequenceFlow id="sequenceFlow2" sourceRef="parallelGateway1" targetRef="userTask2"/>

The following JUnit test snippet illustrates how the Send Order and Enter Permit Mail tasks are executed in parallel:

  // 2 parallel tasks
  List<Task> allTasks = this.taskService.findTasks(taskQuery);
  assertEquals(2, allTasks.size());
  
  Task task = allTasks.get(0);
  assertEquals("Enter Permit Mail", task.getName());
  this.taskService.setAssignedUser(task.getId(), UserId.get("user"), NO_DESCRIPTION);
  this.taskService.completeTask(task.getId(), NO_DESCRIPTION);
  
  task = allTasks.get(1);
  assertEquals("Send Order", task.getName());
  this.taskService.setAssignedUser(task.getId(), UserId.get("user"), NO_DESCRIPTION);
  this.taskService.completeTask(task.getId(), NO_DESCRIPTION);
  
  Mockito.verify(this.mailService, Mockito.times(1)).sendPermitMail();

4.4.3. Inclusive Gateways (OR)

BPMN Feature

Inclusive gateways (OR) are used to model a combination of an exclusive and a parallel gateway. They allow handling of parallel operations on the basis of conditions.

or

BPMN Representation of an Inclusive Gateway (OR)

Inclusive gateway style rules:

  • Similar to XOR gateways, OR gateways can be named after the question they represent (e.g. "Order Permitted?").

  • The sequence flows after an OR gateway can be named after the conditions they represent (e.g. "Yes", "No"). Note that they are not necessarily exclusive.

  • Use a standard flow when your conditions are complex (beyond simple "Yes"/"No") to avoid deadlock.

  • OR gateways are used to join unconditional parallel splits. That means the OR gateways ignores the death paths, they wait only for the incoming sequence flows which were enabled in this instance (used to avoid deadlocks).

Real-World Use Case

Use cases for inclusive gateways:

  • Concurrent execution on the basis of decisions (fork).

  • Wait for all incoming sequences which are enabled in this process instance (join).

Sample Use Case

The following diagram illustrates a simple process which uses an inclusive gateway:

InclusiveGatewaySample

XML snippet of the process sample:

  <inclusiveGateway gatewayDirection="Diverging" id="inclusiveGateway1" name="Order Permitted?"/>
  <inclusiveGateway gatewayDirection="Converging" id="inclusiveGateway2"/>
  <sequenceFlow id="sequenceFlow1" name="Yes" sourceRef="inclusiveGateway1" targetRef="userTask1">
      <conditionExpression xsi:type="tFormalExpression">#{orderPermitted}</conditionExpression>
  </sequenceFlow>
  <sequenceFlow id="sequenceFlow2" name="Yes" sourceRef="inclusiveGateway1" targetRef="userTask2">
      <conditionExpression xsi:type="tFormalExpression">#{orderPermitted}</conditionExpression>
  </sequenceFlow>
  <sequenceFlow id="sequenceFlow3" name="No" sourceRef="inclusiveGateway1" targetRef="serviceTask1">
      <conditionExpression xsi:type="tFormalExpression">#{!orderPermitted}</conditionExpression>
  </sequenceFlow>

The following JUnit test snippet illustrates how the order will be permitted if the process variable orderPermitted evaluates to true. The gateway routes to the 'Yes' path, in which case the Send Order and Enter Permit Mail tasks are executed in parallel:

  // set process variable 'orderPermitted' to value 'true'
  Map<String, Object> initialVariables = ImmutableMap.<String, Object>of("orderPermitted", true);
  ProcessId processId = ProcessServiceUtils.startProcessForLatestVersionOfProcessDefinitionWithKey("inclusiveGatewaySample", initialVariables,
          this.processDefinitionService, this.processService);
  
  Predicate predicate = Predicate.and(Task.STATE.isActive(), Task.HIERARCHY.childOf(processId));
  TaskQuery taskQuery = TaskQuery.builder().predicate(predicate).sorting(Task.NAME.orderAsc()).build();
  
  // 2 parallel tasks
  List<Task> allTasks = this.taskService.findTasks(taskQuery);
  assertEquals(2, allTasks.size());
  
  Task task = allTasks.get(0);
  assertEquals("Enter Permit Mail", task.getName());
  this.taskService.setAssignedUser(task.getId(), UserId.get("user"), NO_DESCRIPTION);
  this.taskService.completeTask(task.getId(), NO_DESCRIPTION);
  
  task = allTasks.get(1);
  assertEquals("Send Order", task.getName());
  this.taskService.setAssignedUser(task.getId(), UserId.get("user"), NO_DESCRIPTION);
  this.taskService.completeTask(task.getId(), NO_DESCRIPTION);
  
  Mockito.verify(this.mailService, Mockito.times(0)).sendCancelMail();
  Mockito.verify(this.mailService, Mockito.times(1)).sendPermitMail();

If the process variable orderPermitted evaluates to false, the 'No' path will be executed:

  // set process variable 'orderPermitted' to value 'false'
  Map<String, Object> initialVariables = ImmutableMap.<String, Object>of("orderPermitted", false);
  ProcessId processId = ProcessServiceUtils.startProcessForLatestVersionOfProcessDefinitionWithKey("inclusiveGatewaySample", initialVariables,
          this.processDefinitionService, this.processService);
  
  Predicate predicate = Predicate.and(Task.STATE.isActive(), Task.HIERARCHY.childOf(processId));
  
  List<Task> allTasks = this.taskService.findTasks(predicate);
  assertEquals(0, allTasks.size());
  
  Mockito.verify(this.mailService, Mockito.times(1)).sendCancelMail();
  Mockito.verify(this.mailService, Mockito.times(0)).sendPermitMail();

5. edoras gear - FAQs

5.1. How do I start the latest version of a process?

If there is a process with key myProcess that is deployed in the process engine, the process definition can be retrieved from the process definition service using a query with the appropriate key and lastVersion predicates set. With the returned process definition id, the process service can then be called to start the process.

  // Query for the latest version of myProcess
  ProcessId processId = ProcessServiceUtils.startProcessForLatestVersionOfProcessDefinitionWithKey("myProcess",
          this.processDefinitionService, this.processService);
Tip

You find the source code of this example in the class com.edorasware.gear.documentation.faq.StartLatestVersionOfProcessWithId.

5.2. Which variables can I retrieve from a task instance?

The following variables can be retrieved from a task instance:

  • the variables that were available in the process-level conversation at the time the task was created, and

  • the variables that were defined in the conversation metadata, and

  • the variables that you add to the task during the lifetime of the task.

The example below applies the following conversation metadata which defines a conversation variable called conversation-customerId:

The following code example shows that the method Task#getVariables() always returns the variables that were available in the process-level conversation at the time the task was created - even if the task object is retrieved again between updates of the variables of the process-level conversation.

In order to get the latest state of the variables in the process-level conversation, the process service can be queried.

Tip

You find the source code of this example in the class com.edorasware.gear.documentation.faq.RetrieveVariables.

5.3. Can I store task-local variables?

Yes, variables can be stored in the task-level conversation. For an example, see How to retrieve variables from a task instance.

5.4. Can I store any Serializable object as the value of a variable?

Yes, but using serialization for variable persistence is not recommended.

Java serialization is rather slow. Moreover, it is very error prone with respect to class versions. Reading a serialized object back into the application may fail with an java.io.InvalidClassException if the implementation of the class has changed since the time of persistence.

If you are having trouble reading serialized variables back in, consider setting an explicit serialVersionUID and/or overriding the readObject method in the corresponding class (see Discover the secrets of the Java Serialization API ).

Instead of using serialization, we recommend generating a string representation of a class in order to persist its state. XML or JSON are good candidates which can easily be written and read in a safe manner. If necessary, both can be tailored to cope with complex class evolution scenarios.

See the FAQ entry on how to configure a custom converter to deal with custom marshalling/unmarshalling of variable values.

5.5. Can I configure a custom converter for variable values?

Yes, you can provide your own converters by registering a custom converter provider with the persistence management component.

In the following example, we define a custom converter provider com.example.MyConverterProvider and include it in the persistence-management configuration:

  <gear:persistence-management id="persistenceManagement"
                               database-schema-creation-strategy="create-drop"
                               converter-provider="myConverterProvider"/>
  
  <bean id="myConverterProvider" class="com.edorasware.gear.documentation.MyConverterProvider"/>

The custom converter provider com.example.MyConverterProvider is implemented as following:

  public final class MyConverterProvider implements ConverterProvider {
  
      @Override
      public ImmutableList<? extends ValueConverter<?, ?>> getConverters(ConverterType converterType) {
          if (CommonsEntityConverterType.VARIABLE == converterType) {
              return ImmutableList.of(CUSTOM);
          }
  
          return ImmutableList.of();
      }
  
      private static final ValueConverter<String, String> CUSTOM = new BaseValueConverter<String, String>("CUSTOM", String.class, String.class) {
  
          @Override
          protected String convertToTargetTypeNullSafe(Object value) {
              return "<custom>" + value + "</custom>";
          }
  
          @Override
          protected String convertToSourceTypeNullSafe(Object value) {
              return ((String) value).replaceAll("<custom>", "").replaceAll("</custom>", "");
          }
  
      };
  
  }

In the previous implementation, we are providing the CUSTOM VariableType converter. The CUSTOM VariableType converter handles String variable values and persists them as String values that are surrounded with a custom tag.

You can use the above pattern to define more meaningful implementations, for instance, you could convert a DTO stored in a variable into XML and persist the XML as a String. When the variable is read from the persistent store, the XML can be converted back to a DTO.

5.6. How can I plug in a custom conversation metadata lookup strategy?

To plug in a custom conversation metadata lookup strategy, the two interfaces ConversationMetadataContextBasedLookupStrategy and ConversationMetadataContextBasedLookupStrategyFactory need to be implemented and registered with the task conversation configuration.

The following example shows a custom lookup strategy that maps every task to a fixed conversation id globalConversationId.

Finally, register the custom lookup strategy with the task conversation configuration of the task management component.

Tip

You find the source code of this example in the class com.edorasware.gear.documentation.faq.GlobalConversationId.

5.7. How to define a conversation metadata lookup strategy that supports task-specific conversation metadata mappings and uses process-specific mappings as a fallback?

The code example below shows a conversation metadata lookup strategy that

  • first looks up the conversation metadata by the name of the given task

  • if nothing is found, looks up the conversation metadata by the name of the process to which the given task belongs.

Tip

You find the source code of this example in the class com.edorasware.gear.documentation.faq.StrategyForTaskOrProcess.

5.8. How can I configure the database aspects?

Please refer to the user guide, section Database Configuration.

5.9. How can I force a process to wait until all of its ad-hoc tasks have been completed?

You can use a receive task and send it a signal when an ad-hoc task has been completed. Please refer to the user guide, section Process Messages, for details on how to send a message to a process or receive task. Since a receive task completes when it receives a message, this behavior can be used to create a loop at the end of a process to prevent it from finishing until all ad-hoc tasks have been completed. The following diagram shows an example process with such a loop at the end. As long as ad-hoc tasks are running, the process will loop back to the receive task. Every time an ad-hoc task has been completed, a message needs to be sent to the receive task or its process. The following example demonstrates this behavior.

WaitForAdHocTasks

Visualization of the process that waits for running ad-hoc tasks to complete.

  @Test
  public void run() {
      ProcessId processId = startProcess("waitForAllAdHocTasksFinishedProcess");
      assertProcessRunningWithTaskCount(processId, 1);
  
      addAdHocTask("ad-hoc task 1", processId, AdHocTaskService.AD_HOC_TASK_PROVIDER_ID);
      addAdHocTask("ad-hoc task 2", processId, AdHocTaskService.AD_HOC_TASK_PROVIDER_ID);
      assertProcessRunningWithTaskCount(processId, 3);
  
      claimAndCompleteTask("task");
      assertProcessRunningWithTaskCount(processId, 2);
  
      claimAndCompleteTask("ad-hoc task 1");
      assertProcessRunningWithTaskCount(processId, 1);
  
      claimAndCompleteTask("ad-hoc task 2");
      assertProcessFinished(processId);
  }
  
  private ProcessId startProcess(String processDefinitionKey) {
      return ProcessServiceUtils.startProcessForLatestVersionOfProcessDefinitionWithKey(processDefinitionKey,
              this.processDefinitionService, this.processService);
  }
  
  private void addAdHocTask(String name, ProcessId processId, TaskProviderId adHocTaskProvider) {
      Task adHocTask = Task.builder().name(name).providerId(adHocTaskProvider).state(WorkObjectState.ACTIVE).build();
      this.taskService.addTask(adHocTask, processId, null);
  }
  
  private void claimAndCompleteTask(String taskName) {
      Predicate isActive = Task.STATE.isActive();
      Predicate matchesName = Task.NAME.eq(taskName);
      Task task = this.taskService.findTask(isActive.and(matchesName));
  
      this.taskService.setAssignedUser(task.getId(), UserId.get("developer"), null);
      this.taskService.completeTask(task.getId(), null);
  
      if (task.getProviderId().equals(AdHocTaskService.AD_HOC_TASK_PROVIDER_ID)) {
          this.processService.sendMessage(task.getParentProcessId(), Collections.singletonList(AD_HOC_TASK_LISTENER_ID), null, null);
      }
  }
  
  private void assertProcessRunningWithTaskCount(ProcessId processId, long taskCount) {
      Process process = this.processService.findProcessById(processId);
      assertNotNull(process);
  
      Predicate predicate = Predicate.and(
              Task.STATE.isActive(),
              Task.HIERARCHY.childOf(processId)
      );
  
      long numberOfActiveTasks = this.taskService.countTasks(predicate);
      assertEquals(taskCount, numberOfActiveTasks);
  }
  
  private void assertProcessFinished(ProcessId processId) {
      Process process = this.processService.findProcessById(processId);
      assertEquals(WorkObjectState.COMPLETED, process.getState());
  }
Tip

You find the source code of this example in the class com.edorasware.gear.documentation.faq.WaitForAdHocTasks.

5.10. How can I create arbitrary todo tasks and trigger a reminder event when the todo task reaches a certain date/time?

Until edoras gear provides a specific component to deal with due dates, you can set up a standard BPMN 2.0 process with a single task that has an intermediate timer attached.

TodoWithReminder

Visualization of the process that creates a todo task and will trigger an event if the todo task is not completed by a certain due date.

  // Start the process with process variables that define:
  //  - the description of the item is to clean windows
  //  - the task's intermediate timer should fire 5 seconds after the task has become active
  Map<String, Object> initialVariables = ImmutableMap.<String, Object>of(
          "todoItem", "Clean Windows",
          "reminderDate", ISO_8601_DATE_FORMAT.format(new Date(System.currentTimeMillis() + 5 * 1000)));
  ProcessId processId = ProcessServiceUtils.startProcessForLatestVersionOfProcessDefinitionWithKey("todoWithReminderProcess", initialVariables,
          this.processDefinitionService, this.processService);
  
  // once the process is started, the task is available to the user
  Predicate predicate = Predicate.and(Task.STATE.isActive(), Task.HIERARCHY.childOf(processId));
  Task initialTask = this.taskService.findTask(predicate);
  assertNotNull(initialTask);
  
  // don't complete the current task within 5 seconds, this triggers the task's intermediate timer to fire an event
  waitForTaskToExceedDueDate(initialTask.getId());
  
  // by now, the timer has fired an event and the initial task has been interrupted
  predicate = Predicate.and(Task.ID.eq(initialTask.getId()), Task.STATE.isInterrupted());
  Task interruptedTask = this.taskService.findTask(predicate);
  assertNotNull(interruptedTask);
  
  // also, a new task has been created with the updated reminder date
  predicate = Predicate.and(Task.STATE.isActive(), Task.HIERARCHY.childOf(processId));
  Task newTask = this.taskService.findTask(predicate);
  assertNotNull(newTask);
Tip

You find the source code of this example in the class com.edorasware.gear.documentation.faq.TodoWithReminder.

5.11. How can I write unit tests that apply customized versions of the DDLs that ship with edoras gear?

For unit tests, we recommend using an in-memory database like H2 since you typically get the best execution performance and the lowest setup overhead. But, the following recipe also applies to any other database configuration.

  • Make sure the empty database has been created and the appropriate permissions have been set. In the case of H2, this step is not needed.

  • In the setup of each test, modify the created edoras gear default database schema to your needs by executing the appropriate DDL statements.

  • Run your test.

  • In the teardown of each test, revert your modifications.

Make sure you have the database schema creation strategy set to 'create-drop' or 'update-drop' in the persistence-management configuration.

In the following example, before each test run, the tables defined in the custom DDL file are created. These custom tables are dropped again at the end of each test run:

  public class CustomDatabaseSchemaDefinition {
  
      @Autowired
      private PersistenceManagementConfiguration persistenceManagementConfiguration;
  
      @Before
      public void customizeDatabase() {
          executeDdlStatements(PersistenceUtils.DDL_CREATE_OPERATION);
      }
  
      @After
      public void revertCustomization() {
          executeDdlStatements(PersistenceUtils.DDL_DROP_OPERATION);
      }
  
      @Test
      public void run() {
          DataSource dataSource = this.persistenceManagementConfiguration.getDataSource();
  
          assertTrue("activiti tables have been created", DbUtils.containsTable("ACT_RU_TASK", dataSource, null, null));
          assertTrue("custom tables have been created", DbUtils.containsTable("CUSTOM_TABLE", dataSource, null, null));
      }
  
      private void executeDdlStatements(String operation) {
          DdlHandler ddlHandler = this.persistenceManagementConfiguration.getDdlHandler();
          String customDdlResource = "com/edorasware/gear/documentation/faq/" + "CustomDatabaseSchemaDefinition." + operation + ".sql";
          ddlHandler.executeStatements(IOUtils.createInputStreamReader(IOUtils.getResourceAsStream(customDdlResource), "UTF-8"));
      }
  
  }
Tip

You can find the source code of this example in the class com.edorasware.gear.documentation.faq.CustomDatabaseSchemaDefinition.

5.12. How can I store and read AnyWorkObjects without losing the information on the concrete type of the ids and of the path ids?

Let’s assume you have a custom Note work object that you want to persist through the AnyWorkObjectService. You want to persist it in a way such that you can read it again as an AnyWorkObject and still have NoteId as the concrete type of the id of the work object.

First, we define a new entity type and a new id type.

  public static final class Note {
  
      public static final Type ENTITY_TYPE = Type.getInstance("NOTE");
  
  }
  
  public static final class NoteId extends WorkObjectId {
  
      private static final long serialVersionUID = 53;
  
      public static final NoteId UNDEFINED = get(UNDEFINED_VALUE);
  
      private NoteId(String value) {
          super(value);
      }
  
      @Override
      public WorkObjectId withValue(String value) {
          return get(value);
      }
  
      public static NoteId get(String value) {
          return new NoteId(value);
      }
  
  }

We can then create a custom converter for our new id type NoteId.

  public static final class NoteIdAwareIdConverter {
  
      private NoteIdAwareIdConverter() {
      }
  
      public static final IdConverter ID;
  
      static {
          ID = new IdConverter(ImmutableMap.<Class<? extends Id>, Type>builder().
                  put(NoteId.class, Note.ENTITY_TYPE).build());
      }
  
  }

Finally, we create a custom converter provider that registers our new provider.

  public static final class NoteIdAwareConverterProvider implements ConverterProvider {
  
      @Override
      public ImmutableList<? extends ValueConverter<?, ?>> getConverters(ConverterType converterType) {
          if (CommonsEntityConverterType.ID == converterType) {
              return ImmutableList.<ValueConverter<?, ?>>of(NoteIdAwareIdConverter.ID);
          }
  
          if (CommonsEntityConverterType.VARIABLE == converterType) {
              return ImmutableList.<ValueConverter<?, ?>>of(NoteIdAwareIdConverter.ID);
          }
  
          return ImmutableList.of();
      }
  
  }

Once all the classes are ready, we register our custom converter provider with the persistence management component.

  <gear:persistence-management id="persistenceManagement"
                               database-schema-creation-strategy="create-drop"
                               converter-provider="customConverterProvider"/>
  
  <bean id="customConverterProvider" class="com.edorasware.commons.core.persistence.CompositeConverterProvider">
      <constructor-arg>
          <list>
              <bean class="com.edorasware.gear.documentation.faq.CustomEntityConverter$NoteIdAwareConverterProvider"/>
              <bean class="com.edorasware.gear.core.persistence.GearEntityConverterProvider"/>
          </list>
      </constructor-arg>
  </bean>
Tip

You find the source code of this example in the class com.edorasware.gear.documentation.faq.CustomEntityConverter.