1. edoras one Overview

1.1. What is edoras one?

edoras one comprises a new generation of integrated platform that combines business process management, case management and content management simply and effectively. It makes work processes much more economical. Running in the background, the platform guarantees quick access to whatever content is relevant for your company’s tasks. To achieve this, edoras one doesn’t rely on a rigid process structure. Instead, it’s based on "design by doing"; each individual user decides how much structure he actually needs and determines for himself which work fragment will help him achieve his task. All process steps have grown organically and, as such, are part of natural productivity. That means your company stays well organized in an unstructured world and sustains the dynamics necessary for success.

1.2. What is the main concept behind edoras one?

edoras one organizes everything you do around the concept of work items. No matter whether you create a single task, start a new case or process, or upload a document, everything is a work item and supports a common set of functionality like ownership and assignment, due date and priority. Forms are used to manage the data associated with work items. Work items can have relations to each other and support hierarchies. The behavior of a work item is driven by its model, which is called a definition. For instance a process is driven by its process definition which is modeled using the modeling standard BPMN 2. These models can be designed and configured within the modeler dashboard of edoras one. An edoras one App contains all types of models and configurations to support a specific aspect or area of your organization. An App can be deployed and all of its models become available to drive the behavior of your work items.

1.3. What is the concept behind "cases"?

The work item type called "case" is key in edoras one as it acts as the main context around a collection of work items and information. A case can represent many things depending on how you define it. Think of a case like a box with a label where you can find everything that you need to work on a particular task. There are small and large boxes, square and round ones, boxes with one compartment only or boxes with a lot of compartments to organize the contents. For example if you handle customer complaints with edoras one, a single "complaint" could become a "case" and everything that happens while solving that complaint can be stored in and made available in that case. Or if you use edoras one for your recruiting, each job applicant is represented by a "case" where all structured data like name, birthday etc. for the applicant is part of the case form and documents like CV and references are simply stored within the case as attachments. Another great fit for edoras one is request and approval handling where a vacation request, a travel request or even a request for a quote from a customer becomes a case, supporting the handling and approval process through edoras one.

1.4. What can I use edoras one for?

The best matches for edoras one are all types of processes where people are involved working and collaborating together, on-site or distributed, with internal colleagues and even external participants like customers or service providers - wherever data has to be collected, approved, discussed and solutions have to be found using a process style that is a combination of structured and unstructured, ad-hoc work. Let’s have a look at some examples.

1.4.1. Approval Processes

Think about a request in your organization which contains data that has to be approved, decided on and processed. Examples includes a vacation or travel request, an order form to be approved and processed, or access rights and permissions for applications that need to be approved by several people in your organization. An approval process could even be a typical e-government process, where a citizen submits an application for processing by the government. edoras one allows every participant to be part of the whole process as needed.

1.4.2. Help Desk

There is a lot of unstructured work in a typical help desk environment. It needs to be fast, nothing must be lost and the customer needs answers. From submitting a help desk request either via email or over the phone, it all starts with a need from the customer. Some requests such as ordering a new license or upgrading to a new version of your product. might be standard and can be fully structured or even automated. But complaint management or support issues are typically less structured and more driven by knowledge, not routine. That’s exactly where edoras one fits best: the combination of structured and knowledge driven work. If your help desk sees repeating patterns, start structuring them and you will gain added value every day, but even without any structure at all you will never loose a case in your help desk, you always have full access to all data, information and past discussions and you don’t miss a due date, at least not because of the tooling.

1.4.3. HR

HR processes are a great fit for edoras one as there are many people involved. Think of using edoras one in your recruiting process, let applicants apply for a job through an initital job application case form and upload their documents (CV, references etc.) . Then work on the application internally - everyone from HR people to superiors and managers that are part of the recruiting process can participate in the case and make it an effective place to collect information, have discussions and make decisions. Onboarding a new employee is another great example where a lot of people are typically involved - did you think about and remember everything that needs to be done? edoras one will help you with that organization. Once the employee has started work, vacation requests, expenses or travel requests are typical processes where edoras one is a great fit. How do I start using edoras one in my organization? Using edoras one is very simple and even without your own collection of processes, templates and forms, you can benefit from edoras one from the first day by using default templates like a simple case, ad-hoc tasks and review process. Start collecting feedback from the people working with those default templates and then start modeling your own processes, forms and templates accordingly in small steps. Adjust them, deploy them, use them and collect feedback again to constantly improve. That’s what we call "desgin by doing" where your App emerges from your organizational needs in small, but continuous steps.

1.5. What license do I need to work with edoras one?

There are two options to use edoras one, either on-premise or as a service in the cloud. Both options are based on the same license model which is built around three user types:

  • Agents

  • Participants

  • Modelers

Agents and modelers are licensed as named users and participants are all free of charge. An agent has full access to the edoras one features, except the modeling features. You have to be a modeler in order to access the modeling capabilities of edoras one. A participant is able to start new cases and is able to participate with full feature capability in all the cases he started. In foreign cases, a participant can be included for single tasks (like approval) with limited access to the case, for everything else, you need to be an agent to have full access to all features.

1.6. What are the supported runtimes for edoras one?

edoras one is available as a service in the cloud from edorasware directly, as a domain specific service in the private cloud from one of our partners or on-premise in your own infrastructure. On-premise, you can simply use edoras one as a platform and integrate it into your infrastructure and applications or you can even use edoras one as a development platform in order to build your own solution on top of it.

1.7. Architecture Overview

The following diagram shows the main components of edoras one.

architectureOverview
Figure 1. Architecture overview
  1. Web Container (e.g. Tomcat)
    edoras one runs in a Java web-compliant container like Tomcat for instance. You can also deploy edoras one into a JEE compliant server like JBoss, although the JEE parts of the container are not necessarily needed but are integrated with, if available.

  2. Client / Web Browser
    edoras one is fully web-based and only needs a web browser to work with. Even the graphical modeling environment is fully running within the browser without the need of an extra plugin. The client part is based on HTML5 / CSS3 and uses a client side model-view-controller framework (AngularJS) to abstract the data from the view and the controllers.

  3. RDBMS
    All information within edoras one is stored transactionally within an RDBMS. The following database systems are supported by edoras one; Oracle, MS SQL Server, DB2, MySQL, PostgreSQL, H2 and Derby.

  4. Content Repository
    File-based content or large unstructured content is stored within the content repository of edoras one. There is a simple, built-in repository supporting a light-weight content storage system with built-in versioning capabilities. The content can also be stored in an external system like Alfresco, Sharepoint or the like using the CMIS protocol.

  5. Search Infrastructure
    edoras one supports a full text search integration, each work item is searchable as well as its content. The search can be integrated with the featured Java based query API or through the search REST endpoint, supporting full text search as well as structured search.

  6. Work Item Infrastructure (edoras gear)
    The core of edoras one is it’s work item infrastructure supported by edoras gear, the work management framework. You can read more about edoras gear later in this document.

  7. Modelling Infrastructure (edoras vis)
    Part of edoras one is the modelling infrastructure named edoras vis. It is embedded into edoras one for a smooth modeling experience, deploying directly into edoras gear and runtime experience using edoras one. For each supported type of models there is an appropriate provider interpreting that model. Process models for instance are modeled with the BPMN 2 standard and being executed by the process provider, based on the Activitit process engine. The form models are stored in JSON and rendered through the form engine in the client.

  8. REST API
    edoras one supports a REST API based on the JSON data protocol. The edoras one client actually uses the same API as well for all of its features. Additionally, the REST API can be used for any kind of integration with edoras one.

  9. Extensions / Listeners / Providers
    There are a lot of well designed extension points in edoras gear that can be used to extend the work item platform in many ways. Read more details about the extension points later in this document.

  10. Client / Server Communication
    The client / server communication is based on the REST API supported by the edoras one backend services. JSON is used for the data in the requests and HTTP / HTTPS as the transfer protocol. edoras one can be integrated with SSO infrastructures as well.

2. edoras one User Guide

2.1. Basic application layout and navigation

2.1.1. Dashboards

When you log into edoras one, you will normally see the edoras one user dashboard:

dashboardUser

The edoras one dashboards each provide a summary of information that is relevant to a particular user role, in this case the user’s active work items. Only work items that are visible to the current user are displayed. You can change between dashboards using the dashboard selector in the application toolbar:

dashboardSelect
Note
that depending on the user’s permissions, some dashboards may be available.

2.1.2. List view

The dashboard widgets typically show a summary of a particular type of work item:

dashboardWidget

Clicking on the widget header will open the corresponding list view to show the full list of work items (the widget itself only shows the first few):

listView

2.1.3. Work item page

Clicking on a particular item in the dashboard list will take you directly to the work item page where the work item details can be viewed and/or edited:

workItemPage

The work item page will typically provide a number of different work item views, which can be selected using the icons in the top right-hand corner:

viewSelection

and a number of work item actions in the action toolbar on the right hand side which can be used to perform operations on the work item:

actions

2.1.4. Work item views

Browse view

The browse view shows the work item details and is available for all work items. Select the Browse icon to open the work item view:

browseIcon

This will show the work item work form and if the item is active then the details may also be modifiable:

browseView
Search view

The search view shows the current child work items and is available for container work items such as Apps and Cases. Select the Search icon to open the child work item view:

searchIcon

This will show the list of child work items:

searchView
Comments view

The comments view shows the work item comments and is available for all work items.

Select the Comments icon to open the comments view:

commentsIcon

This will show the work item comments, including both automatically generated change notifications and user comments:

commentsView
Preview view

The preview view shows a preview of the work item contents and is available for all work items with suitable content.

Select the Preview icon to open the comments view:

previewIcon

This will show the content preview, which depends on the work item content type. As an example, a form model preview will show a preview image of the form layout:

previewView
Edit entity view

The edit entity view allows all work items to be viewed and/or modified (not just those that are accessible through the normal work item forms). This is a technical view of the work item that is available for all work items but only for users with suitable access permissions.

Select the Edit Entity icon to open the entity editor view:

editEntityIcon

This will show the work item values as a form:

editEntityView
Edit entity JSON view

The edit entity JSON view allows all the raw work item definition to be viewed and/or modified. This is a technical view of the work item that is available for all work items but only for users with suitable access permissions.

Select the Edit Entity JSON icon to open the entity JSON editor view:

editEntityJsonIcon

This will show the work item as a raw text field:

editEntityJsonView

2.1.5. Bookmarking pages

Every page in edoras one has a unique persistent URL that can be bookmarked in the browser or referenced from a link. Simply add a bookmark or copy the page URL from the browser address toolbar.

When following a link to edoras one you may have to log in if the browser does not have active edoras one credentials. When the link points to a work item view, the view will only be shown if the user that is logged in has suitable access permissions for the work item in question.

2.1.6. Keyboard shortcuts

edoras one provides a number of keyboard navigation shortcuts:

Table 1. Keyboard shortcuts
Key Description

c

create a new work item. The work item types that can be created are defined by the current dashboard. Select the work item type using the arrow keys and press Enter to create it.

d

change dashboard. Select the dashboard using the arrow keys and press Enter to open it.

ctrl + s

save form. Save the pending changes of a browse view or a create action.

2.2. Dashboards

2.2.1. User dashboard

The user dashboard provides a summary of the work items that are related to the workflows currently in progress in edoras one:

dashboardUser

The work items shown in the user dashboard are:

Case

a container for related work items and processes.

Task

describes a task to be performed by a user.

Document

a document or other file.

Query

a stored search configuration.

2.2.2. Modeler dashboard

The modeler dashboard provides a summary of the workflow apps and models:

dashboardModeler

The work items shown in the modeler dashboard are:

App Model

a container for related models that combine to define a workflow. The workflow defined by the models can only be used when the app is deployed.

Case Model

defines a particular type of case.

Process Model

defines an executable workflow that combines several tasks, either manual or automated.

Task Model

defines a particular type of task.

Document Model

defines a particular type of document.

Form Model

defines a form used by other models to interact with a specific work item.

Please refer to the edoras one Modeler Guide for more details of the modeler dashboard and the modeler work items.

2.2.3. Admin dashboard

The admin dashboard provides a summary of the system administration work items:

dashboardAdmin

The work items shown in the admin dashboard are:

Account

a container for related users and groups.

Group

defines related sets of users, and can also be used to grant permissions.

User

a particular user within the system.

App

the instance of an app model where the the dynamic properties can be updated.

Please refer to the edoras one Admin Guide for more details of the admin dashboard and the admin work items.

2.2.4. Management dashboard

The management dashboard provides a statistical work item overview:

dashboardManagement

2.3. User Profile

You can manage your profile by accessing the user profile. Click on your user name in the top right corner of the screen and select User Profile.

2.3.1. Change your email address

edoras one sends email notifications depending on your settings. to change your email address simply type the new address in the field Email address and save your changes by clicking save at the bottom of the profile

2.3.2. Change your language

edoras one currently supports english, spanish, german, french and italian as plattform languages. To change the language just select the desired language from the language dropdown and save your changes by clicking save at the bottom of the page.

Note

Changing the language needs a page refresh after the save to become effective. Not every App has to be maintained in all languages. If your language is not maintained in a particular App, it defaults to the primary language defined by the issuer of the App.

2.3.3. Change your password

To change your password simply enter the new password in the password field (There are currently no restrictions about password strength). Then re-enter the password in the repeat password field and save your changes by clicking save at the bottom of the page.

2.3.4. Deputies

You are allowed to select users which can act as your deputies. Activated deputies can act on behalf of you. You can decide whether deputies are notified on the notification events. In the case when somebody else has chosen you as a his/her deputy you can impersonate the user by clicking on the user menu user name and Users to impersonate.

2.4. Avatar

edoras one uses the Gravatar online Avatar Service to load your avatar from the internet. If you don’t use gravatar yet and want to have a custom avatar in edoras one, sign up to Gravatar with the email address specified in your user profile by clicking on Create your own Gravatar on their website.

2.5.1. Basic searching

To search for work items in edoras one, use the search functionality provided by the edoras one toolbar.

Clicking on the triangle to the right of the search field opens a pull-down dialog where a number of common search settings can be selected. As these options are changed, the results of the search are shown in the list view:

Search dialog

Typing a plain text value into the search text field will also restrict the search results to work items that contain the given text, either in the name, in the description, or in a string attribute value.

2.5.2. More complex searching

In addition to plain text search, the search text field supports a query language that allows more complex queries to be created.

Search terms

A query consists of a list of search terms separated by whitespace:

<term1> <term2> <term3> …​

In the simplest case, a term can be a single word which performs a text search for work items that contain the given word. This is the basis for the search mechanism described in <basic-search>.

When searching, the search results from each term are combined with 'and', so each additional term restricts the search results more tightly.

To include whitespace in a search term, surround the search text with quotes (i.e. +'multiple word search').

More complex terms follow the pattern:

<termKey>:<expression>,<expression>,…​

where the term key defines the type of search to be performed, and the list of expressions defines the values to be matched.

The values matched by the expressions are combined with 'or', so each additional expression extends the list of search results.

Expressions may also consist either of a single value (e.g. 123) or be an operator / operand pair, also separated by a : (e.g. gt:123).

As an example, the search term priority:1,3,gt:5 will return all work items that have a priority of 1, 3 or any value greater than 5.

The following search term keys are currently supported by edoras one:

Table 2. Supported term keys
Term key Expression type Searches by

app

Model expression

user work items based on models from matching apps

assignee

User expression

assigned user

created

Date expression

creation date

dashboard

Dashboard expression

work item type (using dashboard shortcuts)

descendantsOf

ID

hierarchy

description

String expression

work item description

desc

String expression

work item description (alias for description)

due

Date expression

due date

for

User expression

assigned user (alias for assignee)

model

Model expression

user work items based on matching models

name

String expression

work item name

owner

User expression

owner

parent

ID

hierarchy (alias for descendantsOf)

priority

Number expression

priority

state

State expression

state

text

String expression

textual content (the default search term key)

type

Type expression

work item type

updated

Date expression

last modified date

<variable name>

all

variable value

2.5.3. Sort terms

In addition to the search terms described above, a sort term may also be specified which allows the order of the search results to be specified. A sort term begins with sort:, is followed by an optional asc: (ascending, the default) or desc: (descending), and then the attribute to sort by. The following sort attributes are supported:

Table 3. Sort term values
Sort attribute Description

assignee

assignee (grouping only, no alphabetical sort)

creation

creation date

due

due date

modification

last modified date

name

work item name

owner

owner (grouping only, no alphabetical sort)

priority

priority

type

work item type

Examples
sort:desc:name
sort:creation

2.5.4. User expression

A user expression accepts the following operand values:

Table 4. User operand values
Operand value Matches

<ID>

the specific user ID (work object ID)

all

any assigned user

me

the current user

unassigned

unassigned work items (a value alias for the empty operator)

<text>

any user with the given text as part of the display name

The following operators are also supported:

Table 5. User operators
Value Matches

empty:

unassigned work items

Examples
owner:empty:
owner:all
owner:me
owner:joe

2.5.5. Date expression

The following date operators are supported:

Table 6. Date operators
Operator Operand value Matches

eq:

date

values equal to the given value (default)

gt:

date

values greater than the given value

gte:

date

values greater than or equal to the given value

lt:

date

values less than the given value

lte:

date

values less than or equal to the given value

range:

date..date

values in the given range (inclusive)

The following date specification formats are supported:

Table 7. Date specification formats
Format Description

yyyy-MM-dd

a specific date

[yY][mM][wW][dD]

a date relative to today in years, months weeks, or days

today

today’s date

Examples
created:today
created:lt:2014-02-14
updated:range:-2W..2W
Note
Date values are currently interpreted in the UTC timezone, so a date-based search may occasionally contain unexpected results when viewed from a particular user’s local timezone.

2.5.6. Number expression

The following number operators are supported:

Table 8. Number operators
Operator Operand value Matches

eq:

<number>

values equal to the given value (default)

gt:

<number>

values greater than the given value

gte:

<number>

values greater than or equal to the given value

lt:

<number>

values less than the given value

lte:

<number>

values less than or equal to the given value

range:

<number>..<number>

values in the given range (inclusive)

Examples
priority:1,2
priority:gt:5

2.5.7. State expression

No special state operators are supported, only the following state operand values:

Table 9. State operand values
Operand value Matches

all

all states (no restriction)

open

open work items (active, pending, pending inactive)

active

active work items

inactive

inactive work items

pending

pending work items

'pending inactive'

pending inactive work items (quotes are required)

completed

completed work items

archived

archived work items

interrupted

interrupted work items

Examples
state:open
state:archived,completed

2.5.8. Type expression

No special type operators are supported, only the following operand values:

Table 10. Type operand values
Operand value Matches

CAS

cases

DOC

documents

TSK

tasks

QUERY

queries

APP

apps

CASE_MODEL

case models

DOCUMENT_MODEL

document models

FORM_MODEL

form models

MAIL_MODEL

mail models

PROCESS_MODEL

process models

TASK_MODEL

task models

APP_MODEL

app models

ACC

accounts

GRP

groups

USR

users

Examples
type:TSK,DOC

2.5.9. Dashboard expression

No special dashboard operators are supported, only the following operand values:

Table 11. Dashboard operand values
Operand value Matches

admin

accounts, groups, users & apps

modeler

models

user

cases, task, documents & queries

Dashboard terms are just a shortcut for a type term with the corresponding type list.

Examples
dashboard:user

2.5.10. String expression

The following string operators are supported:

Table 12. String operators
Operator Operand value Matches

contains:

<string>

values that contain the given substring (default)

empty:

<string>

values that are not set

eq:

<string>

values that exactly match the given string

Examples
name:example
description:empty:

2.5.11. Model expression

The following model operators are supported.

Table 13. Model operators
Operator Operand value Matches

contains:

<string>

values that contain the given substring (default)

eq:

<string>

values that exactly match the given string

The operand value is used to perform a name search on apps or models (depending on the term). For the app term, all models from the matching apps will be selected, for the model term only the matching models will be selected.

The search results will consist of all work items that were created from one of the selected models.

Examples
app:vacation
model:eq:'Vacation Request Case'
Note
In order to retrieve the corresponding work items, permissions to access the corresponding models are needed.

2.6. Glossary

app model

an app model is a container for a collection of models that define a specific workflow.

dashboard

a page in the edoras one application where work objects can be found, displayed and manipulated. Each dashboard manages related work object types that play a given role within the system.

model

a description of a workflow component which can be deployed together with other models to create a executable workflow in edoras one

operand

the value part of a search term expression.

operator

the optional first part of a search term expression which says how the operand value should be interpreted.

process

a process is a workflow definition, described using a BPMN 2.0 model and executed within edoras one. It typically defines the automated and manual tasks that must be performed to complete the workflow.

search term expression

part of a search term. A search term can contain multiple expressions, separated by commas. The result set for the term contains work items that match any of the operators. A search term expression contains an optional operator, and an operand value.

search term

part of a search query. The result set for the query contains work items that match all of the separate terms.

task

a task is a single action within a workflow, and may be either automated (a service task) or manual (a user task).

work object / work item

an item that can be created or manipulated in edoras one. Different types of work item are supported. The work object types are typically grouped according to their role within the system and accessed through dashboards.

3. edoras one Modeler Guide

3.1. Modeler Overview

The modeler dashboard can be accessed by authorized users through the dashboard menu and is used to create and deploy workflow models for use within edoras one.

Before we look at the details of the modeler dashboard it will help to understand how models are related to the user dashboard.

Models are created edited in the modeler dashboard, and as long as they are only edited they are not visible in the user dashboard. When models are deployed, corresponding definitions are created. These definitions can be used in the user dashboard to create work objects of the new type.

3.1.1. App Models

An edoras one App model contains a collection of different models that combine to define a specific workflow within edoras one.

An app model can be created and then different models can be added and linked together as required. When the workflow is ready, it can be deployed to create the corresponding definitions which are used to execute the workflow, and it can then be tested.

App models can be transferred between different edoras one installations using the Export and Import actions.

3.1.2. edoras vis

edoras vis is a modeler with focus on modeling BPMN 2.0-compliant processes, edoras one compliant forms and CMMN 1.0-compliant case models.

3.2. Model types

3.2.1. Form model

A form model defines a form that can be used by other workflow models to interact with the user. Each form contains a number of form fields arranged in a given layout. The fields in a form may contain predefined content, or may be bound to work object values at runtime, for example to allow the name and description of a document to be displayed and perhaps changed.

Typically two types of forms are used: init forms are used to initialize new work objects, and work forms are used to edit existing work objects.

Form models can be quickly checked by hovering over the form name, previewed within edoras one using the Preview view or edited in the edoras vis modeler using the Design action. It is also possible to download the XML form definition using the Download action.

3.2.2. Task model

A task model is used to define a new type of user task. The init and work forms for a task are used to create and edit the task contents, and are set using the Browse view. The default candidate groups used for sharing can also be set, and the actions that will be allowed on this type of task can also be selected.

3.2.3. Document model

A document model is used to define a new type of document. As in the task model, the Browse view allows the init and work forms to be selected, as well as setting the candidate groups and allowed actions. Moreover, the placeholders can also be defined in this view.

Skeleton documents

A skeleton document can be set using the Upload skeleton action. If a skeleton document is provided then whenever a new document of this type is created it will be initialized with its own copy of the skeleton document. Placeholders can also be defined to insert dynamic text into the document copy.

Placeholder expressions

Document models can define placeholder expressions that can be used to insert dynamic values into documents.

A placeholder definition consists of a key (which will be referenced from the document) and an expression that will be evaluated to give the corresponding value:

placeholders

The expression will be applied at the time that the placeholders are updated, i.e. either when the document is first created from the document model or when the Update placeholders action is used subsequently.

Referencing placeholders from a Microsoft Word document

In a Microsoft word document, placeholders may be referenced by inserting a form field into the document with a Bookmark name that corresponds to the placeholder key:

word placeholder
Referencing placeholders from an Adobe PDF document

In a PDF document, placeholders may be referenced by inserting a form field into the document with a name that corresponds to the placeholder key:

pdf placeholder
Note
A suitable PDF editing tool is required to create PDF documents with form fields.
Microsoft Word Mailing support

Microsoft Word documents support mailings functionality. Mailings fields can be replaced in a similar way as placeholders with document variable values. The fields have to be encapsulated into TableStart/End tags (Mailings regions support). variableName tag is replaced by variable value on update placeholders action.

wordMailings

3.2.4. Process model

A process model allows a full BPMN 2.0 process execution model to be defined. As for form models a process model can be previewed or edited in the modeler using the Design action, and the process definition XML can also be downloaded. Process models allow modeler to specify process init and work forms.

3.2.5. Case model

A case model is used define a new type of case. A case serves as a container for other work objects and can be used to store shared context information for a given workflow.

When a new case is created, it is possible to start processes, create tasks or create documents and link these to the case automatically. It is also possible to define which processes, tasks and documents can be created. This behaviour is defined using the Browse view. This view allows the init and work forms to be selected, as well as setting the candidate groups and allowed actions.

3.2.6. Mail model

A mail model provides a template for mails that can be sent by a given workflow. The mail model contains the templates for mail subject and header, both of which can use edoras one backend expressions to fill in the template with dynamic information at the time that the mail is sent.

3.3. Working with models

3.3.1. Accessing the modeler dashboard

All users that are part of the modeler group will be able to access the modeler dashboard.

3.3.2. Importing App models

An App model can be imported into the system by creating a new App through the user interface, and then using the Import action within the app to upload and import a Zip file containing the models to be imported.

When models are imported, the model will be checked to see whether it is already present on the system. If it is not present, then it will be created in the current App. If it is already present within the current App then it will be updated from the import file. If it is already present but as part of a different App then an error will be shown.

If a new model is created from scratch it is assigned an unchangeable ID which is used to track the model across all systems where the model is installed, and it is this ID which is used to establish the relationship between the models on the system and the models in the import file.

If the Import as duplicate option is selected when an App is imported the models in the imported App will not be checked against the models already present. New models will be created that are unconnected with the originals. This can be useful for creating a completely new App, using an existing App as a starting point.

3.3.3. Models state

Models have a state which changes as the models are edited and deployed. The available states are:

Table 14. Model states
State Applies to Description

active

Models

No pending changes. The model is currently deployed on the system.

inactive

Models except App model

No pending changes. The model has been deactivated on the system.

pending

Models

The model contains changes that have not yet been deployed.

pending inactive

Models except App model

The model will be deactivated on the system when it it next deployed.

3.3.4. Deploying an App model

When an App model is deployed, the models within that App model will be checked for consistency. If a model has been deactivated but there are still references from other active models then it will not be possible to deploy the App model. If the models are consistent then they will be deployed and corresponding definitions will be created. The users of the system will then be able to access the new workflow in the user dashboard.

Note that work objects and processes may already exist that were created using older versions of the model. These will not automatically be updated to use the new definitions: they will continue to use the definitions that were valid when they were created and will therefore not behave any differently after the new deployment.

3.3.5. Moving models between App models

All of the basic model types can be moved to another App as long as there are no outstanding references within the App model that it is being moved from.

3.3.6. Duplicating App models

All basic model types can also be duplicated, making an exact copy with a new name. This can be very useful when several small variations of a complex model are required.

3.3.7. Sharing models

Models may also be shared with different groups. By default all new models are shared with the modeler group, but in some circumstances it may be required for particular users to work on a particular set of models.

3.3.8. The System App

The administrator will also be able to see the System app in the modeler dashboard in addition to the App models that can be seen by normal modeler users. This App model contains a number of models that are required by the edoras one application and should normally not be changed.

3.4. Actions

Certain models are used to create user work items (e.g. Case, Task, Document). When created, the work item action toolbar (on the right hand side) will contain the actions that can be performed on the work item.

Which actions are available to the user depends on a number of factors:

  • the actions allowed by the model itself

  • the state of the work item

  • the user’s permissions

Some constraints are defined by edoras one itself and cannot be changed, but the model plays a large role in defining the available actions, and this should be considered carefully when designing a workflow.

To change the allowed actions, select the Browse view and add or remove actions as required in the Allowed Actions field:

allowedActions
Table 15. Actions
Action Case Task Document Description

Activate

Yes

Yes

Yes

Re-activate an archived work item

Archive

Yes*

No

Yes

Archived and active work item.

Assign

Yes

Yes

Yes

Change the work item’s owner / responsible

Create variables

Yes

Yes

Yes

Allow new variables to be created in the work item admin view

Download

No

No

Yes

Download the work item content

Download Pdf

No

No

Yes

Download the document in pdf format

Edit document

No

No

Yes

Edit the document

Move

No

No

Yes

Move the work item to another case

Preview

No

Yes

No

Show a work item preview

Share

Yes

Yes

Yes

Share the work item with specific groups

Start process

Yes

No

No

Start a new process instance

Update placeholders

No

No

Yes

Upload a new document

Upload document

No

No

Yes

Upload a new document

*There are three possibilities to archive cases which are displayed in the following table:

Table 16. Archive options for cases
Option Name Description

Archive only if there are no active children

If case contains active processes, tasks or documents archive action returns an error.

Archive only if there are only active documents

If case contains active processes or tasks archive action returns an error. Active documents are archived automatically.

Archive the case and all children

Active processes and tasks are interrupted and it is not possible to activate them again. Active documents are archived.

3.5. Form palette

This section describes the elements available in the form palette of edoras one. Each modelling element and it’s corresponding attributes are described in detail below.

3.5.1. Concepts

This section describes the form concepts in edoras one. It starts with a description of how the layout of forms and fields works in edoras one. Then it describes how a form and its fields are bound to the data model.

Form layout

A form is a graphical layout of fields that may be bound to a data model to allow this data model to be displayed to the user. The fields are arranged in rows.

formLayout
Figure 2. Form layout

Each row is made up of exactly twelve slots. All slots are equal in size, so the width of a slot is one twelfth the width of the whole form. A field can occupy exactly all slots or it can occupy only a part of the slots. With this it is possible to place more than one field in a row.

formLayoutWithSlots
Figure 3. Form layout with slots

Not every slot needs to be occupied by a field. It is allowed that single slots of a row are empty. These empty slots can be at the beginning, in the middle, or at the end of a row.

formLayoutWithEmptySlots
Figure 4. Form layout with empty slots
Field layout

A field is made up of several parts (all but the widget part are optional):

widget

The widget visualizes the data model. Most widgets are interactive and let the user change the data model, however some widgets are read-only. There are widgets to manipulate strings, numbers, dates, rich text, selections, and many others. The widget is located in the center of the field, all other parts are arranged around the widget. The field configuration defines the concrete positioning of the field parts.

Label

The label tells the user to which part of the data model the widget is bound. The label is located on the left side or on top of the widget (depends on the field configuration).

Required indicator

The required indicator is displayed for required fields. Required means that the widget cannot be empty. The required indicator is located on the right side of the widget.

Description

The description gives additional information to the user. This can be formatting instructions or further description about how the App uses the entered value. The description is located on the bottom of the widget.

fieldLayout
Figure 5. Field layout
Field alignment

To enhance the readability of a form, the field parts of all fields are aligned among themselves: the labels are aligned, the widgets and descriptions are aligned, the required indicators are aligned.

The alignment is performed per slot. All fields that start in the same slot build a field group. Inside such a field group edoras one looks for the widest label which becomes the dominating label for that slot. All labels in the field group are then enlarged to the same width of the dominating label. This is done by adding empty space between the label and the widget. This ensures that all widgets and descriptions of a field group start in one line and all.

fieldAlignment
Figure 6. Field alignment

The only exception is with fields that have top label position. For these fields the label, the widget and the description all start at the beginning of the slot.

fieldAlignmentTop
Figure 7. Field alignment with top label position
Binding

Binding is the process to connect the form fields to the data model. The binding in edoras one is always two-way. When the user changes the form fields then those changes are immediately propagated to the data model. On the other side changes in the data model are immediately propagated back to the form fields.

A binding expression defines to which part of the data model the form field is connected. The binding expression has to be a writable frontend expression, e.g. {{foo}}, {{case.bar}}.

There are some reserved expressions which can not be used as they are used internally to save some work item information. They are:

  • id

  • definitionId

  • definition

  • type

  • state

  • creationTime

  • updateTime

  • currentUser

Moreover the expressions referring to the hierarchy (e.g. root, parent) can be only be used in order to bind nested widgets like subforms.

3.5.2. Common attributes

There are several attributes that are available for all form widgets. These common attributes define the rendering and the layout of the form field, as well as the part of the data model to which the form field is bound.

Attribute name Description

Label

The label is a single word or term that describes the purpose of the form widget. When the form is rendered, the label is located either on the left side or on top of the widget (depends on the value of the Label position attribute).

Typical labels are: First name, Last name, Sex, Address, ZIP code, City.

Labels can be localized. See Localization to learn how to localize your Apps.

Label position

The label position relative to the widget, can be either Left or Top.

Value

The value connects the form field to the data model. As such the value decides which part of the data model the form field visualizes and which part of the data model is updated when the user interacts with the widget.

See Binding to learn how binding in edoras one works.

Default value

The default value is the value that is applied when the data model is undefined, i.e. not yet initialized. The default value can be expressed as a static value or as a frontend expression.

Typical default values are: Support (= string), false (= boolean), 42 (= number), Comment from {{role}} (= frontend expression).

See Frontend expressions to learn the frontend expression syntax.

Description

The description is a whole sentence that describes the purpose of the form widget in more detail. When the form is rendered, the description is located below the widget.

A typical description is: The ZIP file format is ##, e.g 4143. The ZIP is used to lookup the city.

Descriptions can be localized. See Localization to learn how to localize your Apps.

Documentation

Intended for documenting details about specific widget to explain concepts of its use for future reference.

Visible

The visible flag decides if the form field is shown or hidden. If true, the form field is shown, if false the form field is hidden. The visible flag is a runtime value which means you can configure a frontend expression for it.

A typical frontend expression for the visible flag is: {{case.showDeliveryAddress}}

See Frontend expressions to learn the frontend expression syntax.

Editable

The editable flag decides if the form widget is editable or read-only. If true, the user can manipulate the form widget to change the data model, if false the user is not able to change the data model. The editable flag is a runtime value which means you can configure a frontend expression for it.

A typical frontend expression for the editable flag is: {{case.step1Completed}}

See Frontend expressions to learn the frontend expression syntax.

Style class

The style class allows to add additional CSS styles to the form field.

3.5.3. Validation attributes

A validation method checks if a widget contains valid data, if not edoras one displays an error message that instructs the user how to fix his input. edoras one supports many different validation methods, not all are available for all form widgets.

Note
Validation is only done if the user is able to fix a potential validation error. This is not the case for hidden and read-only form fields, so such fields are not validated.

The following table lists the supported validation methods.

Attribute name Description

Required

The required flag defines if a form field can be empty or not. If true then the form field is not allowed to be empty, if false the form field might be empty.

A typical sample where the required flag is set to true is a name field that is mandatory.

The following form widgets support the required flag: text, text area, rich text area, password, number, date, checkbox, autocomplete, select, radio buttons.

Minimum length

The minimum length validation ensures that the text input is longer than a minimum length.

A typical sample is a password that has to be at least 4 characters long.

The following form widgets support the minimum length validation: text, text area, rich text area, password.

The Minimum length error message attribute holds the error message that is displayed when the text input is less than the configured number of characters.

Maximum length

The maximum length validation ensures that the text input has a smaller than a maximum length.

A typical sample is a password that has to be at most 8 characters long.

The following form widgets support the minimum length validation: text, text area, rich text area, password.

The Maximum length error message attribute holds the error message that is displayed when the text input is bigger than the configured number of characters.

Regular expression

The regular expression validation ensures that the text input matches a regular expression. See http://www.w3schools.com/jsref/jsref_obj_regexp.asp for more information on the supported regular expression syntax. Regular expression can be become quite complex and difficult to understand. http://www.regular-expressions.info/javascriptexample.html provides a good way to test your regular expressions.

A typical sample is the number of a credit card which matches: ^4[0-9]{11,12}(?:[0-9]{3})?$.

The following form widgets support the regular expression validation: text, text area, rich text area, password.

The Regular expression error message attribute holds the error message that is displayed when the text input does not match the regular expression.

Mask

The mask validation ensures that the text input adhere to a certain pattern. The mask can contain the following special characters: * a - Represents an alpha character (A-Z,a-z) * 9 - Represents a numeric character (0-9) * * - Represents an alphanumeric character (A-Z,a-z,0-9) * all other characters are treated as fixed input

A typical sample is the expiry date of a credit card which is the month followed by a dash followed by the year. The mask that adheres to this pattern is 99/9999.

The text form widget supports mask validation.

The Mask error message attribute holds the error message that is displayed when the text input does not match the mask.

Minimum

The minimum validation ensures that the number input is equals to or bigger than a minimum.

A typical sample is an order quantity field which has to be equals to or bigger than 1.

The number form widget supports the minimum validation.

The Minimum error message attribute holds the error message that is displayed when the number input is less than the configured minimum.

The minimum number supported is -9007199254740991.

Maximum

The maximum validation ensures that the number input is equals to or less than a maximum.

A typical sample is an order quantity field which has to be equals to or lower than 100.

The number form widget supports the minimum validation.

The Maximum error message attribute holds the error message that is displayed when the number input is bigger than the configured maximum.

The maximum number supported is 9007199254740991.

Minimum date

The minimum date validation ensures that the date input is equals to or after a minimum date. The minimum date can be a fixed date, today, or a day relative to today.

A typical sample is an delivery date field which has to be equals to or after today.

The date form widget supports the minimum date validation.

The Minimum date error message attribute holds the error message that is displayed when the date input is before the configured minimum date.

Maximum date

The maximum date validation ensures that the date input is equals to or before a maximum date. The maximum date can be a fixed date, today, or a day relative to today.

A typical sample is a birthday field which has to be equals to or before today.

The date form widget supports the maximum date validation.

The Maximum date error message attribute holds the error message that is displayed when the date input is after the configured maximum.

Invalid selection error message

This validation displays an error message when the selection input is not available anymore.

The autocomplete and the select widgets support the invalid selection validation.

Both form widgets look up if the selection is in the available options to check if the selection is valid or not.

Minimum number of elements

The minimum number of elements validation ensures that the input contains at least a certain number of elements.

A typical sample is an order which has to contain at least one order position.

The subform widget and the autocomplete widget with multi-selection supports the minimum number of elements validation.

The Minimum number of elements error message attribute holds the error message that is displayed when the input contains less than the configured number of elements.

Maximum number of elements

The maximum number of elements validation ensures that the input contains at most a certain number of elements.

A typical sample is a wish list which can hold at most ten wishes.

The subform widget and the autocomplete widget with multi-selection supports the maximum number of elements validation.

The Maximum number of elements error message attribute holds the error message that is displayed when the input contains more than the configured number of entries.

Minimum number of attachments

The minimum files validation ensures that not less than a certain number of files are attached.

A typical sample is a support incident attachment field where at lease one screenshot has to be attached.

The attachment form widget supports the minimum files validation.

The Minimum number of attachments error message attribute holds the error message that is displayed when the attachment input has less files attached than the configured number.

Maximum number of attachments

The maximum files validation ensures that at not more than a certain number of files are attached.

A typical sample is a favourite images attachment field where not more than ten images can be attached.

The attachment widget supports the maximum files validation.

The Maximum number of attachments error message attribute holds the error message that is displayed when the attachment input has more files attached than the configured number.

Minimum number of rows

The minimum number of rows validation ensures that not less than a certain number of rows are displayed.

A typical sample is a list which has always the same name of rows even they are empty.

The list form widget supports the minimum number of rows validation.

Maximum number of rows

The maximum number of rows validation ensures that at not more than a certain number of rows are displayed.

A typical sample is a wish list which can hold at most ten wishes.

The list form widget supports the maximum number of rows validation.

3.5.4. Text form widgets

The text form widgets allow the user to input text. To support different text input requirements edoras one provides different text form widget variants. The following list explains the different text form widget variants:

Form widget Description
formComponentText

Used to input a single line of regular text.

formComponentTextRuntime
formComponentTextArea

Used to input multiple lines of regular text.

formComponentTextAreaRuntime
formComponentRichTextArea

Used to input multiple lines of rich text.

In contrast to regular text, rich text can be formatted in various ways: regular, bold, italic, ordered list, unordered list, headings, and much more.

formComponentRichTextAreaRuntime
formComponentPassword

Used to input a password.

The password form widget is marked with a small key icon at the right border of the widget. To hide the typed text from curious people all typed characters are visualized as dots.

formComponentPasswordRuntime
formComponentIntegralNumber

Used to input an integral number.

The integral number form widget is marked with a small hash icon at the right border of the widget.

formComponentNumberRuntime

We should to be aware that Javascript number has a limited precision (64-bit binary format IEEE 754 value).

It does not support a negative exponential notation like 1e-2 but can support positive exponential notation like 1e+2.

formComponentFloatNumber

Used to input an float number.

The float number form widget is marked with a small hash icon at the right border of the widget.

formComponentNumberRuntime

We should to be aware that Javascript number has a limited precision (64-bit binary format IEEE 754 value). So if, for example, we have a number with too many decimals, which cannot be represented, it will be rounded.

formComponentDate

Used to input a date.

The date form widget is marked with a small calendar icon at the right border of the control. All non date input is treated as invalid.

The user can enter a date in two ways. First he can enter the date as date string, e.g. 1970-11-25. Second he can use a date picker to select a date with the mouse. The date picker pops up as soon as the date control receives focus.

formComponentDateRuntime
Text attributes
Attribute Description

Common

See Common attributes to learn more about the common attributes.

Validation

The text form widget supports the required, the minimum length, the maximum length, the regular expression, and the mask validation.

See Validation attributes to learn more about validation.

Text area attributes
Attribute Description

Common

See Common attributes to learn more about the common attributes.

Validation

The text area form widget supports the required, the minimum length, and the maximum length validation.

See Validation attributes to learn more about validation.

Rich text area attributes
Attribute Description

Common

See Common attributes to learn more about the common attributes.

The Default value attribute of a Rich text area form widget is rich text which supports the following formatting options (see toolbar buttons on top of the default value editor):

  • bold, italic, and underline text style

  • font size

  • foreground and background color

  • left, center, and right text alignment

  • headline, sub-headline, and section headline style

  • ordered and unordered list

  • text indentation

  • horizontal line

  • links

formComponentRichTextAreaDefaultValue

Validation

The rich text area form widget supports the required, the minimum length, and the maximum length validation.

See Validation attributes to learn more about validation.

Password attributes
Attribute Description

Common

See Common attributes to learn more about the common attributes.

Validation

The password form widget supports the required, the minimum length, and the maximum length validation.

See Validation attributes to learn more about validation.

Integral number attributes
Attribute Description

Common

See Common attributes to learn more about the common attributes.

*Default property: it only supports a frontend expression or an integral number without format.

Validation

The number form widget supports the required, the minimum, and the maximum validation.

The maximum value for a number is 9007199254740991. The minimum value for a number is -9007199254740991.

See Validation attributes to learn more about validation.

Format

Defines the format of the number with thousand and decimal separators. "No format" will not apply any format.

Format "auto" will apply the format depending on the language of the current user. With Format "auto", if the language is English, it will apply "1,000,000" format. With Format "auto", if the language is Spanish, German or Italian it will apply "1.000.000" format. With Format "auto", if the language is French, it will apply "1 000 000" format.

Float number attributes
Attribute Description

Common

See Common attributes to learn more about the common attributes.

*Default property: it only supports a frontend expression or a number without format.

Validation

The float number form widget supports the required, the minimum, and the maximum validation.

The maximum value for a number is 9007199254740991. The minimum value for a number is -9007199254740991.

See Validation attributes to learn more about validation.

Fraction size

Defines the maximum number of decimal characters for float type.

If fraction size is bigger than 0, exponential notation is not supported. If fraction size is 0, it will behave like integer number widget.

Format

Defines the format of the number with thousand and decimal separators. "No format" will not apply any format.

Format "auto" will apply the format depending on the language of the current user. With Format "auto", if the language is English, it will apply "1,000,000.22" format. With Format "auto", if the language is Spanish, German or Italian it will apply "1.000.000,22" format. With Format "auto", if the language is French, it will apply "1 000 000,22" format.

Date attributes
Attribute Description

Common

See Common attributes to learn more about the common attributes.

The default value of a date field form field can be specified as absolute date, as relative date, or as frontend expression.

formComponentDateDefaultValue

Validation

The date form widget supports the required, the minimum date, and the maximum date validation.

See Validation attributes to learn more about validation.

Format

Used to define the format in which the date is visualized to the user. If empty then the date is presented in the ISO 8601 format, e.g. 1970-11-25.

See Formatting dates in frontend expressions to learn more about the possible format strings.

Number of visible years

Used to define the range of years displayed in the year drop-down.

formComponentDateRuntimeYearRange

3.5.5. Select form widgets

The select form widgets allow the user to select one or more options from a potential large set of options. The available options are presented in a list from which the user can select one or more options with help of the keyboard and the mouse.

Some select form widget support the filtering of the available options. When the user starts typing then only the options that match the typed text are presented as available options. This makes it easy to locate a specific option in a large set of options.

The select form widgets are very powerful form widgets. To support different selection requirements edoras one provides different variants of select form widgets. The following list explains these variants:

Form widget Selection Options Description
formComponentCheckbox

Single

Boolean true/false

Used to select a simple yes/no option.

When the checkbox is ticked then true is stored in the data model, otherwise false is stored in the data model.

formComponentCheckboxRuntime
formComponentRadioButtonGroup

Single

Static options

Used to select a single option from a static list of options.

At design time the modeler configures for each option a name and a value. The name is displayed to the user whereas the value is stored in the data model.

In the radio button form widget all options are always visible. With this the user can always see all possible option, on the other side this uses a lot of form real estate and is therefore suited only it there are few options.

formComponentRadioButtonGroupRuntime
formComponentAutocompleteRest

Single or multiple

JSON options

Used to select one or more options from a dynamic list of options.

The dynamic list of options is built at runtime by calling a custom REST endpoint that the modeler has configured. Either a single attribute of the option or the complete option is stored in the data model.

formComponentAutocompleteRuntime
formComponentAutocomplete

Single or multiple

edoras one work items

Used to select one or more work items from a dynamic list of work items.

The dynamic list of work items is built at runtime by calling edoras one with a query that the modeler has configured. Either the work item id or the complete work item is stored in the data model.

formComponentAutocompleteStatic

Single or multiple

Static options

Used to select one or more options from a static list of options.

At design time the modeler configures for each option a name and a value. The name is displayed to the user whereas the value is stored in the data model.

formComponentSelectStatic

Single

Static options

Used to select a single option from a static list of options.

At design time the modeler configures for each option a name and a value. The name is displayed to the user whereas the value is stored in the data model.

The select static form widget is not able to filter the possible options. To select an option the user first has to navigate to the desired option by scrolling in the option list. Therefore this form widget is not a good choice if there is a large number of options.

Note
The functionality of the Radio button group, the Autocomplete static and the Select static form widgets is similar. The only difference is the look and feel. The Radio button group form widget always shows all options with the drawback that this requires a lot of form real estate. The Autocomplete static and the Select static form widgets need little form real estate with the drawback that the user first has to focus the field and open a popup to see the available options.
Note
the functionality of the REST autocomplete select and the Autocomplete select form widgets is similar. The only difference is that the REST autocomplete select form widget uses a custom REST endpoint to load the possible options (which can be of any format) whereas the Autocomplete select form widget uses edoras one to load the possible work items.
Checkbox attributes
Attribute name Description

Common

See Common attributes to learn more about the common attributes.

Validation

The checkbox form widget supports the required validation.

See Validation attributes to learn more about validation.

Radio button group attributes
Attribute name Description

Common

See Common attributes to learn more about the common attributes.

Validation

The radio button group form widget supports the required validation.

See Validation attributes to learn more about validation.

Options

Defines the options from which the user can choose. Each option has a name and a value: the name is shown to the user whereas the value is stored in the data model.

formComponentOptions

Orientation

Defines if the single radio buttons in the group are Horizontal or Vertical aligned.

formComponentRadioButtonGroupRuntimeVertical
REST autocomplete select attributes
Attribute name Description

Common

See Common attributes to learn more about the common attributes.

Validation

The REST autocomplete select form widget supports the following validations:

  • Required.

  • Invalid selection.

  • Minimum number of elements.

  • Maximum number of elements.

See Validation attributes to learn more about validation.

Query URL

Used to load the available options that are presented to the user from a URL.

The query URL attribute can be expressed as a static value or as a frontend expression. See Frontend expressions to learn the frontend expression syntax.

The autocomplete form widget expects the options to be returned as list of JSON objects, e.g.

[ {
  "id" : "GEAR-a65c4a12-20b4-45d8-bb09-2105be7f0d1f",
  "title" : "edoras one Modeler",
  "value" : "GEAR-a65c4a12-20b4-45d8-bb09-2105be7f0d1f"
}, {
  "id" : "GEAR-c641f3bb-876e-45f4-9550-ef95ef5a2fac",
  "title" : "edoras one User",
  "value" : "GEAR-c641f3bb-876e-45f4-9550-ef95ef5a2fac"
} ]

The user typed text is added as a URL parameter to the query URL.

https://one.edorasware.com/rest/one-groups?typedText=edoras

Usually the REST endpoint uses the typed text to filter the available options. This way the user sees only the options that match the already typed text and can easily locate single options in a large set of options.

Note
The options are filtered in the REST endpoint and not in the autocomplete form widget. If the REST endpoint does not do the filtering then independent of the typed text the autocomplete form widget always presents the same options to the user.
Note
Referencing external resources may have problems due to SSL (https) or CORS security browser limitations. To solve CORS security, the resource server must send appropriate response headers. Another option is using a proxy to receive the right response headers from any url e.g .:https://cors-anywhere.herokuapp.com. To solve SSL if edoras one is being executed over https, the url of the external resource should also have the https protocol.

Lookup URL

Used for two things:

  • first: to load the complete information of the selected options (only if they are stored in serialized form in the data model)

  • second: to validate if the selected options are still available, see Validation attributes to learn more about this validation

In both cases the serialized option is appended to the lookup URL, e.g.

https://one.edorasware.com/rest/one-groups/GEAR-a65c4a12-20b4-45d8-bb09-2105be7f0d1f

The option is serialized with help of the Serialization. So the Lookup URL and the Serialization attribute have to play together.

Serialization

Used to select the attribute in the JSON option that is stored in the data model.

The prefix to select an attribute from the JSON option is item.

So if the JSON option looks like

{
  "id" : "GEAR-c641f3bb-876e-45f4-9550-ef95ef5a2fac",
  "title" : "edoras one User",
  "value" : "GEAR-c641f3bb-876e-45f4-9550-ef95ef5a2fac"
}

then a Serialization of item.id results in GEAR-c641f3bb-876e-45f4-9550-ef95ef5a2fac being stored in the data model.

Note
Please note that the Serialization, the Query URL, and the Lookup URL attributes have to play together. The Serialization converts the JSON options returned by the Query URL REST endpoint into strings which then are stored in the data model. And the Lookup URL REST endpoint does the opposite: it takes the strings stored in the data model and converts them back into JSON options.

Format

Used to compose a user friendly string from the JSON option.

The prefix to select an attribute from the JSON option is item. Literals have to be enclosed in quotes, attributes and literals can be concatenated with +.

So if the JSON option looks like

{
  "id" : "GEAR-c641f3bb-876e-45f4-9550-ef95ef5a2fac",
  "title" : "edoras one User",
  "value" : "GEAR-c641f3bb-876e-45f4-9550-ef95ef5a2fac"
}

then a format of Group: {{item.title}} results in Group: edoras one User being presented to the user for this JSON option.

Navigation URL

Used to set a link icon to the left of each option in the list and each selected option. The Navigation URL defines the URL to navigate to when the user clicks a link icon.

The navigation URL attribute can be expressed as a static value or as a frontend expression. See Frontend expressions to learn the frontend expression syntax.

The prefix to select an attribute from the JSON option is $item.

So if the JSON option looks like

{
  "id" : "GEAR-c641f3bb-876e-45f4-9550-ef95ef5a2fac",
  "title" : "edoras one User",
  "value" : "GEAR-c641f3bb-876e-45f4-9550-ef95ef5a2fac"
}

then a navigation URL of /GRP/{{$item.id}}/browse results in /GRP/GEAR-3456061f-4892-4ef4-b234-69217201cd09/browse being invoked when the user clicks the link icon.

formComponentAutocompleteRuntimeLink

Target

Used to define the tab where the linked option is loaded.

Possible values are:

  • _blank ⇒ Load in a new tab

  • _self ⇒ Load in the same tab as it was clicked

  • tabname ⇒ Load in a named tab, if there is no tab with that name then a new tab is created with that name

Multi selection

False if only one option can be selected, true if multiple options can be selected.

If true then the selected options are stored as a list in the data model.

formComponentAutocompleteRuntimeMultiple

Preselect all

If true, ALL objects returned by the REST endpoint are automatically selected. This only works, if multi selection is turned on too.

Autocomplete select attributes
Attribute name Description

Common

See Common attributes to learn more about the common attributes.

Validation

The autocomplete select form widget supports the following validations:

  • Required.

  • Invalid selection.

  • Minimum number of elements.

  • Maximum number of elements.

See Validation attributes to learn more about validation.

Query

The edoras one query that is used to load the available work items that are presented to the user.

The user typed text is added as additional filter to the configured query.

Typical queries:

type:CAS for:me

all cases that are assigned to me

type:TSK for:me,unassigned

all tasks that are either directly assigned to me or are unassigned

type:CAS app:foo

all cases that belong to the foo App

See section Searches in the User guide to learn more about the search syntax and about all possible search terms.

Format

Used to compose a user friendly string from the work item.

The prefix to select an variable from the work item item. Literals have to be enclosed in quotes, attributes and literals can be concatenated with +.

So if the format is Group: {{item.name}} then the work item is shown to the user as Group: edoras one User.

Navigation view

Used to set the target view for the link.

Possible values are:

  • Browse ⇒ The link points to the form view of the work item

  • Preview ⇒ The link points to the preview view of the work item

Target

Used to define the tab where the linked work item is loaded.

Possible values are:

  • _blank ⇒ Load in a new tab

  • _self ⇒ Load in the same tab as it was clicked

  • tabname ⇒ Load in a named tab, if there is no tab with that name then a new tab is created with that name

Multi selection

False if only one option can be selected, true if multiple options can be selected.

If true then the selected options are stored as a list in the data model.

formComponentAutocompleteRuntimeMultiple

Preselect all

If true, ALL objects returned by the query are automatically selected. This only works, if multi selection is turned on too.

Static autocomplete select attributes
Attribute name Description

Common

See Common attributes to learn more about the common attributes.

Validation

The static autocomplete select form widget supports the following validations:

  • Required.

  • Invalid selection.

  • Minimum number of elements.

  • Maximum number of elements.

See Validation attributes to learn more about validation.

Options

Defines the options from which the user can choose. Each option has a name and a value: the name is shown to the user whereas the value is stored in the data model.

formComponentOptions

Navigation URL

Used to set a link icon to the left of each option in the list and each selected option. The Navigation URL defines the URL to navigate to when the user clicks a link icon.

The navigation URL attribute can be expressed as a static value or as a frontend expression. See Frontend expressions to learn the frontend expression syntax.

The prefix to select an attribute from the option is $item. The following two option attributes are available:

name

the name that is shown to the user

value

the value that is stored in the data model

So if the navigation URL is https://www.google.ch/search?q={{$item.value}} then the link URL results in https://www.google.ch/search?q=edorasware.

formComponentAutocompleteRuntimeLink

Multi selection

False if only one option can be selected, true if multiple options can be selected.

If true then the selected options are stored as a list in the data model.

formComponentAutocompleteRuntimeMultiple

Preselect all

Preselects all the available options. It only applies if no options are selected by default.

Static select attributes
Attribute name Description

Common

See Common attributes to learn more about the common attributes.

Validation

The static select form widget supports the required and the invalid selection validation.

See Validation attributes to learn more about validation.

Options

Defines the options from which the user can choose. Each option has a name and a value: the name is shown to the user whereas the value is stored in the data model.

formComponentOptions

3.5.6. Read only form widgets

The read only form widgets allow to present information, to add navigation functionality, and to split a form into section.

Form widget Description
formComponentOutputTextArea

Used to show static rich text.

The rich text can be formatted in various ways: regular, bold, italic, ordered list, unordered list, headings, and much more.

formComponentOutputTextAreaRuntime
formComponentHTMLComponent

Used to render html code.

formComponentListRest

Used to show a list of options that are returned by a custom REST endpoint.

The dynamic list of options is build at runtime by calling a custom REST endpoint that the modeler has configured. The rest list form widget can be used to enable simple navigation inside your App. For this the modeler can configure the links that are followed when the user clicks one of the options in the list.

formComponentListRestRuntime
formComponentList

Used to show a list of work items that are returned by edoras one.

The dynamic list of work items is build at runtime by calling calling edoras one with a query that the modeler has configured. The rest list form widget can be used to enable simple navigation inside your App. For this the modeler can configure the links that are followed when the user clicks one of the options in the list.

formComponentImage

Used to show an image.

formComponentImageRuntime
formComponentLink

Used to show a generic link.

formComponentLinkRuntime
formComponentCreateLink

A link which navigates to a specific initialization form for a new work item to be created based on a given model you can select in the work item model attribute

formComponentCreateLinkRuntime
formComponentSearchLink

Used to show a link that navigates to a list of work items returned by a search using a specified query.

formComponentSearchCountLink

A link which renders a text including the numbers of elements returned by a count query you can specify.

The link text can be configured to show the number of elements returned by the query. Clicking the link navigates to the result of the query.

formComponentCreateCountLinkRuntime
formComponentRestSearchCountLink

A link which renders a text including the numbers of elements returned by a REST endpoint.

The link text can be configured to show the number of elements returned by the REST query. Clicking the link navigates to the result of the REST query.

formComponentLinkQueryRestRuntime
formComponentButton

Used to show a button that points to a generic URL.

formComponentLinkButtonRuntime
formComponentCreateButton

A button with a link which navigates to a specific initialization form for a new work item to be created based on a given model you can select in the work item model attribute.

formComponentDynamicLinkButton

A button which calls a REST endpoint to obtain dynamic navigation information. When pressed, the REST endpoint will be invoked and the response used to navigate to a specific work item view.

formComponentRestButton

A button which calls a REST endpoint and loads the resulting JSON object into the bound variable.

formComponentButtonGroup

A container that can only contain button widgets and has properties which affect the group of buttons contained.

formComponentButtonGroupRuntime
formComponentHorizontalLine

Used to show a horizontal line.

The Horizontal line form widget is useful to structure large forms by splitting the form into sections, e.g. basic information, detail information, address, …​

formComponentHorizontalLineRuntime
formComponentIframe

A view to display content from a given source through an iframe.

formComponentPdfPreview

Used to preview documents as pdf. It can preview formats like doc or txt in addition to pdf.

Note
the functionality of the REST list and the List form widgets is similar. The only difference is that the REST list form widget uses a custom REST endpoint to load the displayed items (which can be of any format) whereas the List form widget uses edoras one to load the displayed work items.
Note
the functionality of the REST query link and the Query link form widget is similar. The only difference is that the REST query link form widget uses a custom REST endpoint to load the number of found elements whereas the Query link form widget uses edoras one to load the number of found elements.
Note
the functionality of the Link and the Link button as well as the Query link and the Query link button is similar. The only difference is that the Link and the Query link form widgets are rendered as links whereas the Link button and the Query link button form widgets are rendered as buttons.
Output text area attributes
Attribute name Description

Common

See Common attributes to learn more about the common attributes.

The Output text area form widget does not support the Default value and the Editable attribute, as these do not make sense for read only widgets.

The Value attribute of a Output text area form widget is rich text which supports the following formatting options (see toolbar buttons in the image below): bold, italic, and underline text style, font size, foreground and background color, left, center, and right text alignment, headline, sub-headline, and section headline style, ordered and unordered list, text indentation, horizontal line, links.

formComponentOutputTextAreaValue
HTML attributes
Attribute name Description

Common

See Common attributes to learn more about the common attributes.

The HTML widget does not support the value, default value and the editable attributes, as these do not make sense for read only widgets.

Content

Defines the content of the html.

The "Content" attribute of an HTML widget is a plain text for writing html code. It may include front-end expressions.

Border

Defines if it will show a wrapper border.

REST list attributes
Attribute name Description

Common

See Common attributes to learn more about the common attributes.

The REST list form widget does not support the Value, the Default value and the Editable attribute, as these do not make sense for read only widgets.

Query URL

Used to load the items that are presented to the user from a URL.

The query URL attribute can be expressed as a static value or as a frontend expression. See Frontend expressions to learn the frontend expression syntax.

The list form widget expects the items to be returned as list of JSON objects, e.g.

----
[ {
  "id" : "GEAR-a65c4a12-20b4-45d8-bb09-2105be7f0d1f",
  "title" : "edoras one Modeler",
  "value" : "GEAR-a65c4a12-20b4-45d8-bb09-2105be7f0d1f"
}, {
  "id" : "GEAR-c641f3bb-876e-45f4-9550-ef95ef5a2fac",
  "title" : "edoras one User",
  "value" : "GEAR-c641f3bb-876e-45f4-9550-ef95ef5a2fac"
} ]
----
Note
Referencing external resources may have problems due to SSL (https) or CORS security browser limitations. To solve CORS security, the resource server must send appropriate response headers. Another option is using a proxy to receive the right response headers from any url e.g .:https://cors-anywhere.herokuapp.com. To solve SSL if edoras one is being executed over https, the url of the external resource should also have the https protocol.

List title

Used to define the title that is shown in the header of the list.

The list title attribute can be expressed as a static value or as a frontend expression. See Frontend expressions to learn the frontend expression syntax.

Format

Used to compose a user friendly string from the JSON item.

The prefix to select an attribute from the JSON item is item. Literals have to be enclosed in quotes, attributes and literals can be concatenated with +.

So if the JSON item looks like

{
  "id" : "GEAR-c641f3bb-876e-45f4-9550-ef95ef5a2fac",
  "title" : "edoras one User",
  "value" : "GEAR-c641f3bb-876e-45f4-9550-ef95ef5a2fac"
}

then a format of Group: {{item.title}} results in Group: edoras one User being presented to the user for this JSON item.

Minimum number of visible items

Used to define the minimum height of the list.

The list height is forced to a height that can show at least a certain number of items. If the concrete list has less items then the missing space is filled with empty items. For example if there are 5 items to show and the minimum height is 10 then the list height is forced to a height that can show 10 items.

formComponentListRestRuntimeMinimumHeight

Maximum number of visible items

Used to define the maximum height of the list.

The list height is forced to a height that can show at most a certain number of items. If the concrete list has more items then the list shows a scroll with which the user can scroll in the list. For example if there are 15 items to show and the minimum height is 10 then the list height is forced to a height that can show 10 items.

formComponentListRestRuntime

Navigation URL

Used to define the URL to navigate to when the user clicks an item in the list.

The navigation URL attribute can be expressed as a static value or as a frontend expression. See Frontend expressions to learn the frontend expression syntax.

The prefix to select an attribute from the JSON option is $item.

So if the JSON item looks like

{
  "id" : "GEAR-c641f3bb-876e-45f4-9550-ef95ef5a2fac",
  "title" : "edoras one User",
  "value" : "GEAR-c641f3bb-876e-45f4-9550-ef95ef5a2fac"
}

then a navigation URL of /GRP/{{$item.id}}/browse results in /GRP/GEAR-3456061f-4892-4ef4-b234-69217201cd09/browse being invoked when the user clicks the item in the list.

Target

Used to define the tab where the linked item is loaded.

Possible values are:

  • _blank ⇒ Load in a new tab

  • _self ⇒ Load in the same tab as it was clicked

  • tabname ⇒ Load in a named tab, if there is no tab with that name then a new tab is created with that name

List attributes
Attribute name Description

Common

See Common attributes to learn more about the common attributes.

The List form widget does not support the Value, the Default value and the Editable attribute, as these do not make sense for read only widgets.

Query

The edoras one query that is used to load the work items for the list.

Typical queries:

type:CAS for:me

all cases that are assigned to me

type:TSK for:me,unassigned

all tasks that are either directly assigned to me or are unassigned

type:CAS app:foo

all cases that belong to the foo App

See section Searches in the User guide to learn more about the search syntax and about all possible search terms.

List title

Used to define the title that is shown in the header of the list.

The list title attribute can be expressed as a static value or as a frontend expression. See Frontend expressions to learn the frontend expression syntax.

Format

Used to compose a user friendly string from the work item.

The prefix to select an variable from the work item item. Literals have to be enclosed in quotes, attributes and literals can be concatenated with +.

So if the format is Group: {{item.name}} then the work item is shown to the user as Group: edoras one User.

Minimum number of visible items

Used to define the minimum height of the list.

The list height is forced to a height that can show at least a certain number of items. If the concrete list has less items then the missing space is filled with empty items. For example if there are 5 items to show and the minimum height is 10 then the list height is forced to a height that can show 10 items.

formComponentListRestRuntimeMinimumHeight

Maximum number of visible items

Used to define the maximum height of the list.

The list height is forced to a height that can show at most a certain number of items. If the concrete list has more items then the list shows a scroll with which the user can scroll in the list. For example if there are 15 items to show and the minimum height is 10 then the list height is forced to a height that can show 10 items.

formComponentListRestRuntime

Navigation view

Used to set the target view for the link.

Possible values are:

  • Browse ⇒ The link points to the form view of the work item

  • Preview ⇒ The link points to the preview view of the work item

Target

Used to define the tab where the linked work item is loaded.

Possible values are:

  • _blank ⇒ Load in a new tab

  • _self ⇒ Load in the same tab as it was clicked

  • tabname ⇒ Load in a named tab, if there is no tab with that name then a new tab is created with that name

Image attributes
Attribute name Description

Common

See Common attributes to learn more about the common attributes.

The Image form widget does not support the Value, Default value and the Editable attribute, as these do not make sense for read only widgets.

Source URL

Used to define the URL from which the image is loaded.

The source URL attribute can be expressed as a static value or as a frontend expression. See Frontend expressions to learn the frontend expression syntax.

This can be an absolute URL like http://www.activiti.org/images/edorasware_logo.png or a relative URL that points to the content of a document work item, for example rest/workobjects/GEAR-faded1c0-a147-4fb1-9875-e13b185b0abe/content.

Maximum height

Used to define the maximum height of the image.

If set then the height of the Image form widget is forced to that height, which means the contained image is scaled down to that height. If not set then the height of the Image form widget is equals to the height of the contained image.

Refresh time (seconds)

Used to define a refresh internal in seconds.

The image is reloaded every x seconds, where x is the defined refresh time. This is useful if the Source URL attribute point to a dynamic image that can change over time.

Navigation URL

Used to define the URL to navigate to when the user clicks the image.

The navigation URL attribute can be expressed as a static value or as a frontend expression. See Frontend expressions to learn the frontend expression syntax.

Target

Used to define the tab where image is loaded.

Possible values are:

  • _blank ⇒ Load in a new tab

  • _self ⇒ Load in the same tab as it was clicked

  • tabname ⇒ Load in a named tab, if there is no tab with that name then a new tab is created with that name

Attribute name Description

Common

See Common attributes to learn more about the common attributes.

The Link form widget does not support the Value, Default value and the Editable attribute, as these do not make sense for read only widgets.

Link text

The attribute is used to define the link text.

Navigation URL

Used to define the URL to navigate to when the user clicks the link.

The navigation URL attribute can be expressed as a static value or as a frontend expression. See Frontend expressions to learn the frontend expression syntax.

Context variables

Parameters that can be added to the URL linked. A dialog is opened where the name and the value of the parameters are specified. The value of the parameter can be a frontend-expression. See Frontend expressions to learn the frontend expression syntax.

formComponentLinkContextVariables

An special parameter is "forwardTo", which defines the url where we will be redirected after executing the submit action of the page where we navigate to.

Tooltip

Used to define the tooltip, that is, a text which will appear when the mouse enters in the widget.

The tooltip attribute can be expressed as a static value or as a frontend expression. See Frontend expressions to learn the frontend expression syntax.

Target

Used to define the tab where the linked item is loaded.

Possible values are:

  • _blank ⇒ Load in a new tab

  • _self ⇒ Load in the same tab as it was clicked

  • tabname ⇒ Load in a named tab, if there is no tab with that name then a new tab is created with that name

Attribute name Description

Common

See Common attributes to learn more about the common attributes.

The Create Link form widget does not support the Value, Default value and the Editable attribute, as these do not make sense for read only widgets.

Link text

The attribute is used to define the link text.

Work item type

Type of the work item you want to create. The items can be Case, Task, Document or Process. The attribute can be set by a frontend-expression.See Frontend expressions to learn the frontend expression syntax.

Work item model

Select the models available to create, depending on the Work item type selected. The attribute can be set by a frontend-expression. See Frontend expressions to learn the frontend expression syntax.

Context variables

Parameters that can be added to the URL linked. A dialog is opened where the name and the value of the parameters are specified. The value of the parameter can be a frontend-expression. See Frontend expressions to learn the frontend expression syntax.

formComponentLinkContextVariables

An special parameter is "forwardTo", which defines the url where we will be redirected after executing the submit action of the page where we navigate to.

Hide selectors

If its value is true, the template and parent selectors will be hidden. This only happens if the parameters (modelId and parentId) of the creation page where we are going to navigate are defined in the url.

Tooltip

Used to define the tooltip, that is, a text which will appear when the mouse enters in the widget.

The tooltip attribute can be expressed as a static value or as a frontend expression. See Frontend expressions to learn the frontend expression syntax.

Target

Used to define the tab where the linked item is loaded.

Possible values are:

  • _blank ⇒ Load in a new tab

  • _self ⇒ Load in the same tab as it was clicked

  • tabname ⇒ Load in a named tab, if there is no tab with that name then a new tab is created with that name

Attribute name Description

Common

See Common attributes to learn more about the common attributes.

The Search Link form widget does not support the Value, Default value and the Editable attribute, as these do not make sense for read only widgets.

Link text

The attribute is used to define the link text.

Query

The edoras one query that is used to load the work items for the list.

Typical queries:

type:CAS for:me

all cases that are assigned to me

type:TSK for:me,unassigned

all tasks that are either directly assigned to me or are unassigned

type:CAS app:foo

all cases that belong to the foo App

See section Searches in the User guide to learn more about the search syntax and about all possible search terms.

Show as button

If it’s wanted to show the link as a button

Tooltip

Used to define the tooltip, that is, a text which will appear when the mouse enters in the widget.

The tooltip attribute can be expressed as a static value or as a frontend expression. See Frontend expressions to learn the frontend expression syntax.

Target

Used to define the tab where the linked item is loaded.

Possible values are:

  • _blank ⇒ Load in a new tab

  • _self ⇒ Load in the same tab as it was clicked

  • tabname ⇒ Load in a named tab, if there is no tab with that name then a new tab is created with that name

Attribute name Description

Common

See Common attributes to learn more about the common attributes.

The Link form widget does not support the Value, Default value and the Editable attribute, as these do not make sense for read only widgets.

Link text

The attribute is used to define the link text.

The Link text attribute attribute can be expressed as a static value or as a frontend expression. See [frontend-expressions] to learn the frontend expression syntax. The value attribute supports the special $count frontend expression which represents the result count of the query. The value attribute as well supports different texts for singular and plural depending on the result counts. The syntax for this is [singular text|plural text].

So a value of $count case [|s] results in 1 case or 21 cases depending on the result count.

Query

The edoras one query that is used to load the work items for the list.

Typical queries:

type:CAS for:me

all cases that are assigned to me

type:TSK for:me,unassigned

all tasks that are either directly assigned to me or are unassigned

type:CAS app:foo

all cases that belong to the foo App

See section Searches in the User guide to learn more about the search syntax and about all possible search terms.

Tooltip

Used to define the tooltip, that is, a text which will appear when the mouse enters in the widget.

The tooltip attribute can be expressed as a static value or as a frontend expression. See Frontend expressions to learn the frontend expression syntax.

Target

Used to define the tab where the linked item is loaded.

Possible values are:

  • _blank ⇒ Load in a new tab

  • _self ⇒ Load in the same tab as it was clicked

  • tabname ⇒ Load in a named tab, if there is no tab with that name then a new tab is created with that name

Attribute name Description

Common

See Common attributes to learn more about the common attributes.

The Link form widget does not support the Value, Default value and the Editable attribute, as these do not make sense for read only widgets.

Link text

The attribute is used to define the link text.

The Link text attribute attribute can be expressed as a static value or as a frontend expression. See [frontend-expressions] to learn the frontend expression syntax. The value attribute supports the special $count frontend expression which represents the result count of the REST query. The value attribute as well supports different texts for singular and plural depending on the result counts. The syntax for this is [singular text|plural text].

So a value of $count user [|s] results in 1 user or 21 users depending on the result count.

Count URL

Defines the REST URL that returns the count result (as a numerical value).

The count URL attribute can be expressed as a static value or as a frontend expression. See Frontend expressions to learn the frontend expression syntax.

Note
Referencing external resources may have problems due to SSL (https) or CORS security browser limitations. To solve CORS security, the resource server must send appropriate response headers. Another option is using a proxy to receive the right response headers from any url e.g .:https://cors-anywhere.herokuapp.com. To solve SSL if edoras one is being executed over https, the url of the external resource should also have the https protocol.

Navigation URL

Used to define the URL to navigate to when the user clicks the link.

The navigation URL attribute can be expressed as a static value or as a frontend expression. See Frontend expressions to learn the frontend expression syntax.

Tooltip

Used to define the tooltip, that is, a text which will appear when the mouse enters in the widget.

The tooltip attribute can be expressed as a static value or as a frontend expression. See Frontend expressions to learn the frontend expression syntax.

Target

Used to define the tab where the linked item is loaded.

Possible values are:

  • _blank ⇒ Load in a new tab

  • _self ⇒ Load in the same tab as it was clicked

  • tabname ⇒ Load in a named tab, if there is no tab with that name then a new tab is created with that name

Button attributes
Attribute name Description

Common

See Common attributes to learn more about the common attributes.

The Button form widget does not support the Value, Default value and the Editable attribute, as these do not make sense for read only widgets.

Button text

The attribute is used to define the button text.

The Button text can be expressed as a static value or as a frontend expression. See [frontend-expressions] to learn the frontend expression syntax.

Navigation URL

Used to define the URL to navigate to when the user clicks the button.

The navigation URL attribute can be expressed as a static value or as a frontend expression. See Frontend expressions to learn the frontend expression syntax.

Context variables

Parameters that can be added to the URL linked. A dialog is opened where the name and the value of the parameters are specified. The value of the parameter can be a frontend-expression. See Frontend expressions to learn the frontend expression syntax.

formComponentLinkContextVariables

An special parameter is "forwardTo", which defines the url where we will be redirected after executing the submit action of the page where we navigate to.

Tooltip

Used to define the tooltip, that is, a text which will appear when the mouse enters in the widget.

The tooltip attribute can be expressed as a static value or as a frontend expression. See Frontend expressions to learn the frontend expression syntax.

Target

Used to define the tab where the linked item is loaded.

Possible values are:

  • _blank ⇒ Load in a new tab

  • _self ⇒ Load in the same tab as it was clicked

  • tabname ⇒ Load in a named tab, if there is no tab with that name then a new tab is created with that name

Button alignment

The alignment of the button can be Left or Right.

Icon url

The url of an image, which will be displayed inside the button as an icon.

Icon alignment

The alignment of the icon inside the button. Can be Left or Right.

Create Button attributes
Attribute name Description

Common

See Common attributes to learn more about the common attributes.

The Create button form widget does not support the Value, Default value and the Editable attribute, as these do not make sense for read only widgets.

Button text

The attribute is used to define the button text.

The Button text can be expressed as a static value or as a frontend expression. See [frontend-expressions] to learn the frontend expression syntax.

Work item type

Type of the work item you want to create. The items can be Case, Task, Document or Process. The attribute can be set by a frontend-expression.See Frontend expressions to learn the frontend expression syntax.

Work item model

Select the models available to create, depending on the Work item type selected. The attribute can be set by a frontend-expression. See Frontend expressions to learn the frontend expression syntax.

Context variables

Parameters that can be added to the URL linked. A dialog is opened where the name and the value of the parameters are specified. The value of the parameter can be a frontend-expression. See Frontend expressions to learn the frontend expression syntax.

formComponentLinkContextVariables

An special parameter is "forwardTo", which defines the url where we will be redirected after executing the submit action of the page where we navigate to.

Hide selectors

If its value is true, the template and parent selectors will be hidden. This only happens if the parameters (modelId and parentId) of the creation page where we are going to navigate are defined in the url.

Tooltip

Used to define the tooltip, that is, a text which will appear when the mouse enters in the widget.

The tooltip attribute can be expressed as a static value or as a frontend expression. See Frontend expressions to learn the frontend expression syntax.

Target

Used to define the tab where the linked item is loaded.

Possible values are:

  • _blank ⇒ Load in a new tab

  • _self ⇒ Load in the same tab as it was clicked

  • tabname ⇒ Load in a named tab, if there is no tab with that name then a new tab is created with that name

Button alignment

The alignment of the button can be Left or Right.

Icon url

The url of an image, which will be displayed inside the button as an icon.

Icon alignment

The alignment of the icon inside the button. Can be Left or Right.

Inline Iframe attributes
Attribute Description

Common

See Common attributes to learn more about the common attributes.

Source URL

The URL of the source to be displayed in the iframe, might also contain expressions.

Height

Used to define the maximum height of the iframe in pixels.

Show border

If is set to true, it renders a border around the iFrame.

Scrolling type

If is set to true, a scrolling bar is attached to the iframe or not. The possible values are:

  • Auto: the scrolling bar will appear just if the content of the iframe is taller than its height.

  • Yes: the scrolling bar will always appear.

  • No: the scrolling bar will never appear, so part of the iframe could be potentially hidden.

Pdf preview attributes
Attribute name Description

Common

See Common attributes to learn more about the common attributes.

The Pdf preview form widget does not support the Default value and the Editable attribute, as these do not make sense for read only widgets.

Document source type

Possible values are:

  • id ⇒ The document source is the application, so the document is internal.

  • url ⇒ The document source is an external url.

Document source

It depends on the prevous attribute, the Document source type:

  • id ⇒ The id of the document, could be a frontend expression or the id document.

  • url ⇒ An external url should be provided.

Note
Referencing external resources may have problems due to SSL (https) or CORS security browser limitations. To solve CORS security, the resource server must send appropriate response headers. Another option is using a proxy to receive the right response headers from any url e.g .:https://cors-anywhere.herokuapp.com. To solve SSL if edoras one is being executed over https, the url of the external resource should also have the https protocol.

Height

Defines the height of the pdf preview.

Show border

If is set to true, the pdf preview will have a wrapper border.

Button group attributes
Attribute name Description

Common

See Common attributes to learn more about the common attributes.

The Button group form widget does not support the Default value and the Editable attribute, as these do not make sense for read only widgets.

Button alignment

The alignment of the button group. It can be Right, Left or Center.

Horizontal line attributes
Attribute name Description

Common

See Common attributes to learn more about the common attributes.

The Label attribute is used to define the title that is displayed inside the horizontal line.

The Horizontal line form widget does not support the Default value and the Editable attribute, as these do not make sense for read only widgets.

3.5.7. Executable form widgets

The execution form widgets executes actions and saves the result in a variable.

Form widget Description
formComponentDynamicLinkButton

A button which calls an endpoint and redirects to a workitem view depending on the result.

formComponentSearchButton

Button which executes a query and stores the result in a variable. In combination with a sub form, this allows to create search-like forms. If the button is clicked, the search is executed, the result stored in the variable and the subform will show the entries accordingly. If the query is using expression bound to search fields, you can create search forms this way.

formComponentSearchButtonRuntime
formComponentRestButton

A button which makes a call to an url and sets the result in the model, with timer interval capability.

formComponentScriptButton

A button which saves in a variable the result of a script.

formComponentSelectionScriptButton

A button which saves in a variable the result of a script and shows a text based on the result.

Attribute name Description

Common

See Common attributes to learn more about the common attributes.

The Dynamic Link Button form widget does not support the Value, Default value and the Editable attribute, as these do not make sense for read only widgets.

Button text

The attribute is used to define the button text.

The Button text can be expressed as a static value or as a frontend expression. See [frontend-expressions] to learn the frontend expression syntax.

REST url

The REST url where the navigation information is retrieved. The response from the REST endpoint should have the following structure:

{
    "id" : "GEAR-35354f5d-1004-460a-8d53-88a04e0f22c1",
    "type" : "TSK",
    "view" : "browse"
}

Where id is a work object ID, type is the corresponding work object type and view is the name of the work object view that should be shown.

Note
Referencing external resources may have problems due to SSL (https) or CORS security browser limitations. To solve CORS security, the resource server must send appropriate response headers. Another option is using a proxy to receive the right response headers from any url e.g .:https://cors-anywhere.herokuapp.com. To solve SSL if edoras one is being executed over https, the url of the external resource should also have the https protocol.

Target view

Define the view where will be redirected if the view is not defined in the REST url. If any is defined it will be redirected to Browse view by default.

Context variables

Parameters that can be added to the URL linked. A dialog is opened where the name and the value of the parameters are specified. The value of the parameter can be a frontend-expression. See Frontend expressions to learn the frontend expression syntax.

formComponentLinkContextVariables

An special parameter is "forwardTo", which defines the url where we will be redirected after executing the submit action of the page where we navigate to.

Tooltip

Used to define the tooltip, that is, a text which will appear when the mouse enters in the widget.

The tooltip attribute can be expressed as a static value or as a frontend expression. See Frontend expressions to learn the frontend expression syntax.

Target

Used to define the tab where the linked item is loaded.

Possible values are:

  • _blank ⇒ Load in a new tab

  • _self ⇒ Load in the same tab as it was clicked

  • tabname ⇒ Load in a named tab, if there is no tab with that name then a new tab is created with that name

Button alignment

The alignment of the button can be Left or Right.

Icon url

The url of an image, which will be displayed inside the button as an icon.

Icon alignment

The alignment of the icon inside the button. Can be Left or Right.

Search button attributes
Attribute name Description

Common

See Common attributes to learn more about the common attributes.

The Search button form widget does not support the Value, Default value and the Editable attribute, as these do not make sense for read only widgets.

Button text

The attribute is used to define the button text.

The Button text can be expressed as a static value or as a frontend expression. See [frontend-expressions] to learn the frontend expression syntax.

Value

Is used to store the query result once executed (e.g. '{{searchResult}}'). You can bind the same variable to a subform for instance in order to render the result returned by the query.

Query

The edoras one query that is used to load the work items for the list.

Typical queries:

type:CAS for:me

all cases that are assigned to me

type:TSK for:me,unassigned

all tasks that are either directly assigned to me or are unassigned

type:CAS app:foo

all cases that belong to the foo App

See section Searches in the User guide to learn more about the search syntax and about all possible search terms.

Max result size

The maximum number of items being returned by the query in order to limit the result, might even be an expression. See Frontend expressions to learn the frontend expression syntax.

Auto execute

Automatically executes the query during initialization of the form, if set to 'true', might even be an expression. See Frontend expressions to learn the frontend expression syntax.

Tooltip

Used to define the tooltip, that is, a text which will appear when the mouse enters in the widget.

The tooltip attribute can be expressed as a static value or as a frontend expression. See Frontend expressions to learn the frontend expression syntax.

Target

Used to define the tab where the linked item is loaded.

Possible values are:

  • _blank ⇒ Load in a new tab

  • _self ⇒ Load in the same tab as it was clicked

  • tabname ⇒ Load in a named tab, if there is no tab with that name then a new tab is created with that name

Button alignment

The alignment of the button can be Left or Right.

Icon url

The url of an image, which will be displayed inside the button as an icon.

Icon alignment

The alignment of the icon inside the button. Can be Left or Right.

REST button attributes
Attribute name Description

Common

See Common attributes to learn more about the common attributes.

The REST button form widget does not support the Default value and the Editable attribute, as these do not make sense for read only widgets.

Button text

The attribute is used to define the button text.

The Button text can be expressed as a static value or as a frontend expression. See [frontend-expressions] to learn the frontend expression syntax.

Value

Is used to store the REST endpoint result once executed (e.g. '{{result}}'). You can bind the same variable to a subform for instance in order to render the result returned by the REST endpoint.

REST url

Define the URL of the REST endpoint which returns the data.

Note
Referencing external resources may have problems due to SSL (https) or CORS security browser limitations. To solve CORS security, the resource server must send appropriate response headers. Another option is using a proxy to receive the right response headers from any url e.g .:https://cors-anywhere.herokuapp.com. To solve SSL if edoras one is being executed over https, the url of the external resource should also have the https protocol.

Refresh time

The milliseconds interval to execute the button automatically in every tick. If it’s not defined it will not be executed automatically.

Auto execute

Automatically executes the rest request during initialization of the form, if set to 'true', might even be an expression. See Frontend expressions to learn the frontend expression syntax.

Tooltip

Used to define the tooltip, that is, a text which will appear when the mouse enters in the widget.

The tooltip attribute can be expressed as a static value or as a frontend expression. See Frontend expressions to learn the frontend expression syntax.

Target

Used to define the tab where the linked item is loaded.

Possible values are:

  • _blank ⇒ Load in a new tab

  • _self ⇒ Load in the same tab as it was clicked

  • tabname ⇒ Load in a named tab, if there is no tab with that name then a new tab is created with that name

Button alignment

The alignment of the button can be Left or Right.

Icon url

The url of an image, which will be displayed inside the button as an icon.

Icon alignment

The alignment of the icon inside the button. Can be Left or Right.

Script button attributes
Attribute name Description

Common

See Common attributes to learn more about the common attributes.

The Script button form widget does not support the Default value and the Editable attribute, as these do not make sense for read only widgets.

Button text

The attribute is used to define the button text.

The Button text can be expressed as a static value or as a frontend expression. See [frontend-expressions] to learn the frontend expression syntax.

Value

Is used to store the returned value of the script once executed (e.g. '{{scriptResult}}'). e.g It’s possible to bind the variable to a text widget in order to render the result returned by the script.

Script

The script which will be executed.

Inside the script scope is possible to access to a variable called elementId which have the value of the id attribute of the script button.

Is possible to use some util methods or properties:

  • returnAsyncValue (Function): you can execute this function a save the value asynchronously. For example when in the script we do an AJAX call.

  • elementId (String): the attribute id of the clicked button.

  • element (Object): the button clicked.

Refresh time

The milliseconds interval to execute the button automatically in every tick. When is empty, it will not be executed automatically.

Auto execute

If it is set to 'true', the script is executed automatically during initialization of the form. It might even be an expression. See Frontend expressions to learn the frontend expression syntax.

Tooltip

Used to define the tooltip, that is, a text which will appear when the mouse enters in the widget.

The tooltip attribute can be expressed as a static value or as a frontend expression. See Frontend expressions to learn the frontend expression syntax.

Button alignment

The alignment of the button can be Left or Right.

Icon url

The url of an image, which will be displayed inside the button as an icon.

Icon alignment

The alignment of the icon inside the button. Can be Left or Right.

Selection script button attributes
Attribute name Description

Common

See Common attributes to learn more about the common attributes.

The Selection Script button form widget does not support the Default value and the Editable attribute, as these do not make sense for read only widgets.

Button text

The attribute is used to define the button text.

The Button text can be expressed as a static value or as a frontend expression. See Frontend expressions to learn the frontend expression syntax.

Value

Is used to store the returned value of the script once executed (e.g. '{{scriptResult}}').

Script

The script which will be executed.

Inside the script scope is possible to access to a variable called elementId which have the value of the id attribute of the script button.

Is possible to use some util methods or properties:

  • returnAsyncValue (Function): you can execute this function a save the value asynchronously. For example when in the script we do an AJAX call.

  • elementId (String): the attribute id of the clicked button.

  • element (Object): the button clicked.

Refresh time

The milliseconds interval to execute the button automatically in every tick. When is empty, it will not be executed automatically.

Auto execute

If it is set to 'true', the script is executed automatically during initialization of the form. It might even be an expression. See Frontend expressions to learn the frontend expression syntax.

Result format

Shows the result of the script in a text field. If the result is an object saved in the 'result variable', it is possible to display its properties by setting it to {{scriptResult .anyProperty}}

Tooltip

Used to define the tooltip, that is, a text which will appear when the mouse enters in the widget.

The tooltip attribute can be expressed as a static value or as a frontend expression. See Frontend expressions to learn the frontend expression syntax.

3.5.8. Nesting form widgets

The nesting form widgets allow to nest widgets.

The data model of a nested form is usually stored in a nested data context. If configured that way the forms and the data model nest in a corresponding way: A field in a nested form then becomes a attribute in the nested data context.

Nested forms can be repeated. This way it is possible to define lists in the data model.

Usually nested forms are rendered in a way that the user can clearly see that the form fields belong to a nested form. But if desired nested forms can be rendered in a transparent way. The user can then not tell if a field comes from the main form or from a nested form.

Form widget Description
formComponentSingleTypeSubform

Used to nest forms of a single type.

formComponentSingleTypeSubformRuntime
formComponentMultiTypeSubform

Used to nest forms of multiple types.

formComponentMultiTypeSubformRuntime
Single-type subform attributes
Attribute Description

Form reference

Used to define the form that is rendered in place of the subform.

The editor for this attribute allows to pick an existing form or to create a new one.

formComponentFormRef

Element name

Used to define the text for the add button.

Common

See Common attributes to learn more about the common attributes.

The Value attribute is used to define the nested data context. This means if the Value is set to foo then all data inside the subform is stored in the foo attribute of the data model.

This widget does not support the Label position, the Default value, and the Description attribute, as these do not make sense for nesting widgets.

Validation

This widget supports the minimum number of children and the maximum number of children validation.

See Validation attributes to learn more about validation.

Multiple elements

Used to define if the user can repeat the form or not.

If true, additional buttons are rendered to let the user add new forms and remove existing ones.

formComponentSingleTypeSubformRuntimeRepeating

Show border

Used to define if the form inside the subform form widget is rendered nested or not.

If true the form is rendered in a nested way and the user can clearly see that this is a nested form. If false the form is rendered in a transparent way and the user cannot distinguish if a field comes form the main form or from the nested form.

formComponentSingleTypeSubformRuntimeTransparent

Show add/remove buttons

If the subform can hold multiple elements, this attribute specifies whether the add/remove buttons should be shown and thus allowing the user to create new elements or to remove existing elements. This can also be done using a dynamic expression.

Collapsible

If true, the subform can be collapsed/expanded by clicking on an arrow ▲/▼.

Collapsed

If true, the subform will appear collapsed by default.

It supports FE expression by writing it in "Collapsed(RT)" field. Using FE expression, if the value of the expression changes, it will be applied to the subform, that is, if it becomes true, the subform will be collapsed and vice versa.

Multi-type subform attributes
Attribute Description

Form references

Used to define the forms that are rendered in place of the subform.

The editor to define the form references asks for a discriminator value, the form reference and display name. The discriminator value identifies the form reference and hence the form to be used inside the data model. The display name is used to define the text for the add button.

The value stored in the discriminator attribute decides which form is used to display the data in the sub data context.

formComponentFormRefs

Common

See Common attributes to learn more about the common attributes.

The Value attribute is used to define the nested data context. This means if the Value is set to foo then all data inside the subform is stored in the foo attribute of the data model.

This widget does not support the Label position, the Default value, and the Description attribute, as these do not make sense for nesting widgets.

Validation

This widget supports the minimum number of children and the maximum number of children validation.

See Validation attributes to learn more about validation.

Multiple elements

Used to define if the user can repeat the form or not.

If true, additional buttons are rendered to let the user add new forms and remove existing ones.

formComponentSingleTypeSubformRuntimeRepeating

Show border

Used to define if the form inside the subform form widget is rendered nested or not.

If true the form is rendered in a nested way and the user can clearly see that this is a nested form. If false the form is rendered in a transparent way and the user cannot distinguish if a field comes form the main form or from the nested form.

formComponentSingleTypeSubformRuntimeTransparent

Show add/remove buttons

If the subform can hold multiple elements, this attribute specifies whether the add/remove buttons should be shown and thus allowing the user to create new elements or to remove existing elements. This can also be done using a dynamic expression.

Collapsible

If true, the subform can be collapsed/expanded by clicking on an arrow ▲/▼.

Collapsed

If true, the subform will appear collapsed by default.

It supports FE expression by writing it in "Collapsed(RT)" field. Using FE expression, if the value of the expression changes, it will be applied to the subform, that is, if it becomes true, the subform will be collapsed and vice versa.

3.5.9. Attachment form widget

formComponentAttachment

The attachment form widget allows to attach files.

formComponentAttachmentRuntime
Attribute Description

Common

See Common attributes to learn more about the common attributes.

Validation

The attachment form widget supports the minimum files and the maximum files validation.

See Validation attributes to learn more about validation.

Preview type

Used to define the preview type.

The following values are allowed:

None

display no preview

Icon

display an icon that identifies the file type as preview

Thumbnail

display a thumbnail of the attached file as preview, the thumbnail is generated in the backend

Thumbnail maximum height

Used to define the maximum height of the thumbnail.

This attribute is only available when the preview type is Thumbnail.

Select file message

Used to define the message that is shown to the user inside the attachment form widget.

By default the attachment form widget shows Please select file to the user.

3.6. Process Palette

This section describes the elements available in the process modelling palette of edoras one. Each modeling element and it’s corresponding attributes are described in detail below. Since edoras one is based on BPMN 2.0 huge parts of the modelling capabilities come from there. Currently, edoras one does not support the full BPMN 2.0 standard for execution. The process palette contains only the BPMN2.0 elements that are currently supported by the engine. Besides the BPMN 2.0 standard elements, edoras one provides some dedicated process activities that can be used within the processes. These edoras one specific activities are dedicated implementations of BPMN 2.0s generic service task. They are implemented using the custom extensions hook of BPMN 2.0 so if you import a process model to another system that supports BPMN 2.0 these activities are considered as generic service tasks.

There are several attributes, that are available for all modelling elements:

Table 17. Common attributes
Attribute Name Description

Name

The name of the element, displayed on the diagram. This attribute is not equal to the id of the element.

Documentation

Use this attribute to add any documentation / description to the element. The content of the element can not be displayed on the model graphics, but is exported to the BPMN 2.0 XML.

Custom Properties

Define arbitrary properties as key value pairs. The attribute is exported to the BPMN 2.0 XML and can be accessed at runtime.

Background Colour

Specify the background colour of the element in the diagram.

Border Colour

Specify the border colour of the element in the diagram.

LoopType

Selection of the loop type. See Multi Instance to know more about loops.

There are several attributes, that are available to customize the font format of all modelling elements in the diagram. If the attributes are set at process level, all the components of the process will contain these attributes by default:

Table 18. Text format attributes
Attribute Name Description

Font size

Specify the font size of the element in the diagram.

Font weight

Specify the font weight of the element in the diagram.

Font style

Specify the font style of the element in the diagram.

Font Color

Specify the font color of the element in the diagram.

These attributes can also be modified by the shortcut with a "T" icon placed in the left-bottom corner of the components. Clicking on this brings up a text format dialog where the formatting can be changed as required. The dialog includes a button to remove the style format and goes back to the default format.

textFormatShortcut

Moreover the process can contain attributes associated to the whole process. In order to set them click in the design area without selecting any element.

Table 19. Process attributes
Attribute Name Description

Init form ref

The reference to a form which will be rendered before the process starts by the 'Start process' action.

Label expression

Specify a label expression for tasks within this process if you want something else being used as the name property.

Custom Properties

Define arbitrary properties as key value pairs. The cutom properties will be saved to a process level.

Is executable

If is set to false the deployment will ignore the process. Once is set to true and the process deployed

Text format attributes

See the table above. If the text format attributes are set at process level, all the components of the process will be created with the format set.

3.6.1. Events

Start Event

A none start event technically means that the trigger for starting the process instance is unspecified. This means that the engine cannot anticipate when the process instance must be started.

startEvent
Figure 8. Graphical Representation
Table 20. Specific attributes
Attribute Name Attribute Type

Description

none

Any sub-process has always to start with a none start event.

Message start event

A message start event can be used to start a process instance using a named message. This effectively allows us to select the right start event from a set of alternative start events using the message name.

When deploying a process definition with one or more message start events, the following considerations apply:

  • The name of the message start event must be unique across a given process definition. A process definition must not have multiple message start events with the same name. edoras one throws an exception upon deployment of a process definition such that two or more message start events reference the same message of if two or more message start events reference messages with the same message name.

  • The name of the message start event must be unique across all deployed process definitions. edoras one throws an exception upon deployment of a process definition such that one or more message start events reference a message with the same name as a message start event already deployed by a different process definition.

  • Process versioning: Upon deployment of a new version of a process definition, the message subscriptions of the previous version are cancelled. This is also true for message events that are not present in the new version.

messageStartEvent
Figure 9. Graphical Representation
Table 21. Specific attributes
Attribute Name Description

Is interrupting

This attribute denotes whether the sub-process encompassing the event sub-process should be cancelled or not.

Message name

Operation name

Error start event

An error start event can be used to trigger an Event Sub-Process. An error start event cannot be used for starting a process instance.

An error start event is always interrupting.

errorStartEvent
Figure 10. Graphical Representation
Table 22. Specific attributes
Attribute Name Description

Error code

The error code associated to the event.

Timer start event

A timer start event is used to create process instance at given time. It can be used both for processes which should start only once and for processes that should start in specific time intervals. Timer start event is scheduled as soon as process is deployed. That means that the process will be started without parent case. When is needed a timer after start a process (not deploy) then should be used an intermediate timer event after a start event.

Note

A process cannot be deployed if after timer start event there is a create case, create case from subform, create pdf or convert to PDF component.

Note

A subprocess cannot have a timer start event.

Note

There is no need to start an instance explicitly, although starting a process is not restricted and will cause one more starting of the process at the time invocation.

Note

When a new version of a process with a start timer event is deployed, the job corresponding with the previous timer will be removed. The reasoning is that normally it is not wanted to keep automatically starting new process instances of this old version of the process.

timerStartEvent
Figure 11. Graphical Representation
Table 23. Specific attributes
Attribute Name Description

Is interrupting

This attribute denotes whether the sub-process encompassing the event sub-process should be cancelled or not.

Timer properties

Opens a dialog to specify when the timer should fire.

Intermediate Event

An intermediate event marks the occurrence of a particular business event.

Process execution is not delayed.

intermediateEvent
Figure 12. Graphical Representation
Table 24. Specific attributes
Attribute Name Description

Execution listeners

The execution listeners to be executed when the process token arrives in the event.

Message catching intermediate event

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

Message Catching Intermediate Event style rules: * By convention, message catching intermediate events are named after the event they are waiting for. (e.g. "Additional data received")

messageIntermediateCatchingEvent
Figure 13. Graphical Representation
Table 25. Specific attributes
Attribute Name Description

Message name

The name of the message that the event is waiting for

Operation name

Timer intermediate event

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

Timer Intermediate Event style rules: * By convention, timer intermediate events are named after the event they are waiting for. (e.g. "Delivery deadline reached")

timerIntermediateEvent
Figure 14. Graphical Representation
Table 26. Specific attributes
Attribute Name Description

Timer properties

A wizard to configure the timer.

Signal catching intermediate event

Signal Catching Intermediate 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.

Signal Catching Intermediate Event style rules: * By convention, signal catching intermediate events are named after the event they are waiting for. (e.g. "New customer arrived")

signalIntermediateCatchingEvent
Figure 15. Graphical Representation
Table 27. Specific attributes
Attribute Name Description

Is interrupting

If the event is used as a boundary event. Specify if the related activity should be terminated when the signal arrives.

Signal name

The name of the signal the event is waiting for.

Signal throwing intermediate event

Signal Throwing Intermediate Events are used to model sending a particular signal. After sending the signal the process execution continues. One signal can fire execution of several independent process instances in one step.

Signal Throwing Intermediate Event style rules: * By convention, signal throwing intermediate events are named after the event they are throwing.

signalIntermediateThrowingEvent
Figure 16. Graphical Representation
Table 28. Specific attributes
Attribute Name Description

Execution listeners

The execution listeners to be executed when the process token arrives in the event.

Signal name

The name of the signal the event is throwing.

Boundary error event

An intermediate catching error on the boundary of an activity, or boundary error event for short, catches errors that are thrown within the scope of the activity on which it is defined.

Defining a boundary error event makes most sense on an embedded subprocess, or a call activity, as a subprocess creates a scope for all activities inside the subprocess. Errors are thrown by error end events. Such an error will propagate its parent scopes upwards until a scope is found on which a boundary error event is defined that matches the error event definition.

When an error event is caught, the activity on which the boundary event is defined is destroyed, also destroying all current executions within (e.g. concurrent activities, nested subprocesses, etc.). Process execution continues following the outgoing sequence flow of the boundary event.

boundaryErrorEvent
Figure 17. Graphical Representation
Table 29. Specific attributes
Attribute Name Description

Error code

The error code associated to the event.

The errorCode is used to match the errors that are caught:

If errorRef is omitted, the boundary error event will catch any error event, regardless of the errorCode of the error. In case an errorRef is provided and it references an existing error, the boundary event will only catch errors with the same error code. In case an errorRef is provided, but no error is defined in the BPMN 2.0 file, then the errorRef is used as errorCode (similar for with error end events).

End event

A none end event means that the result thrown when the event is reached is unspecified. As such, the engine will not do anything extra besides ending the current path of execution.

endEvent
Figure 18. Graphical Representation
Table 30. Specific attributes
Attribute Name Description

none

Error end event

When process execution arrives in an error end event, the current path of execution is ended and an error is thrown. This error can caught by a matching intermediate boundary error event. In case no matching boundary error event is found, an exception will be thrown.

errorEndEvent
Figure 19. Graphical Representation
Table 31. Specific attributes
Attribute Name Description

Error code

The error code associated to the event.

Terminate end event

A terminate end event means that all active tasks for the given process are terminated. edoras one extension may archive parent case also.

terminateEndEvent
Figure 20. Graphical Representation
Table 32. Specific attributes
Attribute Name Description

Archive type

The options to archive the parent case which contains the given process. * Do not archive * Archive only if there are no active children * Archive only if there are only active documents * Archive the case and all children

terminateEndEventAutomaticProcess
Figure 21. Terminate end event limitation

Terminate end event should reference persisted process instances. Note that a process instance without wait state is not persisted. However wait state can be easily created by changing automatic component property from synchronous to asynchronous.

3.6.2. Tasks

User Task

A user task is used to model work that needs to be done by a human actor. When the process execution arrives at such a user task, a new task is created in the task list of the user(s) or group(s) assigned to that task.

userTask
Figure 22. Graphical Representation
Table 33. Specific attributes
Attribute Name Description

Guid

The task guid. It can not be edited.

Form Reference

Select a form to be attached to the current task. You can also directly create a new form from here

Owner

Set the owner of the task by selecting an existing user statically

Owner (RT)

Use a backend expression to evaluate the owner dynamically at runtime

Assignee

Set the assignee of the task by selecting an existing user statically

Assignee (RT)

Use a backend expression to evaluate the assignee dynamically at runtime

Candidate Users

Set the candidate users of the task by selecting existing users statically

Candidate Users (RT)

Use a backend expression to evaluate the candidate users dynamically at runtime

Candidate Groups

Set the candidate users of the task by selecting existing groups statically

Candidate Groups (RT)

Use a backend expression to evaluate the candidate groups dynamically at runtime

Priority

Set the priority of the task by using a literal or a backend expression. The priority is not currently not supported at runtime

Due Date

Set the due date of the task. The due date is currently not supported at runtime

Allowed actions

The allowed actions of the task can be edited. If they are removed, will not appear in one. The Assign, Share and Create Variables can be selected or removed.

Mail Attributes

Refer to Send Mail Service Task for the usage of these attributes. By using this attribute section, you cause the engine to additionally send an email when the task is created

Manual task

A Manual task defines a task that is external to the BPM engine. It is used to model work that is done by somebody, which the engine does not need to know of, nor is there a system or UI interface. For the engine, a manual task is handled as a pass-through activity, automatically continuing the process from the moment process execution arrives into it.

manualTask
Figure 23. Graphical Representation
Table 34. Specific attributes
Attribute Name Description

none

Service Task

A service task is a task that uses some sort of service, which could be a web service or an automated application.

There are 4 ways of declaring how to invoke Java logic:

  • Specifying a class that implements JavaDelegate or ActivityBehavior.

  • Evaluating an expression that resolves to a delegation object.

  • Invoking a method expression.

  • Evaluating a value expression.

The named objects are resolved in the execution’s process variables and (if applicable) in the Spring context.

Please note that only one of the following attributes must be filled: class, expression or delegateExpression.

serviceTask
Figure 24. Graphical Representation
Table 35. Specific attributes
Attribute Name Description

Expression

Specify a UEL method expression or UEL value expression that should be evaluated.
Example UEL method: #{printer.printMessage()}, method printMessage (without parameters) will be called on the named object called printer. It’s also possible to pass parameters with a method used in the expression like #{printer.printMessage(execution, myVar)}
Example UEL value expression: #{split.ready} , the getter method of property ready, getReady (without parameters), will be called on the named bean called split.

Delegate expression

Expression that resolves to a delegation object. It is also possible to use an expression that resolves to an object provided in the attribute. This object must follow the same rules as objects that are created when the class attribute is used.
Example: ${delegateExpressionBean}, the delegateExpressionBean is a bean that implements the JavaDelegate interface, defined in for example the Spring container.

Result variable

The return value of a service execution (for service task using expression only) can be assigned to an already existing or to a new process variable by specifying the process variable name as a literal value for resultVariable attribute of a service task definition.
Any existing value for a specific process variable will be overwritten by the result value of the service execution. When not specifying a result variable name, the service execution result value gets ignored.

Class

Class that implements JavaDelegate or ActivityBehaviorSpecify. The class is called during process execution. The fully qualified classname needs to be provided by the attribute. Example: org.activiti.MyJavaDelegate

Owner

Set the owner of the task by selecting an existing user statically

Owner (RT)

Use a backend expression to evaluate the owner dynamically at runtime

Assignee

Set the assignee of the task by selecting an existing user statically

Assignee (RT)

Use a backend expression to evaluate the assignee dynamically at runtime

Candidate Users

Set the candidate users of the task by selecting existing users statically

Candidate Users (RT)

Use a backend expression to evaluate the candidate users dynamically at runtime

Candidate Groups

Set the candidate users of the task by selecting existing groups statically

Candidate Groups (RT)

Use a backend expression to evaluate the candidate groups dynamically at runtime

Create Case Service Task

The Create Case Service Task can be used to create a new case from within a process. Input data for the new case can be mapped and the newly created case can be referenced form the current case if needed.

createCaseServiceTask
Figure 25. Graphical Representation
Table 36. Specific attributes
Attribute Name Description

Case Model

Select a case model for the new case to be created

Init Variables

Specify additional variables that should be initialized during the creation of the new case

Case Name

Set the name of the new case using literals or backend expressions

CaseId Variable

specify a variable name where the id of the newly created case should be stored in the current case. This is optional

ParentId

specify an expression to resolve the work item id to be the parent item of the new case to be created. This is optional

Create case from subform service task

The create case from subform service Task is an itemization of the create case service task. Here you can specify a variable (subform) of the current workitem hierarchy as the input values for the new case instead of binding all input parameters manually.

createCaseServiceTask
Figure 26. Graphical Representation
Table 37. Specific attributes
Attribute Name Description

Case Model

Select a case model for the new case to be created

Init Variables

Specify additional variables that should be initialized during the creation of the new case

Case Name

Set the name of the new case using literals or backend expressions

CaseId Variable

specify a variable name where the id of the newly created case should be stored in the current case. This is optional

ParentId

specify an expression to resolve the work item id to be the parent item of the new case to be created. This is optional

Subform Variable

Specify the variable name where the subform to create the new case from is bound to

Remove Subform Variable

If set to true, the variable of the subform will be removed from the data model after creating the case

Create document service task

edoras one allows to enhance business processes with document creation service tasks. The create document service task creates a copy of the specified document template insid the current case. In case the document template is either a MS Word or Adobe PDF form, form fields will automatically be populated with execution data. Refer to Document Placeholders for more information.

createDocumentServiceTask
Figure 27. Graphical Representation
Table 38. Specific attributes
Attribute Name Description

Document Model

Select a document model for the new document to be created

Document Id variable name

Specify a variable name where the id of the newly created document should be stored in the current case. This is optional.

Document name

The name of the document to be created within the case, might also contain expressions

Convert to pdf service task

edoras one allows to enhance business processes with document conversion to pdf service tasks. The 'Convert to pdf' service converts a specified document to pdf format.

createDocumentServiceTask
Figure 28. Graphical Representation
Table 39. Specific attributes
Attribute Name Description

Document Id

Specify the id if the document which will be converted to pdf.

Save as copy

Indicates if the pdf document is saved as a new document.

Document id variable

Only active when 'Save as copy' is true. Specify a variable name where the id of the newly created document should be stored in the current case.

Send Mail Service Task

edoras one allows to enhance business processes with automatic mail service tasks that send e-mails to one or more recipients, including support for cc, bcc, mail templates, …​ etc.

Note

See section Mail Server in the Operators Guide for information on configuring the mail server in an on-premise environment

sendMailServiceTask
Figure 29. Graphical Representation
Table 40. Specific attributes
Attribute Name Description

Mail Model

This is a reference to a mail model in the current App. This model defines rich-text templates for the mail subject and body. Backend expressions in the mail model templates will be replaced when the mail is sent

Mail Recipient

The mail recipient is a required expression or literal that defines the email recipient (or multiple recipients)

Mail Reply-To

The mail reply-to is an optional expression that specify an explicit reply address. If no reply address is defined then the global system default will be used (which will vary from installation to installation)

Mail CC

Specify one or multiple CC recipients using literals or backend expressions. This attribute is optional

Mail Bcc

Specify one or multiple BCC recipients using literals or backend expressions. This attribute is optional

Mail Priority

An optional expression or literal that specifies the mail priority (usually an integer in the range 1 - 5)

Mail Headers

Specify additional mail headers. They will be applied before the message is sent

Attachments ids

The list of documents sent by the email. The documents can be referenced by its id or a backend expression.

Invoke REST endpoint service task

The Invoke REST endpoint service task provides a generic client to call arbitrary REST endpoints. The task supports all HTTP operations, Basic Authentication, custom headers and different mime-types.

invokeRESTEndpointServiceTask
Figure 30. Graphical Representation
Table 41. Specific attributes
Attribute Name Description

Protocol

Specify the protocol to use for the request. HTTP and HTTPS are supported

Request Type

Specify the type of the HTTP Request. GET, POST, PUT and DELETE are supported

Request Headers

Add additional HTTP message headers to your request

Request Body

Specify the message body that will be sent with the request. You can use backend expressions to include data from the data model within your message body structure. This can only be used with POST and PUT requests

Content Type

Define the content type header to be set within the request. This value will be overwritten if you have set a content type in the Header attribute

Host Name

The host name of the remote server (e.g. service.edorasware.com

Port Number

The port number of the remote server. If not specified, 80 is used

Request Path

Specify the path to the endpoint on the remote server. The path must start with a leading / You can use backend expressions to set the path dynamically at runtime based on values from the data model

Request Parameters

Specify the parameters that are passed as part of the request URL. Multiple parameters can be defined using the editor provided

Username

If the remote server requires basic authentication specify the username of the user you want to use for the request

Password

If the remote server requires basic authentication specify the password of the user you want to use for the request

Response Type

Select the format of the response of the REST call. Currently JSON and XML are supported

Set Variables

Map the response data to your data model. specify the variable name and use X-Path expressions to select the respecting value from the response

Overwrite if existing

Set to true if you want to overwrite the values of existing variables with the values from the REST calls response

JSON to XML mapping

The JSON to XML mapping used to prepare data for the XPath expression evaluation is mainly intuitive, but for completeness here is a quick summary of how different JSON content is mapped:

Table 42. JSON to XML mapping
JSON XML XPath example Result
{ 'node': 'value' }
<root>
  <node original-type="string">value</node>
</root>

//node

value

{ 'node': [ 'value1', 'value2' ] }
<root>
  <node1 original-type="string">value1</node1>
  <node2 original-type="string">value2</node2>
</root>

//node[2]

value2

{ 'node': { 'child':'cValue' } }
<root>
  <node>
    <child original-type="string">cValue</child>
  </node>
</root>

//child

cValue

'single value'
<root original-type="string">single value</root>

/root

single value

[ 'one', 'two', 'three' ]
<json>
  <root original-type="string">one</root>
  <root original-type="string">two</root>
  <root original-type="string">three</root>
</json>

/json/root[2]

two

Note

Note that in the case where multiple values are in the top-level JSON content, an additional root node (json) will be created as XML may only have a single root node.

Initialize variables service task

Use the initialize variables service task to initialize arbitrary variables in the current workitem hierarchy. You can use Expressions or literals to set the values of the variables.

initializeVariablesServiceTask
Figure 31. Graphical Representation
Table 43. Specific attributes
Attribute Name Description

Init Variables

Specify variable name and target workitem of the variable you want to initialize using literals. Use literals or backend expressions to specify the value of the variable

Overwrite if existing

If set to true, the values of already defined variables will be overwritten. Otherwise, already initialized variables are skipped

Initialize variables on query result

Use the 'Initialize variables on query result' service task to initialize variables in every work item being returned by a query. You can use Expressions or literals to set the values of the variables.

initializeVariablesServiceTask
Figure 32. Graphical Representation
Table 44. Specific attributes
Attribute Name Description

Query

The specified variables will be applied to every work item being returned by that query.

Variables to initialize

Specify variable name and target workitem of the variable you want to initialize using literals. Use literals or backend expressions to specify the value of the variable. For each variable it’s specified the flag 'Overwrite if existing', If set to true, the value of variable will be overwritten. Otherwise, already initialized variable is skipped

Result list item name

The name of the local variable representing the currently processed result list work item. This name might be used in an expression to access the current work item (e.g. '#{item.name}')

Result list item index

The name of the local variable representing the index (0-based) of the currently processed result list work item. This might be used in an expression to access the loop counter (index) of the currently processed work item (e.g. '#{itemIndex+1}')

Comment service task

Use the comment service task to add a comment to an arbitrary workitem reachable by the expression resolver. You can use templates similar to the ones in the send mail service Task to add formatted, structured comments.

commentServiceTask
Figure 33. Graphical Representation
Table 45. Specific attributes
Attribute Name Description

Comment Target Object

Use an expression that resolves to the workitem where the comment should be added to the comment-stream

History variable name

Stores the last comment added in the given variable name. If empty the comment is added to the comment-stream.

Comment User

Specify a the user in whose name the comment is created using an expression

Comment Template

Select the template for the comment

3.6.3. Execution elements

Sequence Flow

A sequence flow is the connector between two elements of a process. After an element is visited during process execution, all outgoing sequence flow will be followed. This means that the default nature of BPMN 2.0 is to be parallel: two outgoing sequence flow will create two separate, parallel paths of execution.

A sequence flow can have a condition defined on it. When a BPMN 2.0 activity is left, the default behavior is to evaluate the conditions on the outgoing sequence flow. When a condition evaluates to true, that outgoing sequence flow is selected. When multiple sequence flow are selected that way, multiple executions will be generated and the process will be continued in a parallel way.

Note

The above holds for BPMN 2.0 activities (and events), but not for gateways. Gateways will handle sequence flow with conditions in specific ways, depending on the gateway type.

Currently conditionalExpressions can only be used with Backend Expressions, detailed info about these can be found in section Backend Expressions. The expression used should resolve to a boolean value, otherwise an exception is thrown while evaluating the condition.

All BPMN 2.0 tasks and gateways can have a default sequence flow. This sequence flow is only selected as the outgoing sequence flow for that activity if and only if none of the other sequence flow could be selected. Conditions on a default sequence flow are always ignored.

sequenceFlow
Figure 34. Graphical Representation
Table 46. Specific attributes
Attribute Name Description

Condition Type

Specify the condition type of the message flow. standard an conditional flow behave the same, but are displayed differently. Use a backend expression in the Condidtion Expression Attribute to specify the condition. If you set the condition type to Default, this flow is executed when no other flow condition evaluates to true

Condition Expression

A backend expression that must evaluate to a boolean value

Exclusive Gateway

An exclusive gateway (also called the XOR gateway or more technical the exclusive data-based gateway), is used to model a decision in the process. When the execution arrives at this gateway, all outgoing sequence flow are evaluated in the order in which they are defined. The sequence flow which condition evaluates to true (or which doesn’t have a condition set, conceptually having a 'true' defined on the sequence flow) is selected for continuing the process.

Note

Note that the semantics of outgoing sequence flow is different to that of the general case in BPMN 2.0. While in general all sequence flow which condition evaluates to true are selected to continue in a parallel way, only one sequence flow is selected when using the exclusive gateway. In case multiple sequence flow have a condition that evaluates to true, the first one defined in the XML (and only that one!) is selected for continuing the process. If no sequence flow can be selected, an exception will be thrown.

exclusiveGateway
Figure 35. Graphical Representation
Table 47. Specific attributes
Attribute Name Description

none

Inclusive gateway

The inclusive gateway can be seen as a combination of an exclusive and a parallel gateway. Like an exclusive gateway you can define conditions on outgoing sequence flows and the inclusive gateway will evaluate them. But the main difference is that the inclusive gateway can take more than one sequence flow, like the parallel gateway. The functionality of the inclusive gateway is based on the incoming and outgoing sequence flow:

  • fork: all outgoing sequence flow conditions are evaluated and for the sequence flow conditions that evaluate to true the flows are followed in parallel, creating one concurrent execution for each sequence flow.

  • join: all concurrent executions arriving at the inclusive gateway wait in the gateway until an execution has arrived for each of the incoming sequence flows that have a process token. This is an important difference with the parallel gateway. So in other words, the inclusive gateway will only wait for the incoming sequence flows that will be executed. After the join, the process continues past the joining inclusive gateway.

Note that an inclusive gateway can have both fork and join behavior, if there are multiple incoming and outgoing sequence flow for the same inclusive gateway. In that case, the gateway will first join all incoming sequence flows that have a process token, before splitting into multiple concurrent paths of executions for the outgoing sequence flows that have a condition that evaluates to true.

Note

Note that at least one condition must be true

inclusiveGateway
Figure 36. Graphical Representation
Table 48. Specific attributes
Attribute Name Description

none

Parallel gateway

Gateways can also be used to model concurrency in a process. The most straightforward gateway to introduce concurrency in a process model, is the Parallel Gateway, which allows to fork into multiple paths of execution or join multiple incoming paths of execution. The functionality of the parallel gateway is based on the incoming and outgoing sequence flow:

  • fork: all outgoing sequence flow are followed in parallel, creating one concurrent execution for each sequence flow.

  • join: all concurrent executions arriving at the parallel gateway wait in the gateway until an execution has arrived for each of the incoming sequence flow. Then the process continues past the joining gateway.

Note that a parallel gateway can have both fork and join behavior, if there are multiple incoming and outgoing sequence flow for the same parallel gateway. In that case, the gateway will first join all incoming sequence flow, before splitting into multiple concurrent paths of executions.

Note

An important difference with other gateway types is that the parallel gateway does not evaluate conditions. If conditions are defined on the sequence flow connected with the parallel gateway, they are simply neglected.

parallelGateway
Figure 37. Graphical Representation
Table 49. Specific attributes
Attribute Name Description

none

Call activity (subprocess)

BPMN 2.0 makes a distinction between a regular subprocess, often also called embedded subprocess, and the call activity, which looks very similar. From a conceptual point of view, both will call a subprocess when process execution arrives at the activity.

The difference is that the call activity references a process that is external to the process definition, whereas the subprocess is embedded within the original process definition. The main use case for the call activity is to have a reusable process definition that can be called from multiple other process definitions.

When process execution arrives in the call activity, a new execution is created that is a sub-execution of the execution that arrives in the call activity. This sub-execution is then used to execute the subprocess, potentially creating parallel child execution as within a regular process. The super-execution waits until the subprocess is completely ended, and continues the original process afterwards.

callActivity
Figure 38. Graphical Representation
Table 50. Specific attributes
Attribute Name Description

LoopType

Selection of the loop type. See Multi Instance to know more about loops

Sub Process Reference

Select the subprocess to be called within the call activity

Sub Process Reference (RT)

Use a backend expression to evaluate the sub process reference dynamically at runtime.
Example: The expression ${modelManager.findLatestDefinitionKeyByModelId(String modelId)} gets the latest model definition for the given model Id.

In

Map input data to be passed from the parent (current) process to the subprocess while starting the subprocess

Out

Map output data to be passed form the subprocess to the parent process after the subprocess has finished

Subprocess

BPMN 2.0 makes a distinction between a regular subprocess, often also called embedded subprocess, and the call activity, which looks very similar. From a conceptual point of view, both will call a subprocess when process execution arrives at the activity.

The difference is that the call activity references a process that is external to the process definition, whereas the subprocess is embedded within the original process definition. The main use case for the call activity is to have a reusable process definition that can be called from multiple other process definitions.

A subprocess is executed within the scope of the parent process.

subprocess
Figure 39. Graphical Representation
Table 51. Specific attributes
Attribute Name Description

LoopType

Selection of the loop type. See Multi Instance to know more about loops

Text Annotation

The Text Annotation is for documentation purpose only, it’s not evaluated by the engine. Use it to add textual explanations to an element of the process model. In contrast to the Documentation property of an element, the text annotation is also visible on the graphical representation of the process model.

textAnnotation
Figure 40. Graphical Representation
Table 52. Specific attributes
Attribute Name Description

Text

Text annotation displayed in the process model.

Association

An association is represented with a dotted line. It is used to associate an artifact or text to a flow object, and can indicate some directionality using an open arrowhead (toward the artifact to represent a result, from the artifact to represent an input, and both to indicate it is read and updated). No directionality is used when the artifact or text is associated with a sequence or message flow (as that flow already shows the direction).

Note

edoras one currently only supports undirected associations

association
Figure 41. Graphical Representation
Table 53. Specific attributes
Attribute Name Description

none

3.6.4. Role elements

Horizontal Pool

Represents the organizational boundaries of a process, typically separating different organisations. A pool contains one or more lanes (like a real swimming pool). One process has to be executed within a pool. If the end-to-end process includes multiple pools, each pool has it’s own process flow. Communication between pools is handled by Message Flows. The horizontal pool in edoras one represents the system boundaries of edoras one in the mentioned orientation.

horizontalPool
Figure 42. Graphical Representation
Table 54. Specific attributes
Attribute Name Description

isExecutable

Indicates if this pool is executable.

Vertical Pool

Represents the organizational boundaries of a process, typically separating different organisations. A pool contains one or more lanes (like a real swimming pool). One process has to be executed within a pool. If the end-to-end process includes multiple pools, each pool has it’s own process flow. Communication between pools is handled by Message Flows. The vertical pool in edoras one represents the system boundaries of edoras one in the mentioned orientation.

verticalPool
Figure 43. Graphical Representation
Table 55. Specific attributes
Attribute Name Description

isExecutable

Indicates if this pool is executable.

Collapsed pool

A collapsed pool hides internal detail from the viewer. It’s used to visulaize communication between the mail pool and an external system / organization that is not in the scope of the current project. The collapsed pool has no impact at execution time.

collapsedPool
Figure 44. Graphical Representation
Table 56. Specific attributes
Attribute Name Attribute Type

Description

none

Lane

Used to organise and categorise activities within a pool according to function or role, and depicted as a rectangle stretching the width or height of the pool. A lane contains the flow objects, connecting objects and artifacts.

lane
Figure 45. Graphical Representation
Table 57. Specific attributes
Attribute Name Description

none

Message Flow

A Message Flow is represented with a dashed line, an open circle at the start, and an open arrowhead at the end. It tells us what messages flow across organizational boundaries (i.e., between pools). A message flow can never be used to connect activities or events within the same pool.

messageFlow
Figure 46. Graphical Representation
Table 58. Specific attributes
Attribute Name Description

none

3.6.5. Multi Instance

edoras one supports the creation of multiple instances of several BPMN 2.0 elements (Human Task, Call Activity, Embedded Subprocess and all Activities in the section Service Tasks of the edoras one palette). Multi-instancy is configured within the process model.

To specify multi-instancy select the corresponding type parallel or sequential.

If you chose parallel, all instances of the element are created at once, when the execution arrives at the activity. If you choose sequential, the next instance of the activity ist created, when the previous activity was completed.

Table 59. Multi-instance attributes
Attribute Name Description

Loop collection

A varialbe from the variable hierarchy that holds a list of elements. All elements of the list are processed, that means, the platform will generate one instance per list element

Loop element variable

a copy of the current list element. If you want to modify the variable, write the new value to the original list element since the loop element variable is not merged with the original one, once the execution is terminated.

Loop element index variable

The name of the variable where the current list index is stored.

Loop cardinality

An expression that evaluates to an integer which determines the number of instances to be created if you do not use a list for iteration.

Completion condition

A boolean expression do define a condition, to terminate the multi-instancy. As long as the expression evaluates to true, a new instance will be created.

3.7. Keyboard shortcuts

Table 60. Keyboard shortcuts supported by the property panel
Name Description

Up

Navigate to above property in the panel.

Down

Navigate to below property in the panel.

ENTER

To open editors and closing/committing entered value for inline editors.

SPACE

To open inline editors / toggle boolean values.

ESC

To close inline editor without committing the values.

F2

To open editor for editing selected attribute.

Table 61. Keyboard shortcuts supported by the treeview dialog editor
Name Description

ALT + 1

Selecting a New radio button.

ALT + 2

Selecting a Reference radio button.

ALT + 3

Selecting a Url radio button.

ALT + 4

Selecting a Remove Reference radio button.

SHIFT + F

Switch to Find(Search) mode in reference panel.

SHIFT + T

Switch back to Treeview mode in reference panel.

Up

Navigate to above node in tree.

Down

Navigate to below node in tree.

Left

Collapse folder.

Right

Expand folder.

ENTER

Select the current model.

Table 62. Keyboard shortcuts supported by the complex dialog editor
Name Description

INSERT

Adding a new row.

DELETE

Deleteing a selected row.

CTRL + up

Moving selected row up.

CTRL + down

Moving selected row down.

ENTER

Editing a selected cell.

TAB

Selecting next cell.

F2

To open editor for editing the selected cell.

3.8. Designer Usability hints

The designer provides various usability hints.

3.8.1. Alignment of shapes

Using Middle shape_align_middle and Center shape_align_center toolbar buttons we can align shapes horizontally or vertically.

Click the link to see demo.

3.8.2. Resizing of shapes to same size

Using Same size shape_align_size toolbar button we can resize all selected shapes to the same size.

Click the link to see demo.

3.8.3. Auto layout of connectors in process designer

Using Auto Layout shape_auto_align toolbar button we can auto layout the connectors (sequence flows) in the process diagram.

3.8.4. Creating dockers and removing dockers

We can create and remove dockers in 2 ways:

  • By using Add Docker vector_add and Delete Docker vector_delete toolbar buttons

  • By using mouse pointer directly on connecting shapes.

Click the link to see demo.

3.8.5. Changing the position of shapes

Using Change the position of shapes shape_translate toolbar button we can move the shapes horizontally or vertically.

Click the link to see demo.

3.8.6. Transform shape

Using Transform shape feature we can transform a shape to another type belonging to same group.

Click the link to see demo.

3.8.7. Validating a process

Using Validate Process checker_syntax toolbar button we can find if there are any errors in the process.

Click the link to see demo.

3.8.8. Deleting of shapes

Using Delete all selected shapes cross toolbar button we can delete all selected shapes. We can also delete selected shapes using Del key.

Click the link to see demo.

3.8.9. View preview in form designer

Using View preview edorasform_preview toolbar button we can view preview of the form.

3.8.10. Import BPMN 2.0 XML in process designer

Using Import BPMN 2.0 XML import toolbar button we can import process from a BPMN 2.0 XML into the process diagram.

3.8.11. Export/Import localization text

Export/Import localization text epc_export toolbar button supports exporting of all localized texts for a given model and importing of modified localized texts into the given model.

3.8.12. Compare revisions

Compare revisions compare toolbar button can be used to compare the revisions of the model to view the changes done between revisions.

3.8.13. Drop shape on connectors / remove shape from connector

When we drop a new shape on a connector which is already connecting 2 other shapes, the connector automatically splits to accomodate the new shape.

Similarly, if we delete a shape which is already connected to two other shapes, the connectors automatically rejoin.

Click the link to see demo.

3.8.14. Quick shape menu

We can quickly add and connect shapes to existing shapes using the quick shape menu.

Click the link to see demo.

3.8.15. Editing shape property in process/from designer

We can directly edit some of the shape properties like name in the process editor, and label, description and value in the form editor.

Click the link to see demo.

3.8.16. Using label expression in process designer

We can define a custom label expression for shapes, so that corresponding values are displayed as label on the shapes

Click the link to see demo.

3.8.17. Resizing of shapes using mouse

Using mouse pointer, we can resize the shapes.

Click the link to see demo.

3.9. Expressions

edoras one can evaluate expressions in both the frontend (i.e. in the browser) and in the backend (i.e. in the server). Expressions can be used in a number of ways in the edoras one application, for example:

  • to create a binding between a form field and work object data

  • to modify the state of form fields dynamically in response to user input (e.g. field visibility)

  • to populate a mail template before the mail is sent

  • to control the execution flow in a process execution

  • to execute method calls on the server

  • …​

3.9.1. Frontend expressions

Form binding expressions

Forms are used to display and edit the data of underlying work objects. The work object data and the form definition are fetched from the server, and the expressions used within the form definition are then used to link the form fields with corresponding data. For editable fields, any changes made are reflected in the underlying work object data, which is sent back to the server to be persisted when the form is submitted:

formbinding

Form data binding expressions are placed between two sets of curly braces, e.g. {{expression}}, and are set using a form field’s Value attribute:

formeditor
Tip

The Value attribute of most form fields may only be bound to a single expression, and this may not be preceded or followed by other text.

The only exception to this rule is the output field, which doesn’t bind to a single value, but instead uses form expressions embedded within a rich text template to create dynamic textual output.

Simple variable bindings

A simple value name in double curly braces (e.g. {{value}}) is used to link to the corresponding variable in the current work object. If the work object variable doesn’t exist when the form is opened then it will be created automatically.

Bindings to other hierarchy work objects

A work object in edoras one is typically part of a hierarchy, where work objects are linked together in a tree of parent/child relationships. For example a case typically serves as a container for other work objects (tasks, running processes, documents etc.). Form binding expressions can also be used to access the values from related work objects in the work object hierarchy. This is done be preceding the variable to be accessed by a qualifier (e.g. {{root.value}}):

The following qualifier keywords are currently supported:

parent

the parent of the current work item

root

the root of the current work item hierarchy

self

the current work item

$this

the form. When we use a subform widget it would be the subform itself.

As an example, if we start a process within a case then the process’s parent work object will be the case. A form task created by that process will have the process work object as its parent:

hierarchybinding
Figure 47. Example work object hierarchy

In this case {{root.name}} will bind to "Example case", {{parent.name}} will bind to "Example process" and {{name}} will bind to "Example task".

Note

Init forms for tasks and processes only support parent variables update. That means that is not possible to update the case variables using the expression root.

Bindings to other global objects
currentUser

The object of the current user logged in the application, which provides its properties like id, name e.g {{currentUser.id}}

Bindings to global functions
isUserInGroup(userId, groupId)

The result of this expression will become true if the user is member of the provided group.

  • Parameters:

    • userId: the id of the user.

    • groupId: the id of the group.

This expression is executed asynchronously, that means that it will be "false" until the response from server is success.

isUserInGroups(userId, groupIds)

The result of this expression will become true if the user is member of one of the provided groups.

  • Parameters:

    • userId: the id of the user.

    • groupIds: an array of group id’s.

This expression is executed asynchronously, that means that it will be "false" until the response from server is success.

isUserInAllGroups(userId, groupIds)

The result of this expression will become true if the user is member of all of the provided groups.

  • Parameters:

    • userId: the id of the user.

    • groupIds: an array of group id’s.

This expression is executed asynchronously, that means that it will be "false" until the response from server is success.

Formatting dates in frontend expressions

When not being used to create a binding between a form field and a work item attribute (e.g. when used in the output form widget), frontend expression that resolve into a date can have an additional format string that defines how the date is formatted. To do so append the format string to the binding expression, for example:

{{anyBoundDate | date:'MMM/dd/yyyy'}}
{{anyBoundDate | date:'dd-MM-yyyy'}}

If no format string is explicitly specified then yyyy-MM-dd is used.

The following tokens are allowed:

Token Description

yyyy

4 digit representation of year, e.g. 0001, 2010

yy

2 digit representation of year, padded (00-99), e.g. 01, 10

MMMM

month name long (always in English): January-December

MMM

month name short (always in English): Jan-Dec

MM

month of year (two digit): 01-12

M

month of year (no leading zero): 1-12

dd

day of month (two digit): 01-31

d

day of month (no leading zero): 1-31

EEEE

day name long (always in English): Sunday-Saturday

EEE

day name short (always in English): Sun-Sat

The format string can contain literal values which need to be quoted with single quotes. Make sure that you escape the single quotes that denote a literal value. In order to output a single quote, use two single quotes in a sequence:

{{anyBoundDate | date: '\'day\' d \'of\' MMMM \'in the year\' yyyy'}}
{{anyBoundDate | date: '\'Today it\'\'s\' dd/MM/yyyy'}}

It’s also possible to use the age filter in order to calculate the elapsed time from provided date until current date. The elapsed time can be calculate in ages or days.

{{anyBoundDate | age}}
{{anyBoundDate | age: 'd'}}
More complex frontend expressions

When not being used to create a binding between a form field and a work object attribute, that is when not setting value property on most of the widgets, we can use more complex expressions like arithmetic or logic operations.

You can use this kind of expressions in runtime properties (like Editable(RT) or Visible(RT) ), query, default, or in the value of the output text.

Arithmetic operations
{{ (number1 + number2) * number3 }}
{{ number1 / number2 }}
Text concatenation
{{ text + ‘_end’ }}
Logic operations
{{ (boolean && otherBoolean)  &&  ( text + _‘endText’ )}}
Assignment operation

This will create a variable in the data model of the form. For example, in the example bellow if you use this expression in an output text, the variable modelValue will be saved, the result of the expression would the the result of the operation after the =.

{{ modelValue = number1 + number2 }}
Subform with multiple elements special expression keys

When we use subform or multi type subform with multiple elements we can make use of some special expressions:

Key Result type Description

$index

number

iterator offset of the repeated element (0..length-1)

$first

boolean

true if the repeated element is first

$middle

boolean

true if the repeated element is between the first and last

$last

boolean

true if the repeated element is last

$even

boolean

true if the position $index is even

$odd

boolean

true if the position $index is odd

We can, for example, retrieve the value of the previous item, having a subform with "mySub" value, the last item would be {{mySub[$index-1]}}

Accessing arrays

Some widgets saves an array of items (Search button , Autocomplete with multitag, subform with miltiple elements, etc.)

You can access the value of an element through an expression model, for example, having a search button with {{myResult}} value, and a number widget with {{myIndex}}. You can use an expression like {{myResult[myIndex]}} to retrieve an item of the array.

3.9.2. Backend Expressions

Expressions can also be executed on the server, for example within the process models. Server-side expressions are called backend expressions. A much wider range of expressions is possible than in frontend expressions the expressions may also have side-effects within the system (for example causing an e-mail to be sent). As well as accessing data from the work object hierarchy, backend expressions support additional functionality such as accessing work objects outside of the immediate hierarchy, predefined function calls and bean method calls.

There are two basic expression types:

value expressions

have the form #{value.property} and resolve to a value

method expressions

have the form #{bean.method(value)} and result in a method invocation

Tip
The full backend expression language is part of the EE6 specification and only an brief overview will be provided here. For a full description of the expression language syntax please refer to the EE6 specification.
Accessing work objects

As in the frontend, qualifiers can be used to traverse the work object hierarchy, although there are more options available in backend expressions. qualifiers can also be chained (e.g. #{process.parent.name}):

The following qualifier keywords are currently supported:

self

the current work item (the default if no work object is explicitly specified)

case

the top-most parent case to which the current work object belongs

parentCase

the nearest parent case to which the current work object belongs

process

the top-most parent process to which the current work object belongs

parentProcess

the nearest parent process to which the current work object belongs

root

the root of the current work object hierarchy

parent

the direct parent of the current work object

Backend attribute values

Simple attribute names can be used to access work object attributes in backend expressions in the same way as in frontend expressions (see Frontend expressions).

To access attributes, simply use the attribute name: #{case.name}

Tip

One significant difference between frontend expressions and backend expressions is that the work object attributes accessible in the frontend may have been pre-processed by the server so that the attribute values are in a form that makes more sense in the client’s context. With backend expressions this processing is not performed so you will be working with raw work attributes.

As an example, state is represented in a work object by an instance of the State class. In the frontend , the expression {{state}} will be bound to the name field of this object (e.g. "ACTIVE"). The conversion from a State object to the state name string has already been performed by the server.

The corresponding process binding, #{state} resolves to the underlying State object, which if it is used in a string context will be converted to a string literal, e.g. "[State@51d838d9 name = \'ACTIVE']". To access the plain name ("ACTIVE") we can use a java bean property expression: #{state.name}.

For a description of common work object attributes, refer to Common attributes.

Automatic work object ID conversion

Work objects are referenced using IDs, and in a backend expressions it is often useful to access not the ID itself but the referenced work object. To support this use case, an expression that resolves to a work object ID will automatically be converted to the corresponding work object instance as needed.

For example the expression #{ownerId} will resolve to the owner ID, but if we then extend the expression (e.g. #{ownerId.name} then the ID will be converted to the corresponding work object, and the name attribute from this work object will be returned.

As access to other work objects linked from a work object using IDs is so common there are also some shortcut expressions to access this information directly:

Table 63. Predefined work object expressions
Expression Type Description

#{owner}

User work object

the work object’s owner

#{assignee}

User work object

the work object’s assignee

#{initialAssignee}

User work object

the work object’s initial assignee

#{previousAssignee}

User work object

the work object’s previous assignee

#{candidateUsers}

List of user work object

the work object’s candidate user list

Resolving hierarchy variables

A major difference between frontend expressions and backend expressions is the visibility of work object variables within the work object hierarchy. With frontend expressions, accessing parent or root variables requires explicit use of the appropriate qualifier (e.g. parent.attribute).

With backend expressions, however, this is not always required. In the server, variables from parent work objects in the hierarchy are also visible in the child work objects. If a parent and child both have variables with the same name then only the child variable will be visible in the child work object.

For example, take the following work object hierarchy:

variablehierarchy

If we are resolving variables in the task context, then the expression #{case.message} will resolve to "Case message".

The expression #{process.message} will also resolve to "Case message" the process work object will inherit the message value from its parent (the case).

The expression #{message} will resolve to "Task message", however, as the variable inherited from the case has been hidden by a more local value.

Bean resolution

Backend expressions can also be used to access java beans, either to make method calls (#{beanName.method()}) or to access bean properties (#{beanName.property}).

For security reasons it is obviously not a good idea to allow access to all beans within the server, and so edoras one only allows access to a specific set of approved bean names.

Predefined functions

Backend expressions also support a number of predefined functions. As with bean resolution, the functions provided by a given edoras one installation can be configured, so the list of available functions may vary from system to system.

The following functions are provided by default:

now()

returns the current date and time

formatDate(Date date, String pattern)

returns the provided date formatted according to the provided pattern. When input parameters are null or empty throws IllegalArgumentException.

formatDateNullSafe(Date date, String pattern)

returns the provided date formatted according to the provided pattern. When input parameters are null or empty returns empty string.

formatType(Type type, boolean lowerCase)

returns the provided type formatted as a String. When type input parameter is null throws IllegalArgumentException.

anyOf(Boolean... operands)

returns true if any of the provided booleans is true, false otherwise

allOf(Boolean... operands)

returns true if all of the provided operands are true, false otherwise

concatenateListWithList(List<T> base, List<T> toAppend)

returns a list that is the concatenation of the given two lists

concatenateListWithElements(List<T> base, T... toAppend)

returns a list that is the concatenation of the given lists and the given elements

extractValuesFromCollectionOfMaps(Collection<Map<String, T>> maps, String mapKey, boolean skipNullValues)

returns a collection of map values for the given key from the given collection of maps

Identity Manager bean

The predefined bean #{identityManager} provides functions related with the group management.

The available functions of this bean are:

getUsersOfGroup(GroupId)

returns the list of users of a given group.

getUsersOfGroups(Collection<GroupId>)

returns the list of users of a collection of groups.

isUserInGroup(UserId userId, GroupId groupId)

returns a boolean which value is true if the given user belongs to a given group.

isUserInGroups(UserId userId, List<String> groupIds)

returns a boolean which value is true if the given user is member of at least one group in the list, false otherwise. In case of empty list false is returned.

isUserInAllGroups(UserId userId, List<String> groupIds)

returns a boolean which value is true if the given user belongs to all groups in the list, false otherwise. In case of empty list false is returned.

Model Manager bean

The predefined bean #{modelManager} provides functions related with the models.

findLatestDefinitionKeyByModelId(String testModelId)

Returns a string which contains the latest active definition key by the given model id.

System properties in backend expressions

The expressions using system properties with dots in the name are supported in the following way, for the example for the property mail.smtp.from:

#{systemProperties['mail.smtp.from']}

3.9.3. Common attributes

Work object attributes

A number of predefined attributes are available on every work object:

Table 64. Predefined work object attributes
Expression Type Description

name

String

the work object’s name

description

String

the work object’s description

state

String

the work object’s state

type

String

the work object’s type

ownerId

User ID

the work object’s owner

assigneeId

User ID

the work object’s assignee

candidateUserIds

List of group IDs

the work object’s candidate users

candidateGroupIds

List of group IDs

the work object’s candidate groups

creationTime

Date

the work object’s creation time

updateTime

Date

the work object’s last update time

dueTime

Date

the work object’s last due time

priority

Number

the work object’s priority

User attributes

The following attributes are defined in user work objects:

Table 65. Predefined user attributes
Expression Type Description

login

String

the user’s login name

name

String

the user’s display name (full name)

firstName

String

the user’s first name

lastName

String

the user’s surname

address

String

the user’s address

phone

String

the user’s telephone number

mobile

String

the user’s mobile telephone number

email

String

the user’s email address

language

String

the user’s language

homeUrl

String

the user’s home URL

notes

String

the user’s profile notes

memberGroups

List of group IDs

the groups ids which the user belongs to

3.10. Localization

edoras one supports five languages: English, German, French, Spanish and Italian. When modelling is possible to define different languages for tasks, forms widgets, cases, etc.

In this section you can find how to localize your application models.

3.10.1. Localize applications models

In order to set the supported languages of the application model, firstly they should be defined in the 'Browse' view. There you can set a primary language and various secondary languages.

If your language is not maintained in a particular App model, it defaults to the primary language defined.

localizationAppModel

3.10.2. Localize case models

In order to localize the cases, the modeller should navigate to the 'Browse' view and click in 'Localize' action button. The name and description of the case can be set in the languages defined in the application model.

localizationCaseModel

3.10.3. Localize process models

In order to localize the processes, the modeller should navigate to the 'Browse' view and click in 'Localize' action button. The name and description of the process can be set in the languages defined in the application model.

localizationProcessModel

3.10.4. Localization in VIS

There are some attributes of VIS elements which can be also localized, for example the labels or the task names can be set in the languages defined in the corresponding App model. To see the localized attributes in the attributes panel, the language must be selected at the top-right of the panel. A list with the application model languages will be displayed:

localizationSelectLanguageVIS

When a language is selected, a new attribute for each attribute which can be localized will be added in the attributes list:

localizeAttributesVIS

Moreover the design and preview form will be displayed in the language selected in the menu.

3.11. Glossary

app

an app is a container for a collection of models that define a specific workflow.

backend expression

a backend expression will be evaluated in the server, for example to populate a mail template before it is sent.

dashboard

a page in the edoras one application where work objects can be found, displayed and manipulated. Each dashboard manages related work object types that play a given role within the system.

definition

when a model is deployed, a definition is created in the system. Definitions describe the behaviour of the system as seen by a normal user.

deployment

the process of converting the models into definitions to create an executable workflow.

document placeholder

a backend expression identified by a key in a document model. Used to insert dynamic content into documents.

expression

an expression can be used to access content or trigger specific behaviour at runtime. Two types of expressions are used: frontend expressions and backend expressions.

form

a form is a graphical layout of fields that may be bound to work object data to allow this data to be displayed or modified within the edoras one application.

form widget

a form widget is a type of form field, e.g. text form widget, date form widget, number form widget. The form field represents the single element on the form whereas the form widget represents the type of these single elements. As such the form widget defines the look and behaviour of the corresponding form fields.

form field

a form field is a single element on a form. The user interacts with the form field, most of them are interactive but some of them are read-only.

form field part

a form field is composed of different graphical parts: a label, a control, a description, a required indicator. The user interacts with the control part only.

frontend expression

a frontend expression will be evaluated in the browser, for example to bind form fields to work object data.

model

a description of a workflow component which can be deployed together with other models to create a executable workflow in edoras one

process

a process is a workflow definition, described using a BPMN 2.0 model and executed within edoras one. It typically defines the automated and manual tasks that must be performed to complete the workflow.

system app

a special App that can only be seen by the administrator. It contains the models that are required for the correct operation of edoras one.

task

a task is a single action within a workflow, and may be either automated (a service task) or manual (a user task).

work object

an item that can be created or manipulated in edoras one. Different types of work object are supported. The work object types are typically grouped according to their role within the system and accessed through dashboards.

4. edoras one Modeler Tutorial

4.1. Intro

This step-by-step tutorial will help edoras one modelers to get started with modeling Apps for the edoras one platform.

It’s a good idea to have read the basic edoras one intro (general guide) as well as the edoras one user guide, which can be found here: http://documentation.edorasware.com

4.1.1. What you will learn

By executing this tutorial step by step, you will learn how to create your own App, which includes the following steps and learning content.

Basic modeling techniques
  • Creating case models

  • Design forms

  • Design processes

  • Creating ad-hoc task models

  • Creating email templates

Advanced modeling
  • …​

4.1.2. Tutorial App

In this tutorial you will create a new App, which will handle a travel request. A person in need of traveling will create a new travel request and a travel agent is organizing the necessary bookings for the traveling.

The request process is built step by step by adding approvers to the process, modeling a feedback-loop between travel agent and requestor and by adding mail tasks to send out information once the travel has been organized.

Everything around the travel request is modeled within a case, so we are able to attach any necessary information like reservation information, vouchers and the like directly to the case as well as having an overview of everything going on while managing the travel request.

4.2. Basic Travel Request Case and Form

In this chapter you will create the Tutorial App with a new form and case model, deploy it and test the new case model within the user dashboard.

4.2.1. Creating a new App

In order to get started, go to the modeler dashboard and hit c or click the Create button in the menu and choose Create App.

Type Tutorial App as the name for the App.

Select English as the primary language for the App.

Select any other languages you want as secondary languages.

Tip
As an App can be modeled in several languages, you specify a main (primary) language for the App, which will act as the default language, if not all elements are translated. You will be able to design forms and processes with all languages you selected (primary will be default). Now click Create and navigate to the newly created App by clicking on the link in the green success message you see on the top of the screen.
create app 01

4.2.2. Creating a form to start the travel request

Let’s create a first form by typing again c (shortcut) or clicking the Create button in the menu. Now let’s choose Create Form Model and make sure the Tutorial App is selected as the parent App (meaning the new form model will be created inside that App).

Type Travel Request Form as the name for the new form model and click Create and design in order to create the model and directly go to the form designer.

create form 01
Getting familiar with the basics of the form designer

The form designer consists of three parts; on the left you will find the shape repository, the main canvas for the form design in the middle and on the right hand side you find the attributes of the currently selected widget (component) of the canvas.

You can add a new widget to the canvas by simply drag & drop it from the shape repository to the canvas. During the drag & drop procedure you will get feedback by the responsive layout manager on where the widget can be dropped.

The layout is responsive having twelve slots you can use to arrange your widgets. By using the handles on the left or right side of a widget, you can define the span of the widget from one up to twelve slots.

The shape repository has several sections to choose from. Use any predefined widgets in order to not having to care about bindings and any other technical issues. The widget section on the other hand has a collection of all available widgets which can be used by configuration through the attributes section.

Add name and description widgets

To start with the design, let’s use two predefined fields; the name and description. Those fields are always available; each work item is supporting them.

Now drag and drop the name and description field from the predefined bindings section on to the empty form. Rename the name label to Travel subject and the description label to Travel notes.

form design 01
Add traveling locations

Now let’s add two fields to enter the origin and destination of the travel. There is no predefined field for that, so let’s use the Text field from the Components section. Name the first one Origin and the second one Destination. By dropping the second field onto the first one will split up the row into two parts and now having two fields in the same row, both spanning six slots.

Tip
Instead of navigating through the shape repository, you can also use the filter functionality to quickly find a specific widget. Click the filter icon in the top right corner of the Shape repository and type the name of the widget into the search field (e.g. text).

By selecting a field and clicking the red star icon, it will make the fields mandatory. Do this for both Fields. You can do the same by clicking the Required attribute on the right side.

form design 02
Getting familiar with the data model behind work items

Before we can define the data bindings for the new fields, let’s have a short look on how data is stored and managed within edoras one.

The basic infrastructure to manage anything within edoras one is called work item. A work item is an object from a particular type (e.g. a case, a task, a process or a document), it’s behavior is driven by the type and it’s model (e.g. a process work item is driven by it’s process model which in our case is modeled with BPMN 2). Besides the behavior, every work item is capable to store arbitrary data, called variables. Variables are holding different types of data, e.g. text, numeric value, a date or a boolean value. Variables can even hold lists of data or maps or even lists of maps including unlimited nesting.

In order to enter or show data of work items, the variables can be mapped (bound) to fields.

Define data bindings for the fields

The next step is to set the binding for the newly added fields. Double-click the field or go to the Value attribute on the right side. Bindings are telling the form engine where to save and load the data for a particular field.

Let’s save the origin in a variable called travelOrigin and destination in a variable named travelDestination, respectively. Bindings are specified using expressions, which start with a double curly brace {{ and end with a double curly brace }}.

If we don’t specify any prefix to the variable name, the data is stored / retrieved from the very work item the form is used for.

form design 03
Add date pickers for the outward and return trip dates

Now let’s add two new fields to pick the starting and ending date for the travel. Drag and drop two Date fields to the form and name them Outward trip date and Return trip date, respectively.

Make the fields mandatory and set the bindings to {{outwardTripDate}} and `{{returnTripDate}}.

form design 04
Add validation to the date pickers

As we want to make sure there is no date selected in the past, let’s add a validation to the date picker.

You can do so by selecting the date field and then click on the Minimum date attribute on the right side under Specific attributes. Select a relative date validation by choosing Today + 0 Days.

form design 05

For the Return trip date we also want to add a validation. Here we want to ensure that the selected date is not smaller then the Outward trip date. To achieve this, we simply add {{outwardTripDate}} to the Minimum date attribute of the Return trip date widget.

form design 06

Now save the form by clicking the Save icon in the designer menu on the left.

4.2.3. Create case model

Now switch back to the browser tap where you created the new form (not the form designer) and click the Create button once again and select Create case model.

Fill out the case model form:

  • Set Travel Request as the name

  • Choose the Travel Request Form as the init and the work form of the case

  • Everything else can be left as it is by default, we will adapt that later

Now click Create to create the new case model.

create case model 01

4.2.4. Deploy the App

Done! We have our very first version of the Tutorial App, ready to be deployed and used.

Click the Tutorial App link from your previous, green success message or the link within the header of the current case model in order to navigate to the Tutorial App.

In the App, you should now see two models, the Travel Request case model and the Travel Request Form model.

On the right side menu you can select Deploy which opens the deploy dialog. Navigate to the end of the form and click OK to deploy the App and make it available within the user dashboard so we can now test it.

app deploy 01

4.2.5. Test the App

In order to test the newly deployed App, we switch back to the user dashboard. You can do so by manually selecting the User dashboard from the menu of dashboards or use the d shortcut. While modeling and testing, it is also a good practice to have two tabs open constantly, one with the user dashboard and one with the modeler dashboard.

Once in the user dashboard, click Create or type c to open the creation dialog. Select Create Case and choose Travel Request from the selection menu.

Fill out the request init form as we designed it. Please note the validations and within the date picker, specifically see that you will not be able to enter a date earlier than today.

create case 01

Click Create to start the new case. You will see the newly created case with the same form as we used it for both, the init and the work form. So you will be able to change any data and click Save in order to change the data for that case.

create case 02

4.2.6. Getting familiar with the basic navigation

On the right side you will find available actions for the current work item (a case currently). The top right area shows the available views of the work item.

case menu 01

Actions from top to bottom:

Assign

let’s you specify the owner and current assignee of the work item

Share

let’s you share the work item with one or more groups

Start Process

start an ad-hoc process within the current case

Archive

closes the current work item and archives it

Views from left to right:

Search

navigates to the list of children of the current case where you can find open tasks, documents or whatever the current case is holding

Browse

navigates to the main work form of the current work item

Comments

navigates to the comment section of the current work item where you can see all existing comments as well as add new comments or reply to existing ones

Edit Entity

only available for modelers, shows a generic view with all data of the current work item and the ability to change it or even add new variables

Edit Entity JSON

a JSON representation of the current work item, which can also be modified (be very careful in doing so as you could break data of the work item by modifying it’s JSON representation)

4.3. Basic process modeling

In this chapter you will learn how to create your first process and attach it to the previously created case model in order to start it automatically once a new travel request is created.

4.3.1. Create new "Travel Request Process" model

Switch back to the modeler dashboard (either through the dashboard switcher or by going to your second tab with the modeler dashboard still open).

Choose the Tutorial App if not yet opened and click Create or hit c. Now let’s choose Create Process Model and enter Travel Request Management Process as it’s name and click Create and Design in order to create the process model and directly open it in the process modeler.

create prc model 01

4.3.2. Getting familiar with the process modeler

The process modeler is quite similar in it’s basic functionality as the form designer. On the left hand side you find the shape repository with several sections where you can choose process elements from, the middle area is the main process canvas where you design the process and the right hand side is showing the attributes of the currently selected element in the process canvas.

You can add elements to the process by simply drag and drop them from the shape repository onto the canvas.

Hint: as you start modeling processes, there is a very handy feature to make more space in your current process model, the spacing tool:

prc modeler action 01

Activating it will allow you to simply extend (enlarge) the process model or make it smaller. Give it a try once you have some elements on the canvas and make yourself familiar with that feature. Simply turn it off by clicking it once again and deselecting it.

4.3.3. Add a pool and lanes to the process

Let’s start the process model with adding a pool and lanes to it, although this is optional in BPMN 2, but is a good practice in order to visualize the different roles and involved people and groups in the process.

Go to the Roles section in the shape repository, drag and drop a Pool onto the canvas and name it Travel Request Process. A single lane has been added automatically, double-click its label (called Lane) and rename it to Requestor.

prc modeling 01

Now let’s add a second lane for the travel agent managing the travel request. Select Lane from the Roles section of the shape repository and drag it to the bottom part of the pool, which will add it as a second lane below the existing one. Name it Travel Agent. Using the spacing tool mentioned before, it’s easy to make the lanes smaller or larger according to the needs.

prc modeling 02

4.3.4. Model two user tasks as the first version of the process

Now we created two lanes representing two different types of involved people in the process, the requestor actually creating the travel request and the travel agent, responsible to manage the travel.

Let’s add a start event to the process by dragging the Start event element from the Execution section of the shape repository onto the Requestor lane. As long as that element is selected, you should see the fast-modeling menu right next to the element. Click the user symbol in order to add a user task right after the start event. Name it Manage Travel Request, click on the user symbol again and name the next task Review Travel Bookings and then click the thick rounded end event in the fast-modeling menu.

Done! We have our very first process with a start event, two user tasks and an end event.

prc modeling 03

Now let’s move the tasks into the right lane by simply moving them. Move the Manage Travel Request task into the second lane, as it should be done by the travel agent. Make use of the spacing tool in order to add or remove space between lanes, elements, etc.

prc modeling 04

4.3.5. Save process model and attach it to the case model

Now save the process model by clicking on the save action in the menu bar (ignore the warnings for now, we will add task forms later) and go back to the browser tab with the process model within the Tutorial App (not the designer window).

Navigate to the Tutorial App and select the travel request case model. In the Autostart Processes selection choose the newly created process model and save the case model again.

map prc case model 01

4.3.6. Re-deploy and test the App

We now created a first, yet simple process model and attached it to the existing case model. We can now re-deploy the App by using the Deploy action as we did before and switch to the user dashboard in order to test the changes we did.

After successfully deploying the Tutorial App again and switching to the user dashboard, click Create again and select Create Case. Select the Travel Request template, fill out the form and start the next travel request case.

Did you note the gray badge in the left top corner of the children view button? It shows you that there is one child work item of the current case which is the first task being created by the process.

case testing 01

Click the children list view to see that task. The open task should be named Manage Travel Request and behind it you should see the name of the travel request case (the same as within the header).

case testing 02

Navigate to the task by clicking its name. It will take you to the work form of that user task. You can still see the case in the header section of the task and use it for the navigation back to the case or you can also use the browser-back button to go back. Navigating to the task means, selecting it as the current work item. As you probably noted, this will change the available views and actions, always reflecting the possibilities of the currently selected work item.

case testing 03

The user task has a default form with the name of the task and a description field. But let’s have a look at the different options we got working with the task. There is a new view, called Preview, selecting it will show you the process with a red border around the current task.

case testing 04

Go back to the work form by clicking on the form view button, add some notes and click Complete. This will complete the task and hand over the control back to the process engine continuing with the process execution and in our case will lead to the second task being created. You can go to the preview again and see that the process execution has now moved to the second task. Complete the second task as well and this will finish the process execution. You can then navigate back to the case by using the case-link in the header of the task and make sure that the children view does not show the badge again as there are no more open tasks available.

4.3.7. Create user task forms for process execution

As we haven’t created specific user task forms yet, you realized that there were default forms used. Of course this doesn’t make much sense and we want to improve the two user tasks by designing task forms for them, showing all necessary information needed to complete the task.

Switch back to the process designer (if still open, otherwise switch to the modeler dashboard, select the process model and click Design to open the process modeler).

Select the Manage Travel Request user task. There are two options to create / select a form for this user task; either click the Add reference context menu or go to the attributes of the task and click the Form reference attribute.

Now you can choose between selecting an existing form (of course we don’t have any suitable form yet) and creating a new one. And that’s what we are going to do, so choose New. The form designer is opened again with a new form to be designed, which is automatically attached to that task.

Understanding the work item hierarchy

Before we start designing the new task form, let’s have a look on how the data is organized in the work item hierarchy. The process is started within the case, so making the process instance a child work item of the case. If a user task is created automatically through the process, this task becomes a child element of the process, so think of a hierarchy, where the case is the root, having a process as a child and the process having a task as it’s child. You can store data on every work item and every work item has also access to the data of it’s parent up to the root. But of course when building form data bindings, you have to be aware of the hierarchy and where the form is actually used. Bindings like {{foo}} without any prefix are always bound to the work item that form is used with. Let’s say you want to store / read data in the case, use the root prefix, for example the {{root.foo}} would store / read the data from the case’s variable named foo. You can also use parent as a prefix in order to go up one hierarchy level of the current work item. In a user-task, the parent would be the process.

Show task information using an output widget

Now let’s go back to our user task form for managing the travel request. It’s a good practice to include the data the current task needs on the task form. As a first step, we add an Output textarea to it in order to display the travel request in a read-only way. Drag and drop an Output textarea from the shape repository onto the form and don’t enter a label name, leave it blank.

Double click the widget on the form or click the Value attribute to open the rich editor to edit the content for the widget.

Let’s add something like this:

user task design 01

You can use formatting and add any text you want. Of course you can also add expressions the same way you did with the bindings of the fields. Please note, that we are now designing the form of a user task and hence we need the root prefix in order to display data, which is stored in the case. Make sure you use the very same variable names you used in the case init form.

Add editable information to the form with a rich text area

We want the notes for the travel not only to be shown, but also to be editable by the travel agent, so let’s include the notes field to the form using the predefined widget named Case description. Drag and drop the Case description field from the Predefined bindings in the shape repository below the output widget on the form and rename it’s label to Travel notes.

We can now additionally add some description to the notes field to let the travel agent now, that he should enter additional information in there. Select the travel notes field and click in the Description attribute on the right side. Enter something like Please add any additional information for the travel in here. You can also directly edit that description by double-clicking it directly in the form widget.

The form should now look like this:

form design 01

Save the form with name Manage Travel Request Task Form. In order to re-use the output widget with the all the information in it, select the output text area widget and copy it. Now you can close the form designer browser tab, which should take you back to the process modeler.

Create second task form

Switch back to the process modeler browser tab and click on the Add reference context menu for the second task named Review Travel Bookings and select New again. In the new form designer, paste the output widget we previously copied from the first user task form. Note that copy / paste is supported not only within the same model, but also across models, which is quite handy sometimes.

Double click the output widget in order to adapt it’s content to reflect the current task we want to create the form for. As this is just a review task, we also include the traveling notes in there directly rather than put the description field as we did in the previous form.

user task design 02

Now save the task form, name it Review Travel Bookings Task Form and close the form designer browser tab.

4.3.8. Redeploy the App and test again

We added two new task forms and attached them to the appropriate user tasks in the process. So let’s test it by deploying the App again, create a new travel request and step through the process.

Make sure you save the process model too as we now mapped the newly created forms in there, before you redeploy the App!

After redeploying the App again, create a new case again, click on the children view and select the manage travel request task, you should now see the specific task form we designed:

prc testing 01

Add some notes, complete the task and check, whether the data in the second task is reflected correctly.

4.4. Improving the forms

In the last two chapters, we created a new App, a case model, a case initialization form, a management process as well as task forms in order to reflect our needs for the travel request case.

In this chapter we will further improve the forms, making them user-friendlier and add an attachment widget to it and later turn it into a subform and reuse that in other forms.

4.4.1. Add an attachment widget to the case form

Instead of having to manually upload any booking documents, let’s place an attachment widget directly to the case form for easy access to this capability.

First switch back to the App in the modeler dashboard, navigate to the Travel Request Form and click Design.

Let’s place a comment for the attachment widget for the user to understand how to use it in our context. Drag and drop an Output textarea to the bottom of the form, don’t add a label but double-click the widget to open the rich editor and enter something like this:

form design 01

Below the output widget, drag and drop a new Attachment widget from the Component section of the shape repository and name it Attachments / bookings. Similar to text data bindings, the attachment widget can also be bound to a variable, which will hold all information about the attachments uploaded. Use the binding {{attachments}} for that matter and your form should look like this:

form design 02

Now let’s fine-tune the widget by selecting it and change some attributes. First, let’s customize the Select file message by exchanging the default value to Upload any booking documents here or similar. Set the Preview type to Thumbnail as we want to include a preview in the attachments, if available. Set the Thumbnail maximum height to 200 to prevent big pictures from useing to much space on the form.

4.4.2. Redeploy and migrate existing cases to reflect new version

Now save the case form and redeploy the App. Instead of again creating a new case, let’s have a look at how versioning is done while deploying an App.

Understanding App and model versioning

Whenever an App is deployed, all of the models within that App will be deployed to the runtime of the edoras one platform and so they become available within the user dashboard. An App is deployed as a new version every time you deploy it to make it fully consistent and compatible with all models. By default, the deploy mechanism is using a careful strategy by don’t migrating existing instance like cases, processes and tasks. This means, they will still be mapped to exactly the version they have been created with until they are either completed or migrated to a newer version.

Migrate existing travel request cases to newest version

For now, we want our cases to already reflect the newest change within the form, the attachment widget, so go to the Tutorial App and navigate to the Travel Request case model by selecting it. Click the Migrate action in the action menu of the case model and migrate all existing cases to the newest, just redeployed version of the App.

Switch back to the user form and select one of your previously created travel request cases to make sure they reflect the newest version. You should see the attachment widget by now:

case testing 01

Click the upload icon and attach an image for instance. You will have to save the case form in order to see the preview (thumbnail) of the uploaded image:

case testing 02

4.4.3. Turn the attachment section into a subform

Now we also want to add that attachment widget to the task forms. We could do this the same way as we did before by copy / paste it. But this time we want to learn a new feature within forms, sub forms, a great way for re-using existing parts of forms, either due to technical reasons or due to design reasons, doesn’t matter.

Go to the Tutorial App and click Create, select Create Form Model and enter Attachment Section Sub Form as its name and then click Create and Design to directly open it in the form designer.

First, go back to the case form designer and select the output widget AND the attachment widget and copy them into the clipboard. Switch back to the new, still empty subform and paste it there. You should now have the subform look like this:

form design 03

Now save the subform and switch back to the case form designer to add it there, but first we need to understand on how the binding works for subforms.

Understanding bindings in subforms

In order to use a subform in different other forms and thus different contexts, it is essential to isolate its data bindings and make it configurable outside of the subform where it is used.

Assume a subform with two fields in it with bindings to {{foo}} and {{bar}}. The subform should be used within a case form as well as within a task form but displaying the same data. Or the subform should even be used several times on the same form, but with different meanings (e.g. an address subform used twice, once for the main address another one for the billing address). In order to be able to deal with situations like this, the subform itself needs a data binding as well, following the exact same rules as explained previously in this tutorial. For instance, if the subform is bound to {{mySubFormData}}, there will be a variable named mySubFormData which itself will be a map holding all data contained in that subform.

Example 1:

  • subform having two fields bound to {{address}} and {{city}}, respectively

  • the same subform is used twice on another form

    • the first representing the main address is bound to {{mainAddress}}

    • the second representing the billing address is bound to {{billingAddress}}

  • this results in possible expressions (e.g. within an output widget)

    • {{mainAddress.city}} represents the city of the main address

    • {{billingAddress.address}} represents the address data of the billing address

Example 2:

  • a subform having fields {{foo}} and {{bar}}

  • the subform is bound to {{$this}} (a special expression representing the very same context where the subform is used)

  • this will result in the very same behavior as if the two fields would have been placed on the form directly rather than within the subform

Example 3:

  • a subform having fields {{foo}} and {{bar}}

  • the subform is used within a case form with binding {{$this}}

  • the same subform is used in a task form

    • the subform should always represent the very same data of the current case

    • the subform therefore needs a binding {{root}} to map it to the root context in order to display the same data as within the case form

Example 4:

  • same as example 3, but the subform binding in the case form is {{myData}}

  • the subform binding within the task form therefore needs to be {{root.myData}} in order to reflect the same data as with the case form

Understanding different types of subforms

Now we learned the basics for the binding / isolation of data in subforms. There are some possibilities when it comes to which form is actually used as the subform as well as whether the subform should have one entry or is able to show multiple entries.

Single-entry or multi-entry subform?

Subform widgets can be configured to hold exactly one entry (single-entry) or can hold zero to multiple entries. The difference is how the data is mapped, single-entry subforms will store its data in a map directly within the variable they are bound to. Multi-entry subforms will store the data into a collection (list) of maps in the variable they are bound to and thus are able to hold 0 to n entries.

Single- or multi-type subform?

There are basically two types of subform widgets available; single-type or multi-type subforms. The single-type subform will render just one type of form for all entries it is showing. The multi-type subform can hold different types of forms and uses something called a discriminator variable to distinguish which form should be used for which entry in the list. Assume a subform showing a list of selected products. For each entry (each product), there might be a different type of form necessary in order to represent the according product information. The discriminator could then be mapped to the type of the product and hence the appropriate product form would be used in the subform to represent that product the best way.

Adding the subform to the case form

Now having learned the basics for how subforms are bound to data and for the different types of subforms, let’s use it in our case form to represent the attachment section of the request.

Go to the form designer for the Travel Request Form, if not already there and delete the output textarea and the attachment widget as we want to replace them with the subform we just created.

Drag and drop a Single-type subform from the Components section of the shape repository onto the bottom of the case form and label it Attachments.

Click the + button at the bottom of the widget to select the subform we want to use and select Reference and then the Attachment Section Sub Form to be used as the form reference.

Double click the subform or click the Value attribute to enter the binding for the subform. As we want the very same behavior as we had before without the subform, bind it to {{$this}}, meaning the data in the subform will directly be stored in the case (as before, so we will even be able to migrate the existing cases without having to migrate any data).

Make sure Multiple entries is turned off in the Specific attributes section. You can choose whether you want a border around the subform or not. Turn it off to be as much identical as we had before adding the subform.

Because we entered a label name for the subform (optional), we can choose to remove the label from the attachment widget to avoid duplicate information on the form. To do so, click the subform-name-link at the bottom of the subform widget to directly open it in the form editor.

Redeploy, migrate and test again with the subform

Go to the Tutorial App, deploy it again, navigate to the Travel Request case model and migrate the existing cases, switch into the user dashboard and navigate to one of your previously created cases and you should see pretty much the same as before, but now done with a subform:

case testing 03
Add the subform to the task forms as well

Now as we created the subform, let’s add it to both the user task forms as well, so we have all information available in the different contexts of the process.

Open both task forms in the form designer. You can do that by either going back to the process model and click the + button the user task to open the corresponding form, you can go to the modeler dashboard to select the form in the My Form Models widget or you can navigate to the Tutorial App and choose the appropriate forms from there.

Once opened in the form designer, drag and drop a Single-type subform at the bottom of the form and label it Attachments. As we’ve done before within the case form, click the + button at the bottom of the subform and select the Attachment Section Sub Form.

Turn off the border and make sure multiple entries are turned off. Now the binding will be slightly different than we used on the case form. Remember that we bound the subform to {{$this}}, meaning, its data is directly stored within the case itself. In order to show the same data in the context of the task, we now need to bind it to the case, which is {{root}}.

So for the travel management task form, this should look like this:

form design 04

For the other user task form, you can simply copy / paste the subform widget from the other task form (not from the case form though as the binding is different there).

Redeploy, migrate and test again with the subform in the tasks

Go to the Tutorial App, deploy it again, navigate to the Travel Request case model and migrate the existing cases, switch into the user dashboard and navigate to one of your previously created cases (already having at least one attachment).

Now let’s see how we can start the process ad-hoc. Remember than the first time, the process was started automatically when we created the case. Now let’s see how this can be done manually.

Click the Start Process action in the action menu and select Travel Request Management Process and click Start. This will start the process in the same case again. This is how ad-hoc processes can be manually started as an alternative to automatically start them during the creation of the case.

As we had in the beginning, the children view of the case shows one task as we re-started the process again in the case. Navigate to the task and check, whether you see the attachments too there:

case testing 04

4.5. Ad-hoc tasks and process improvements

In this chapter we will include a requestor <–> agent feedback loop to the process. If the travel agent needs more information, clarification, decisions about which flight or hotel to choose from and the like, we will support that by adding a feedback-loop to the process.

But first find out, how this could be done by using ad-hoc tasks.

4.5.1. Getting familiar with ad-hoc tasks

Assume the problem as described above. There are several options how the travel agent could solve it:

  • Take the phone, contact the requestor and clarify the open questions

  • Send an email to the requestor and clarify the open questions

  • Create an ad-hoc tasks within the case, assign it to the requestor and clarify the open questions

  • Add that feedback-loop to the process for better support

The first two options all work well, but are not integrated in a way that the new information is available within the case automatically. In a big organization there might be more than one travel agent working and they would need to share that information somehow, so everyone is always up to date.

As a first step, let’s create an appropriate ad-hoc task model to support that directly within the case. In parallel to the running process, the user can create ad-hoc tasks anytime, if we allow that in the case model (which is the default).

4.5.2. Create ad-hoc task init form

Ad-hoc tasks can be built with forms the same way we created forms so far. Go to the Tutorial App, click Create, select Create Form Model and name it Ad-hoc Feedback Init Task Form. Click Create and Design to open the new form in the form designer.

This time we will also learn the difference between an init- and a work-form. So far we used the same form in the case for both, the init- and the work-form. Additionally we will use a subform to show the data of the case with the case form itself, a nice way to ensure all data of a case is shown directly in another context.

First of all, we want to be able to set a subject for the ad-hoc task (like the subject you would put in an email). We can use one of the predefined widgets, drag the Name widget from the Predefined bindings section of the shape repository onto the form and rename the label to Task subject.

For the description of the task (e.g. the request for additional information, review or whatever) we will also use a predefined widget, so drag and drop the Description widget to the form and rename its label to Task description. Click the star button of the description widget to make it required in order to enforce the user to specify the task description.

Remember, we are still designing the init-form of the ad-hoc task, which is only used during the creation of the new task, so we also want to be able to select the assignee for that ad-hoc task. There is also a predefined widget to do exactly that, so drag and drop the Assignee selection widget from the Predefined selections section onto the form and rename its label to Assignee for the task. Let’s also add a due-date for the task (one of the default fields as well), so drag the Due date widget of the Predefined bindings section and drop it onto the assignee field in the right part to split that row up into two parts and rename its label to Task due date.

Now the form should look like this:

form design 01

4.5.3. Create ad-hoc task work form

As mentioned previously, this time we want different forms for initializing (creating) a new ad-hoc task and for working on it, so go back to the Tutorial App and select the Ad-hoc Feedback Init Task Form and click the Duplicate action as we want to create a copy of the form so we only have to change what’s necessary between the init- and work-form. In the duplicate name field write Ad-hoc Feedback Work Task Form and click Ok to copy it. Go back to the Tutorial App (link in the header) and select that newly created duplicate form and click Design to open it in the form designer.

Now we only have to modify it according to our needs. It doesn’t make sense to change the subject and description, so let’s make them read-only by setting their attributes Editable to false. Next remove the assignee selection and task due date, we don’t want to modify them.

As the requestor of the initial case, we want to be able to modify the case data itself, so let’s add that case information by using a subform. Previously we saw how data from the case can be shown in a task form by using an output textarea widget and then manually add all necessary data with appropriate expressions to it. This is one option, now let’s see a more generic way to include the case data in the task form by adding it as a read-only subform using the very same form as we used for the case itself. This way, we make sure that whenever we add new stuff to the case form, it will automatically be shown up in this task form as well, because it’s a subform. Additionally we want to be able to show and hide this case information through a checkbox.

Drag and drop a Checkbox widget to the form, label it Show case information and bind it to {{showCaseInfo}}. Check the Default value attribute to set it to true by default (so the case info is visible by default).

Now drag and drop a Single-type subform below the checkbox and label it Case information. Click the + button and select Reference and then Travel Request Form, our primary case form. Now we need the binding for the subform. Remember, we want to show the case information in there, so we use {{root}} for the binding which will map to the current case where the ad-hoc task is created within. Last thing to do is to set the Visible (RT) attribute to the same binding as the checkbox {{showCaseInfo}} to make the visibility flag of the subform dependent of the state of the checkbox. Visible (RT) means visible runtime and let you define any expression, which must resolve to either true or false and the visibility is dynamic depending on the expression’s value. Also note that expressions are evaluated in real-time directly within the form engine of edoras one, so whenever the data upon the expression is based is changed (in our case it’s the checkbox), the visibility flag is automatically adjusted accordingly. Make sure we have Show border set to true and Multiple entries to false as there is exactly one case instance to be shown and we want it in a border to visualize its data coming from somewhere else.

The interesting part now is that we can even modify data in one single form for both, the task and the case instance simultaneously, so the user must not even be aware, in which context which data is coming from or being saved.

The ad-hoc task work form now should look similar to this (the attribute section is the one from the selected subform):

form design 02

Now save the form and go back to the Tutorial App for our next step.

4.5.4. Create ad-hoc task model

Go to the Tutorial App and click Create and choose Create Task Model. Set the name to Travel Feedback Task.

Now select the appropriate init- and work-form in the Init Form and Work Form selections of the task model and create the model by clicking Create.

task model 01

4.5.5. Deploy App and test ad-hoc task

Navigate back to the Tutorial App and deploy it again which will make our new ad-hoc task available to be used.

Switch to the user dashboard and select one of your previously created travel request cases. Now click Create and choose Create Task. If you do that in the context of a currently selected case, it should be automatically preselected in the Parent Case selection. Now choose the Travel Feedback Task Template, fill out a task subject and description, choose yourself as the assignee (so you don’t have to logout / login to complete it) and optionally also choose a due date.

prc testing 01

Click Create to create the ad-hoc task. If you check Create another before hitting Create, the new task would be created and you return back to the same init-form in order to create another one.

Going to the newly created ad-hoc there are several things you should note here. First of all you should be able to show / hide the case information by simply clicking the Show case information checkbox. Also note that the information in there reflects the case information and you are able to modify them directly while in that task. You can test that by changing some case information in there, click Save (not yet Complete) and navigate to the case and check whether the data in the case itself have been updated accordingly.

To complete the task, simply click Complete and it will be finished. By default completed tasks are not visible in the children list of a case. If you still would like to see them, go to the children list of the case and select status All within the search box and you will see all work items of the case, even completed ones.

4.5.6. Add feedback loop to the process

As an alternative to the ad-hoc task we just created, we now want to incorporate that agent < - > requestor feedback loop directly into the management process.

And here is the plan on how to do it:

  • Add next step selection in the management task form in order to let the travel agent choose between different options (e.g. need more information, travel organized, etc)

  • Add gateway after that task to select the appropriate sequence flow according the selected next step

  • Add feedback-task as a new path to the process which will loop back to the management task

  • In the review task at the end of the project add a new checkbox to the form where the requestor has the ability to loop back to the agent, if he is not yet happy with how the travel was organized

Add next step selection possibility to management task

Navigate to our travel request management process and open it using the Design action (if the process modeler is not open yet).

In the process model click the + button of the Manage Travel Request user task in order to open the form designer for the task form.

In order to separate the previous form elements from the next one, drag a new Horizontal line from the Component section and drop it right after the Travel notes field.

Drag a Static select widget from the Component section onto the form between the travel notes and the attachment subform and drop it in between. Label it Select next step and bind it to {{parent.nextStep}}. Why did we use the parent prefix? Note that our form is used within a user task created out of a process. If we want to be able to use the data in the nextStep variable in the process context we must not store the data in the local task context, but rather in the process context (parent-prefix) or even the case context (root-prefix). As this information is only needed during the lifecycle of the process, we decide to store it there and not in the case context.

Let’s make the selection mandatory by clicking the red star or setting the attribute Required to true. Now open the Options attribute dialog by clicking its attribute value and enter two options in there:

  • Name: Need more information from requestor, Value: moreInfo

  • Name: Finish organization, travel has been fully organized, Value: finish

The name column reflects the label used for that option whereas the value is used to store the selection in the mapped variable.

If we choose the first option, we want to show a new note field where the agent can specify what kind of information is needed from the requestor. So let’s drag and drop a Rich textarea from the Components section of the shape repository below the selection widget on the form and label it Information request. Make that field required by clicking the red star but note that the required-validation will only take place, if the field is shown, if it stays hidden, there is obviously nothing to be entered and hence the validation will to take it into consideration. Bind the field to {{root.infoRequest}}, we want to make that information persistent, even after the process has been ended, so let’s store it within the case context and that’s why we use the root prefix instead of parent like we did with the nextStep variable.

As we are already thinking ahead when the feedback loop returns from the requestor back to the agent, we want to show the information the requestor entered, so let’s add an output textarea showing that information. Drag and drop a new Output textara below the information request field and leave the label empty. Double click it and add the following content to it:

form design 03

We only want to show the information request field, if the next step selected is moreInfo, so let’s select the Information request field and enter {{parent.nextStep=='moreInfo'}} in the Visible (RT) attribute, so it will only be visible if we select Need more info… in the next step selection.

The output text area should only be visible, if it has content, so let’s enter {{root.infoRequestAnswer}} in the Visible (RT) attribute, so it will only be visible, if the variable root.infoRequestAnswer has content.

The form should now look like this:

form design 04
Add feedback loop to the process model

We now have to add the feedback loop to the process model so let’s open it within the process designer.

Still remembering the spacing tool? We can make use of it now by adding some extra space after the management task in order to add the gateway after the user task.

prc design 01

Now drag a new Exclusive gateway from the Execution section of the shape repository and drop it directly onto the sequence flow after the user task. As soon as the sequence flow becomes green, we can drop it as the modeler has recognized that we want to drop it in between the two user tasks.

prc design 02

As you can see the modeler automatically splits the sequence flow into two and adds the gateway automatically in between.

Select the gateway element (if not yet selected) and drag / drop the small user task icon in order to add another outgoing sequence flow with a user task after the gateway:

prc design 03

Name the new user task Answer Info Request and align it right above the gateway and on the same horizontal line as the other requestor task:

prc design 04

Select the new user task and drag the arrow icon next to it onto the Manage Travel Request user task until it turn green and drop it in order to connect the two tasks with a sequence flow.

prc design 05

We now modeled the feedback loop after the gateway. Next step is to tell the engine which flow to use after the gateway by specifying conditions on all outgoing sequence flows.

Select the sequence flow between the gateway and the Answer Info Request user task and click into the Condition expression attribute and enter #{nextStep=='moreInfo'} as the expression. We will explain the difference in the expression syntax in just a minute, but first let’s add a label to the sequence flow. This can be done by clicking the Name attribute and entering the name or simply by double-clicking the sequence flow directly in the process canvas. Let’s name it more info.

Now select the sequence flow between the gateway and the Review Travel Bookings user task and enter #{nextStep=='finish'} in the Condition expression attribute and name it finish.

It is a good practice to add a label to the gateway element. click the gateway and type Decision?

prc design 06
Getting familiar with frontend and backend expressions

You probably noticed that there is a difference in the syntax for frontend expressions as being used within forms and backend expressions as used now in the process model. The difference is where they are being evaluated, directly within the browser (frontend / forms) or on the server (backend, everywhere else like process, email templates, document templates, etc). As the frontend expression are limited to whatever the browser and form engine know in contrast to the expressions on the server side which could even contain your own service calls, functions, any navigation through the work item network and much more, that’s why we use different syntaxes to make difference very prominent to the modeler.

But now back to our sequence flow conditions #{nextStep=='moreInfo'}. Why didn’t we use parent.nextStep as we did in the form? It is very important to always remember in which context an expression is evaluated. In the user task form we are in the context of a task work item, which is the child element of the process work item. The expression on the sequence flow however is evaluated in the context of the process itself and thus we don’t need the parent prefix as we did in the task context.

Create user task form for the answer task

We now have to create the user task form for the new task added to the process. If you still remember, you can simply click the + icon, select New and it will create a new form, attach it to the user task and open up within the form designer.

Let’s add an Output textarea to the form so we can add the necessary information in there. Leave the label empty, double-click the widget and add the following content to it:

form design 05

Note that we can include expressions everywhere in the content as we like.

Below the output field add a new Rich textarea widget, label it Your answer and bind it to {{root.infoRequestAnswer}} (the same expression as we already used in the management task form). Additionally make it required by clicking the red star icon.

The form will now look like this:

form design 06

Now save the form, name it Answer Info Request Task Form and close it in order to return back to the process design.

Save the process model as well with the feedback loop in order to test it.

4.5.7. Redeploy and test process with feedback loop

Go back to the Tutorial App and redeploy it. Now switch to the user dashboard and create a new case with our Travel Request template.

Did you notice the new attachment widget also visible in the init form? If you don’t want to be able to already upload attachments while initializing the travel request, simply add {{root.id}}` within the `Visible (RT)` attribute of the subform widget holding the attachment subform in the `Travel Request Form and it will not show up as long as the case is not initialized (and hence the root.id variable is not available).

If you navigate the Manage travel request task within the children view of the case, you will see the updated management task form. Select Need more information…` from the `Next step selection and then the new Information request field should appear where you can enter the information you need from the requestor:

prc testing 02

Did you notice the rich capabilities within the text editor? If selecting a text fragment a toolbox appears where you can choose different formatting’s:

prc testing 03

If you now click Complete the next task will be the answer info request task where you can provide the answer and then the process is going back to the management task. You can use this feedback loop as many times as you want. As soon as you choose Finish… the process goes into the review task and will finish.

Did you notice the pre-selected next step combobox when returning back to the management user task? Remember that we bound the selection of this widget to parent.nextStep and as the selection is stored in the process, it’s obviously remembered the next time that information is used again. As we don’t wont that behavior, let’s go back to the process and add a service task to reset that value before we hit the management task again.

Add service task to initialize variables to the process

Make some more space before the Manage Travel Request user task in order to add a service task there. Afterwards drag a new Initialize variables task from the Service tasks section directly onto the sequence flow between the start event and the user task and name it Reset next step selection.

Move the sequence flow end of the flow between the answer and the manage task to the new reset service task by dragging the green point at the end of the arrow and dropping it onto the reset service task. If you want to re-arrange the sequence flows, there are several handles you can do that with. There is a red point in every edge of the sequence flow to move that edge with. Adding a new edge is easy too, simply point anywhere on the sequence flow and drag / move the red point which is appearing. You can move a sequence flow in parallel by moving the red rectangle appearing in the middle of the flow.

Your process model should now look like:

prc design 07

Of course we need to tell the engine now which variables to initialize within the reset service task we just added. Select the service task and click the Init variables attribute where a dialog appears to enter the variables we want to initialize or reset as in our case.

The first column can be left empty as our nextStep variable is in the process scope. Write nextStep in the variable name column and leave the last column empty as well as we simply want to clear the previous selection. If you also want to clear the previously entered information request text, add an extra row to the dialog which should then look the following way:

prc design 08

Close the dialog and set the attribute Overwrite if existing to true (checked and green). This is necessary, as we explicitly want the variables to be overwritten if they already exist. If you would leave that option to false, it would only initialize variables not yet existing and would leave existing variable values untouched.

Save the process model and go back to the Tutorial App in order to redeploy it and test the slightly changed behavior.

4.6. Model responsibilities / assignees

4.6.1. Getting familiar with responsibilities and sharing

Before we design the responsibilities in our process, let’s have a look at the permission concept behind edoras one. Every work item (a case, task, document, etc) can have the following information concerning responsibilities and sharing:

Owner

By default, the creator of a new work item becomes the owner of it, it’s doesn’t mean, he is responsible for working with it, but more in a sense of being in the loop and always informed about what’s happening with that work item.

Assignee

The assignee is the responsible person for working and completing a work item. If manually set, the assignee is typically selected by the owner of a work item.

Candidate groups

Setting one or more candidate groups makes the work item available to all users being a member of at least one of the candidate groups. Assume you have a group named "support" and use it as the candidate group of a support case. This makes all users being a member of the support group a potential assignee for the support case. All users being at least member of one of the candidate groups will have access to the work item.

Candidate users

The candidate users work the same way as the candidate groups, but instead of adding a group of users, you can directly add selected users to become candidate users. They also will have access to the work item.

In order to have access to a work item, you must either be the owner or the assignee of it or be a member of at least one of the candidate groups or you must have been added as a candidate user.

4.6.2. Getting familiar with account-, group- and user-management

If we want to use groups and users in our App, it is important to also understand how they are managed in edoras one. Go to the Administrator dashboard by selecting it from the Dashboard menu (you need to have administrator permission in order to have access to this dashboard of course). You now will see an owerview of your accounts, groups and users in your tenant.

Accounts

Accounts are used to divide your tenant into organizational units, locations or whatever you want. Groups and users are then created as part of an account. As for now, the account does not have any impact on visibility of work items, as we learned in the previous chapter, only groups and users are directly responsible for the visibility of work items.
You can choose alternative main-, background- and highlight-colors as well as your own logo as part of the account settings. To do so, select your current account and give it a try by choosing 004893 as the Main Color, FFFFDF as the Background Color and F2A33D as the Highlight Color. As soon as you save the account settings and reload the current page in the browser, your edoras one now should look like this:

admin 01

Simply remove any settings in order to use the default colors back again.

To create a new Account, click Create or hit C and choose Create Account from the dialog, fill out the necessary information and create it by clicking Create.

Groups

Groups are used to specify a set of users. The meaning of a Group however, is fully up to you. It could represent a group of people within your organization (e.g. the support team, HR or sales) or it could be used to represent a role (e.g. approver, travel agent). By adding a group to a user, it becomes a member of that group or role and hence will have access to all work items this group is used as a candidate group as we learned in the previous chapter.
To create a new Group, click Create or hit C and choose Create Group from the dialog, fill out the necessary information and create it by clicking Create.

Users

Users are needed in order to let people log in to edoras one and participate in the work. All the users settings are managed within his user profile. The profile can be edited by the user itself (without the group permissions of course) and any administrator.
To create a new User, click Create or hit C and choose Create User from the dialog, fill out the necessary information and create it by clicking Create. It’s a good practice to use the email address as the login name, although this is not a necessity.

Good Practice

Although accounts are not directly used for work item visibility, it is a good practice to create a Group for each Account having exactly the same name and use it to share work items with all users from that account. Of course the users need to be a member of that group as well. In a future release, accounts might become more visible as they now are, so this could change.

4.6.3. Adding travel agent group and requestor to task responsibilities

As we already learned earlier, participants, groups and roles are modeled through pools and lanes in BPMN 2.
We currently have two different participants in our process; the requestor of the travel request and the travel agent managing the request.

Creating a group for the travel agents

As we want to use a group for the travel agent assignment, let’s create a new group for it. Go to the Administrator Dashboard and click Create or hit C and choose Create Group from the dialog. Choose your primary account to which the group should be added (not the admin account) and set the groups name to Travel Agents for instance and optionally add a description for that group, then hit Create in order to create our new group:

admin 02

Add yourself to the newly created group in order to easily test the adapted process later (so you don’t have to logout / login in order to see and complete the travel agents tasks). In order to do that, choose User profile in the upper right corner menu, choose Manage Groups and additionally select the new Travel Agents group, then hit Ok in order to make yourself a member of the new group.

admin 03
Modeling the initial requestor’s responsibilities

As the initial requestor of the travel request, he will be responsible to answer any open questions and eventually review the travel bookings. In order to configure that, navigate to our travel request management process and open it using the Design action (if the process modeler is not open yet).
Select the Answer Info Request user task and go to the Assignee (RT) property. RT means Runtime and allows us to use an expression for the assignee during runtime rather than a fixed user during design time. As we already know, the owner of a work item is set to the current user by default, so we simply can use an expression of the owner of our root case to set the assignee of this task to the same user. Set the expression to {root.ownerId} in order to automatically assign this user task to the same user as the owner of our travel request case. Remember that this is a backend expression, so we need {} to tell the process engine what to do.

Your process should now look like this:

assignee 01

Now select the Review Travel Bookings user task and do the same; set the Assignee (RT) property to #{root.ownerId}.

Modeling the travel agents responsibilities

For the travel agents tasks we will use the candidate group feature. This means that the task is not assigned to a single user directly but rather to a group of users. So we leave the Assignee (RT) property empty but will set the Candidate groups property instead.
Select the Manage Travel Request user task and select Travel Agents from the Candidate groups property selection combobox:

assignee 02

If the Travel Agents group does not appear, you might need to reload the process diagram first (only needed, if you created the new group after you opened the process diagram).

Now save the process model and go back to the tutorial App in order to redeploy it. If you create a new travel request, you should now see that the assignee for the requestor tasks is automatically set to the same user starting the request and the management task is assigned to the Travel Agents group and will appear in all members task list.

Use current user for future management tasks

In our previous step, we assigned the travel management task to a group and didn’t use a direct user assignment. This way, there might be several users potentially claiming that management task and organize the travel request. If there is no direct assignee set, the task will appear in the list of tasks in all users personal task list being a member of the group. But remember, we have a feedback-loop built in to the travel request management process. Now assume the travel agent needs some more information and afterwards the process goes back to the management task, but this time, we would like to automatically assign it to the same travel agent previously in charge and not the whole travel agent group.
In order to do that, we want to store the travel agent within the process instance after completing the management task in order to later use it again for the assignee of the management task.

Open the travel request management process (if not yet open) and make some space using the spacing tool after the Manage Travel Request user task. Drag and drop a new Initialize variables service task from the shape repository and drop it onto the sequence flow after the Manage Travel Request user task and name it Store travel agent user. Whenever the previous user task is completed, this service task is executed and will store the current user id to a process variable.

In the property section, select Overwrite if existing (true, green mark), so the variable will always be overwritten with the most current travel agent. Now click Init variables property and fill in the following information:

assignee 03

Set the first cell to root as we want to store the travel agent within the case. We choose travelAgent as the variable name to store the current user id. For the value, we use the #{currentUserId} expression, which will always return the id of the current user, which in our case must be the travel manager just having completed the management task.

Our process should now look like this:

assignee 04

In order to use that information, select the Manage Travel Request user task and set #{root.travelAgent} as the 'Assignee (RT)' property, but still leave the candidate group property selected with our Travel Agents group:

assignee 05

What happens now? Whenever the management task is completed, the current user id is saved in a case variable called travelAgent and then used again as the assignee expression if the management task is ever created again. This will result in an automatic assignment to the same travel agent handling the current travel request.

But now we still have a problem; the travelAgent variable is initialized the first time AFTER the management task, but the expression is used BEFORE that, so we need to add another Initialize variables task at the very beginning of the process in order to initialize this variable in the case.
In order to do that, make some space just before the Reset next step selection task and drop a new Initialize variables task onto the sequence flow and name it Initialize travel agent variable. Select the Init variables property and enter the following information:

assignee 06

This time, we leave the value empty as we just want to initialize the variable, we don’t know the travel agent yet at this point of the process.

Now your process should look like this:

assignee 07

Now save the process model and go back to the tutorial App in order to redeploy it. If you create a new travel request, use at least one feedback loop in order to create more than one management user task, you will see that the first one is not assigned to a particular user but every further management task is assigned to the same user having completed the previous one.

4.7. Add approvers to the process

In this chapter we want to add optional approvers to the travel request. Lets assume the travel agent has some regulations to follow like if the travel exceeds a certain amount of costs or if it includes traveling by airplane there must be an approval from one or more approvers.
In order to keep things easy, we simply add a new possibility to the Select next step combobox in the Manage Travel Request user task to go into an approval phase of the travel request, so it’s up to the travel agent to decide whether an approval is necessary or not.
As a first step, we just add one single approver and later, we extend it with as many as necessary. This way, we can learn how to use multi-subforms in combination with a multi-instance task to loop over the selected approvers.

4.7.1. Extend travel management task form for optional approval

First we need to extend the travel management task form by adding a new possible next step.
Open the Travel Request Management process and click the + sign within the Manage Travel Request user task in order to open the task form.

Select the Select next step combobox and click the Options property. Now add a new option by clicking the Add button. Add the new approval option like the following:

approval 01

You can move the new entry by selecting its row and click the up-arrow.

If the user selects this option, we want to additionally display a text field to enter any additional approval notes and of course select the approving user.
Drag and drop a new Rich textarea just below the Information request text area, name it Approval notes and bind it to {{root.approvalNotes}}. We only want to show this new field, whenever the new step is selected, so set the Visible (RT) property to {{parent.nextStep=='approval'}}.

From the Predefined selections section of the shape repository, choose the User selection widget just below the Approval notes text area and name it Select approver. Change the binding expression to {{root.approver}}, make it required and set the Visible (RT) property to {{parent.nextStep=='approval'}} as well.

Your task form should now look like this:

approval 02

Now save the task form and switch back to the process in order to extend it with this new approval functionality.

4.7.2. Add approval step to the process

As we will add a new role to the process, let’s add a new lane to represent it. Drag and drop a Lane below the Travel Agent lane onto the existing process pool and name it Approver:

approval 03

From the existing gateway, drag a new user task out of it and drop it within the newly added Approver lane and name it Approve Travel Request.
Now select the sequence flow between the gateway and the newly added user task and name it approval. Furthermore, we need to tell the engine when to actually use that flow and so set the Condition expression to #{nextStep=='approval'}.

approval 04

Now drag a new sequence flow out of the new approval task and connect it to the existing Reset next step selection task.
There are now three incoming sequence flows to that task and it is a good practice to use a joining gateway before that task for better visibility of the flow, although this is not necessary by the BPMN 2 standard.
In order to do that, make some more space (using the spacing tool, still remember?) before the Reset next step selection task and drop a new Exclusive gateway onto the sequence flow. Now you have to re-connect the existing sequence flow leading into the Reset next step selection task to the newly added gateway. You can do so by hovering over the sequence flow you want to re-connect until the green circle handles become available and then drag and drop it using those green handles to the new gateway.

Your process should now look like this:

approval 05

We now need to set the assignee for the approval task we selected in the management task. Select the Approve Travel Request user task and set the Assignee (RT) property to #{root.approver}.

The only missing thing now is the approval task form, so lets create one. Click the + icon of the Approve Travel Request user task and choose New as the option in order to create a new task form.

Drag and drop a new Output textarea to the form in order to add some information for this approval task. Double-click it in order to open the editor for its content and add the following to it:

approval 06

Please note the expressions we used in the output widget, first of all, we used frontend expressions using double-curly braces as they will be rendered in the form engine running on the frontend. {{root.name}} actually renders the travel requests name (case name or root name), the {{root.approvalNotes}} is the note field we added within the management task form for the travel agent to add special notes for the approver and finally we have {{root.travelAgent}}. Remember, it will be automatically set after the management task has been completed by the travel agent using a Initialize variable task.

Now let’s add the decision widget for the approver, we basically have two options; using a checkbox (for instance if we have yes / no options) or using a selection combobox, which is a little bit more verbose and of course could have more than two options.

Drag and drop a Static select widget below the output text area and name it Please make your decision. Then double-click the widget and enter {{root.approvalDecision}} for the data binding (or enter it in the value property). Also make the field required (my either clicking the red small star of the widget or checking the Required property).

Now click the Options property and enter the following values to the dialog:

approval 07

Finally, let’s add a Rick textarea, name it Decision notes and set the data binding to {{root.approvalDecisionNotes}} in order to let the approver add any special notes about the decision.

The form should now look like this:

approval 08

Now save the form, name it Travel Request Approval Task Form and switch back to the process model and save it too.

What did we do? We added a new option to the Manage Travel Request task for getting an approval for the travel request, added this approval user task and routed the process back to the travel agent which then needs to decide whether to further organize the travel or stop it, if there is no approval. How does the travel agent know about the decision made by the approver? We could add this information back to the management task, so he has everything there.

Lets go back to the process and open the Manage Travel Request user task form by clicking the + icon or switch to the task form, if it is still open.
There are several options on how we could add this information. As it is a read-only information, we add it using a new output widget we only want to show, if there is an approval already done.
Add a new Output textarea and drop it below the Travel notes rich textarea, double-click it and set it to this content:

approval 09

We only want to show this widget, if we already made the approval. We can do so by using the Visible (RT) property we already used in other occasions. Select the output widget and set {{root.approvalDecision}} to the Visible (RT) property. This way, the widget will only be shown, if the approval variable is set which can only be the case after the approval task was completed.

Now save the form and go back to the Tutorial App in order to redeploy it. Start a new travel request, use the Need approval option, select an approver (e.g. yourself in order not to have to logout and login again) and you should end up in our new approval task.

approval 10

Ok, as we see the approval task now, we have some room for improvements. First of all, do you see how the expression {{root.travelAgent}} is rendered? Yes, its kind of a cryptical user-id rather than the users name. The backend would support something like this #{root.travelAgent.name} in order to get to the name of that user, but the frontend does not support that as for now.
What options do we have? Remember the Initialize variable service task we used to store the travel agent id after the Manage Travel Request user task? Lets add another variable containing the travel agents name there.
Open the Travel Request Management process and select the Store travel agent user task. Now click the Init variables property and add the following variable initialization:

approval 11

Now save the process and open the Manage Travel Approval Task Form (or switch to it, if it is still open). Double click the output widget at the top of the form and replace {{root.travelAgent}} with {{root.travelAgentName}} so will use the name rather than the user id.

Save the form, go back to the Tutorial App, redeploy it and test the process again.

Tip
In order to further improve the process, you can add the approvers name and id as a variable and render it in the output widget in the management task to let the travel agent know, which approver finally approved or declined the travel request. If you remember on how we saved the travel agents id and name and used it in the approval output widget, you should be able to do that on your own now, right?

If not, here are the basic steps:

  • Add a new Initialize variables task after the Approve Travel Request user task and add root | 'approverName' | #{currentUserId.name} to it

  • In the Manage Travel Request user task, add this information to the output widget (something like The travel request has been {{root.approvalDecision}} by {{root .approverName}}…​

4.8. Sending emails from within the process

In this chapter we want to learn how to create and use email templates within a process. The basic idea is to send out an email to the requestor and travel manager of a new travel request once it has been declined by the approver.
Of course this might not be the best way to end the process after the travel was declined but it gives us the opportunity to use email templates in our existing example.

4.8.1. Create email template

We want to create the email template as a first step, so go to the Tutorial App and hit C or click the Create button and choose Create Mail Model and name it Travel Request Declined Email Template. You can directly hit Create and specify the further template later or do it within the Create Mail Model form.

Now create the template so it looks like this:

email template 01

4.8.2. Add gateway for approval decision

In order to send the email only if the travel request was declined, we need to add a new gateway after the approval user task. Open the Travel Request Management Process and make some space at the bottom of the lane in order to add a new gateway. Reroute the existing sequence flow so the process will look similar to this:

email template 02

After having placed the new elements and rerouted the sequence flows accordingly, we need to add the conditions for the new sequence flows leaving our new gateway to tell the engine which flow to take.
So select the sequence flow with the label yes and set the property Condition expression to {root.approvalDecision=='approved'} and for the no labeled flow use the expression {root.approvalDecision=='declined'}. After this extension, whenever a travel request was declined, it will end the process directly without notification.

4.8.3. Send email if approval was declined

Ending the travel request process just because the request was not approved might not be the best solution, so we want to integrate our email template and send out a notification email once the request was declined.
Go to the Travel Request Management Process and make some space between the gateway and the end event, so we can drop a new email task onto the no sequence flow. Now drag and drop a new Send mail service task onto the no sequence flow and name it Send decline notification email.

Select the Travel Request Declined Email Template as the Mail model of the Send mail service task. This will use our email template when creating the email subject and body to be sent. We want to send the email to our initial requestor and send a CC to the travel agent. In order to do that, we use {root.ownerId} as the Mail recipient and {root.travelAgent} as the Mail CC property.
Your process should now look like the following:

email template 03
Tip
As an email recipient you can either specify a fixed defined email address like foo@bar.com or you can specify an expression resolving an email address, like {root.fooEmail} (e.g. fooEmail representing a text field where the email address was entered before) or you can specify an expression resolving to any edoras one user. In this case, the email address of that user is taken to send the email to. In our case, {root.ownerId} will resolve to our initial requestor and #{root.travelAgent} will resolve to our latest travel agent managing the travel request.

4.9. Create document models

In this chapter we will learn how to create document models which can be used in several ways as we will see.
First of all, documents can either be created manually, the same way as ad-hoc tasks are being created or they can be created as part of a process task.

Documents can have meta-information, designed using an init- and / or work-form, the same way as we did with a task model.

4.9.1. Create document meta-information form

A document might have some meta information to be stored as variables in order to classify it or to be able to search for it later. This is done using a form, the same way as forms are used for cases or tasks, they can also be used to manage the meta-information of a document.

Go to the Tutorial App and hit C or click Create and select Create Form Model, name it Travel Request Document Form and click Create and design. Drag and drop the Name and Description field onto the canvas. Maybe we directly want to select an assignee for the document, so let’s drag and drop the Assignee selection widget below the description. To share the document with other groups, we can optionally drag and drop the Share with groups selection widget as well.

If you want to have more meta information for your document, simply add more fields to the form, you can design the document form the very same way as for a task or case.

The document form should now look like this:

document model 01

If we want to be able to upload content during the creation of the document (init-form), we can drag and drop an upload-widget to it. Drag and drop the Upload widget to the top of the form (before the name field) and name it Upload document. This is a special widget, only used for uploading the content of a work item (in our case a document). That’s why we don’t need any special data binding.

We only want the upload widget, the assignee selection and the share with group field being visible during the initialization of the document but the form should be used as both, init- and work-form. There is a way to achieve that, by using the Visible (RT) property in order to show / hide a widget if the form is used as an init-form or as a work-form.

Tip
In order to show a widget only if the form is used as an init-form, use the expression {{!root.id}} as the Visible (RT) property.
If you want to hide a widget if the form is used as an init-form, use the expression {{root.id}} as the Visible (RT) property.
What does {{root.id}} mean? Well, it maps to the id of the root work item, typically the case and if we use a form as the init-form, we don’t have a case yet, that’s why we can use this expression to show or hide the widget according to the existence of the root id.

That being said, let’s add {{!root.id}} as the Visible (RT)` property for the `Upload` widget as well as for the `Assignee` and the `Share with groups widget.

The document form should now look like this:

document model 02

Save the form and go back to the tutorial app.

4.9.2. Document models with content being uploaded

Based on the document form we just created, we want to create a new document model within our Tutorial App, so go back to the tutorial app and hit C or click the Create button and select Create Document Model, name it Travel Request Document and select Travel Request Document Form for both, the Init form and Work form.
Now create it, navigate back to the Tutorial App and deploy it in order to test our new document model.

Upload new document to a case

Go back to the user dashboard and open one of your previous travel cases. The same way we created an ad-hoc task, we can create / upload a new document to the case by clicking the Create button and select Create document. In the Template selection, choose Travel Request Document and click the Add... button in order to select a document to upload. It should automatically set the name of your selected document to the form we created and it will let you change it, if you want.
You can also select the assignee for the document as well as share it with some groups, if you want.

Creating a new document should look similar to this:

document model 03

After having created it, you will notice the missing assignee, share with groups and upload widget, remember? We used the Visible (RT) flag in order to hide it for the work-form but show it within the init-form.

Getting familiar with the document views and actions
document model 04

After having created our new document, lets look at the different views and actions a document provides.
The first view (eye-icon) is the preview of the document which is currently supported for images, pdfs, open-office and microsoft office documents (word, excel and powerpoint). The second view is the work form of the document, similar to the forms of any work item (like cases or tasks). Then we have the comment view where you can add and see comments of the work item and the last two views are for developers / modelers where you can see all the data of the work item as a generic form or as a JSON representation.

And here is the list of actions:

  • Assign lets you change the owner and assignee of the work item (document, in our current case)

  • Share lets you share the work item with groups

  • Move will move the current work item to another case

  • Update Placeholders will update the content within placeholders of a document, if it was created out of a template (we will learn that later)

  • Edit Document allows you to edit the document directly within a locally installed app (like Microsoft Word for instance). This only works, if the document you trying to edit is editable in a local application (same if you would double-click it on your desktop) and if your browser supports Java applets.

  • Upload Document allows you to upload / overwrite the current content with new one.

  • Download allows you do download the content of the document to your local machine

  • Archive will close / archive the document

Tip
You can select the available actions with the form of the document model in the Tutorial App in the section Allowed Actions.

4.9.3. Document models with a file template

We already learned how we can create and use a document work item using uploadable content. In our next step, we will attach a file template, also called a skeleton, in the document model. Go back to the tutorial app and select the Travel Request Document and click the Upload skeleton button. You can now upload a document to be used as the template once we create a new document based on that model. Choose an empty Word document for instance, so we can use it later with placeholders again.
After having uploaded the document, you will see it in the preview (even if it is a Word document).

document model 05

As we now have a file template attached to the document model, we don’t need the upload widget in the init-form again, so go to the Travel Request Document Form and remove the Upload widget from it and save the form.

Go back to the Tutorial App, redeploy it and switch back to the user dashboard and navigate into one of your previous cases again. Click Create, select Create Document, select Travel Request Document, choose any name you want for the document and click Create. If you go to the Preview (eye icon), you will see the very same document you used as the skeleton in the document model.

Whenever there is a skeleton file attached to the document model in an App, it will create a copy of it and store it in the case the document was created in.

4.9.4. Document models with a file template containing placeholders

We now learned how document skeletons can be used as the template to be copied once a new document is created. In our next step, we will add placeholders to the document, so we can automatically fill in data from the case, the document is created within.
Go back to the Tutorial App, select our Travel Request Document and click Edit skeleton which should open the document in Microsoft Word. If the Java plugin for the Office integration is not supported in your browser, you can alternatively use the Download skeleton button, save it to your desktop and open the document from there.

Now write your travel request document template and use text-fields for the placeholders (you typically find them in the developer tools of Microsoft Word).
Your document could now look like this:

document model 06

Double-clicking a placeholder opens up its details:

document model 07

The important information is the maker name, we will use that later for the binding. I used the expression I’m going to map afterwards as the standard text, but this is not necessary, only a good practice to see where the data will be filled in.

I used the following placeholder information:

  • caseName, will later be bound to #{root.name}

  • travelOrigin, will later be bound to #{root.travelOrigin}

  • travelDestination, will later be bound to #{root.travelDestination}

  • outwardTripDate, will later be bound to #{root.outwardTripDate}

  • returnTripDate, will later be bound to #{root.returnTripDate}

  • approverName, will later be bound to #{root.approverName}

  • approvalDecision, will later be bound to #{root.approvalDecision}

Tip
It’s a good practice to use the same name for the text field as the variable name we want the field to be bound to later, although this is not really necessary.

Now save the Word document. If you used the Edit document action, the new version will automatically be uploaded as the new skeleton to the document model. If not, you need to use the Upload skeleton action and upload the saved Word document again.

Navigate to the Tutorial App and redeploy it, then switch to the user dashboard and select one of your previously created cases and create a new document the same way we did before.

Your document in the case should now look similar to this:

document model 08

As you can see, the request name was not properly set within the title, that’s why we used caseName as the name of the marker and this is not an available variable name in our travel request case. Furthermore, we most likely would like to change the formatting of the dates too, so let’s see how we can do that.

Mapping expressions to placeholder names in a document template

If we do not want to use a straight-through mapping by using the same name for the text marker as the name of the variable to be filled in, we can create a mapping using an expression. In order to do that, go back to our Travel Request Document model in the Tutorial App and navigate to the work-form view (the preview is active by default).

At the bottom of the view, you will see a section called Placeholders and here you can add mappings by clicking the Add Placeholder button for each mapping you want to configure. For each placeholder with such a mapping, the engine will first evaluate your expression and then use the outcome of it as the value for the text field.

Add the following placeholder configurations to the document template:

  • Key caseName with expression #{root.name}

  • Key outwardTripDate with expression #{formatDate(root.outwardTripDate, 'dd.MM.yyyy')}

  • Key returnTripDate with expression #{formatDate(root.returnTripDate, 'dd.MM.yyyy')}

The placeholder section should now look like this:

document model 09
Tip
We used a function in our expression called formatDate(date, format) which takes a date object as the first parameter and a date formatting string as the second. Please refer to the reference documentation to see further functions and how to use the expression language.

Now save the document model, go to the Tutorial App, redeploy it and create another document in your case which should now look similar to this:

document model 10

4.9.5. Create documents from within a process

We previously learned how to create a document model, use skeletons and further improve them with placeholders and optionally with placeholder mappings using expressions.
Our next step now takes us back to the process model where we want to automatically create such a document as part of the process.
Go to the Tutorial App and open the Travel Request Management Process

Drag and drop a new Create document service task onto the sequence flow right after the last Review Travel Bookings user task and name it Create Travel Doc or similar.
In the property Document model choose our newly created Travel Request Document model and use root.travelDoc` as the `Document id variable name property.
Optionally, you can specify the name for the document using the Document name variable, so set it to #{root.name}.docx.

Your process should now look like this:

document model 11
Tip
Using a variable name for the document id will give us the possibility to later access this specific document using exactly this variable.

You can now save the process model, redeploy the app and start a new travel request or start the travel management process within an existing case again.
At the end of the process, our document will be created out of the template with the data from the case and will be stored within the case.

4.9.6. Send documents as an attachment of an email

As the final step, we want to send a document of our case as an attachment of an email.
Go back to our Travel Request Management Process and make some space before the Send decline notification email task.
Before we can use the document as the attachment in that email, we first have to create it using the very same task we added as our last step. We could add a new Create document task again or we can copy / paste our existing one, so we don’t have to enter all properties again.
You can do so by selecting the existing Create Travel Doc task, use the Copy button or hit Ctrl-C to copy it and then the Paste button or Ctrl-V to paste it. Drag this copied task and drop it onto the sequence flow just before the Send decline notification email task.

Your process should now look like this:

document model 12

Now select the Send decline notification email task and click the Attachment ids property and enter #{root.travelDoc} in the property dialog.

Save the process, redeploy the app and start the process again or create a new travel request and make sure to use the approval and decline it in order to send the email with the document created as the attachment.

4.10. Create convenience queries

Queries are work items too in its technical representation with the aim of saving and optionally sharing a query like a filter returning a list of work items, if executed. If you’re familiar with the search in edoras one, then you’re almost there as a query is nothing else than a saved search request which can be executed at any time later.

And that’s exactly on how to actually create a new query; simply type some search terms in the search box, hit Enter and then use the Create Query button in order to save it.

First of all you can use the search kind of like a Google-search by entering terms to search for. But you can also use structured search terms like type:CAS or for:me which would search for cases only assigned to the current user.

4.10.1. Create query to return all open travel request cases

If we want to get a list of all open travel request cases, this would be the way to create such a query item:

  • click the search box and enter type:CAS, if you hit Enter you should now see a list of cases

  • now add state:open as a second term, if you hit Enter you should now only see open cases

  • add model:'Travel Request' as a next term, if you hit Enter you should now only see open travel request cases

You probably noticed that some of the terms are being removed from the search box and represented using the predefined, simple search selection, like the state term, the type term or even the sort term.

If you have the result in the way you want it, click the Create Query button and add a name for it, then click Create and if you go back to the user dashboard, you will see the new query item in the list of My Queries.
Optionally you can share a query item with others using the Share action. This way, other will have the opportunity to see and use that query too in their dashboards.

4.10.2. Execute and change an existing query item

Once you created the query item, it’s simple to use it by just selecting it in the user dashboard. It will then be executed and returning a list of work items according the query terms.
If you select the Work Form view, you can change the name and query term of the item later and save it the same way as you would with a case or task item or you can use the Share action to add some groups you want the query item to share with.

4.10.3. Exporting the data returned by a query item

Once you execute a query item, you can export its results and data by clicking the Export Query Data button. It will then create an Excel file with all data from the returned work items in it.
Unfortunately, you can’t configure the data to be exported yet, yo the Excel might become quite big. In a future release, you will be able to configure the variables and information you want to be exported as part of the query item as well.

4.11. Create your own dashboard

In this chapter of the tutorial we are going to create our own dashboard and we will learn how we can set it in our user profile to be used as the default.
There is currently no prominent way to create dashboard items like having a dashboard model item or the like yet, so we will use quite a commonly used workaround to achieve the same goal.

4.11.1. Using a case model to represent our own dashboard

As a possibility, we are going to model and design a case acting as our dashboard. Because we can design a working form for a case, we can design it in a way to act as our dashboard and once we create an instance of such a case, we simply have to store its URL as the default one in our user profile and we’re done.

4.11.2. Designing the form for our dashboard

This is exactly the same as we did many times before now in this tutorial, so go to the Tutorial App and hit C or click the Create button and select Create Form Model to create our dashboard view. ` Name the form Dashboard View as it is more a view rather than a form then click Create and Design to directly create the form model and open it in edoras vis to design it.

Drag and drop an Output textarea to the top of the form where we can add some information about the dashboard then double-click it and add something like this to it:

dashboard 01

We now want to add some lists to the dashboard, so drag and drop a new List widget to the form and remove the label by removing the predefined text of it. Lists are showing work item based on a query term. Still remember the query items from our last chapter? A list widget is based on exactly the same query term and using the same search functionality as a query item or the global search action in edoras one.
Now select the newly added List widget and set the Title property to Open travel requests.
With the Format property we can specify how the label of the items in the list will be rendered. By default, it will use just the name of the work item. We would like to include the original requestor of the travel request as well so lets set the Format property to {{item.name}} (Requestor: {{item.owner.name}}) in order to include the name of the requestor after the name of the travel request case.
Set the Query property to the following search term: type:CAS state:open model:'Travel Request' sort:creation:desc
If we want to limit the height of the list widget, we can set the property Maximum number of rows to 10 for instance.
Your form should now look like this:

dashboard 02

Furthermore, we would like to add two more lists, one of it only showing the Manage Travel Request user tasks and on the other one showing all open tasks, regardless the type of case or app.
First the management tasks; so drag and drop one more List widget to the right hand side of the existing list and configure the following properties:

  • set the Label property to an empty string in order to hide it, we will use the Title property instead

  • set the Query property to type:TSK state:open for:me name:'Manage Travel Request' sort:creation:asc

  • set the Title property to My open management tasks

  • set the Format property to {{item.name}} ({{item.root.name}}) so we include the name of the travel request case as well after the task name

Add a third List widget to the very right of the same row we already added the two other lists and drop it there. If we want the three list widgets to be equally spaced, use the handle on the right or left hand side of the widget and drag it to the left or right until it spawns as many cells as you want (four in our case to have three equally spaced lists).

Set the properties of the third list widget to the following values:

  • set the Label property to an empty string in order to hide it, we will use the Title property instead

  • set the Query property to type:TSK state:open for:me sort:creation:asc

  • set the Title property to All my open tasks

  • set the Format property to {{item.name}} ({{item.root.name}}) so we include the name of the travel request case as well after the task name

Maybe we want to add an instant-filter field to the first list, so we can filter the list to find a particular travel request. Drag and drop a new Text widget between the Output textarea and the List widget on the left and drag its right hand side handle to the left in order to let it spawn the same number of columns as the list widget and name it Search. Set its value binding to {{caseSearch}}.
Now we can use that variable within the Query property of our first List widget in order to dynamically use the entered search text in our filtered case list.

Select the first List widget (containing the open travel request cases) and add {{caseSearch}} at the the end of Query, so it now is configured to type:CAS state:open model:'Travel Request' sort:creation:asc {{caseSearch}}.

The dashboard view should now look like this:

dashboard 03

Now lets save the form and return back to our Tutorial App.

4.11.3. Creating the case model for the dashboard

As we already talked about, we are going to model a case to represent our dashboard. Hit C or click the Create button and select Create Case Model then use Travel Management Dashboard as the name and choose the Dashboard View as the Init Form and Work Form property.
Remove all selections from the Allowed Actions section, just let the Create Variables action in there, as we don’t want to work with that special case, but only display it.
If you want to share the dashboard with others, you can select the appropriate groups within the Candidate Groups property.

Now click the Create button and redeploy the Tutorial App, then go to the user dashboard.

4.11.4. Creating an instance of our dashboard

Before we can set the dashboard as our default one, we need to create a case instance based on that case model, so hit C or click the Create button within the user dashboard and select our Travel Management Dashboard template and click Create.

If you want to avoid a case with an empty name, use the Edit view (only available as an admin or modeler) and set the Name field to something like Travel Management Dashboard and click the Save button.

Your dashboard should now look similar to this one:

dashboard 04
Hiding the Save and Cancel button in the dashboard

Because technically speaking our dashboard is a simple case, the Save and the Cancel button are still showed on our dashboard view, but we don’t need them. So there is a trick to hide those buttons by adding a hidden widget to the Dashboard View form which will cause the rendering engine to hide the buttons.

Go to the Dashboard View form and add a Checkbox widget at the end of the form.
Label it (Hide Buttons Widget)' to indicate what it is used for.
Set the `Value
to {{_hideSavePane}}.
Set the Default value to true (enabled).
Set Style class to hidden. Make sure the Visible attribute is enabled (true).

redeploy the Tutorial App and migrate the current dashboard case.

Your dashboard should now look similar to this one:

dashboard 05

4.11.5. Adding count widgets to the dashboard view

Maybe we have a lot of open tasks or cases and we can’t see all of them in the list widget but would like to see how many there are. There are special Count widgets allowing us to display a link having a count of work items based on a query in it.
Go back to the Dashboard View form designer and add a new Search count link widget just below each of our list widgets and make their spanning width equally to the list widget they will belong to.
Remove the label as we only want to display a link text including the count of the work items in the list.

For the first count widget, use the following properties:

  • set the Link text property to $count open travel request [case|cases] found

  • set the Query property to type:CAS state:open model:'Travel Request' (for the count query we don’t need the sort term obviously and we always want to show all open travel request cases, so we remove the caseSearch term)

  • set the Target property to _self in order to create the navigation link in the same window

For the second count widget, use the following properties:

  • set the Link text property to $count open travel management [task|tasks] found, assigned to me

  • set the Query property to type:TSK state:open for:me name:'Manage Travel Request' (for the count query we don’t need the sort term obviously)

  • set the Target property to _self in order to create the navigation link in the same window

For the third count widget, use the following properties:

  • set the Link text property to $count open [task|tasks] found assigned to me

  • set the Query property to type:TSK state:open for:me (for the count query we don’t need the sort term obviously)

  • set the Target property to _self in order to create the navigation link in the same window

Tip
The $count term is a special term which will be bound to the count result being returned by executing the count query.
Within the [x|y] term, the x represents the label being used, if the count is exactly 1 and y will be used if the count is bigger than 1.

Your dashboard view should now look similar to this one:

dashboard 06

Go to the Tutorial App, redeploy it, go to the Travel Management Dashboard case and click the Migrate action in order to update our dashboard case to the newest version.

Your dashboard view now should look like this:

dashboard 07

4.11.6. Adding create buttons to the dashboard view

As a next step, we would like to add a new Create button to the dashboard view in order to create a new Travel Request without using the general Create button.
So go back to the Dashboard View and add a new Create button below the first count widget and resize it do only spawn four columns.
Use Create new travel request as the Button text property, choose Case as the Work item type property and select Travel Request as the Work item model property.
This will create a button navigating directly to the init form for a new travel request (the same URL actually as if you would click the create button and then select the travel request template).

Your dashboard view now should look like this:

dashboard 08

Go to the Tutorial App, redeploy it, go to the Travel Management Dashboard case and click the Migrate action in order to update our dashboard case to the newest version.

Your dashboard view now should look like this:

dashboard 09

If you click the Create new travel request you will be forwarded directly to the init form in order to create a new Travel Request.

4.11.7. Passing local context variables to the init form with a create button

Lets say we want to add some information we already have on the current form to the init form being invoked by clicking a create button. We can do so by using context parameters.

To visually separate the create button from the list widgets, lets add a horizontal line between them by dragging and dropping a new Horizontal line between the count widgets and the create button.
Then move the create button to the same four columns as the second list widget so we can add a new text field to the first four columns.
Now drag and drop a new Text widget and drop it to the left hand side of our create button, name it Request name and bind it to {{travelRequestName}}. Optionally you can specify a description by setting the Description property to Set the new travel request name used for creating a new travel request.

Now select the create button and click the Context variables property and enter the following in the property dialog:

dashboard 10

We use the value from our newly added text field ({{travelRequestName}}) and pass it along to the init form as the name property so our name field in the init form will be pre-populated with that value.

Your view should now look like this:

dashboard 11

Go to the Tutorial App, redeploy it, go to the Travel Management Dashboard case and click the Migrate action in order to update our dashboard case to the newest version.
If you now enter some text in the request name field and click the create button, that text is passed along and directly added to the init form.

If you want you can add even more fields and functionality to your dashboard view.

4.11.8. Setting our dashboard as the default one

We now created our dashboard case, now we would like to use it as the default one for the user dashboard.
First, make sure our own dashboard is currently shown and then copy the URL-part starting with the CAS part (something like CAS/GEAR-xxxx/browse).
Then we can set it as the default dashboard by going to our user profile (select the User profile menu action in the top right menu bar) and pasting that copied URL to the Home URL field and then saving the user profile.

dashboard 12

To make the changes active, you most likely have to reload the current view and then you should find yourself directly on your own, custom dashboard!
You can always navigate to that by clicking the logo in the top right menu bar. If you want to go back to the default user dashboard, you can do so by selecting it from the dashboard menu or with the Home icon in the menu bar.

5. edoras one Administration Guide

5.1. Administration Overview

The admin dashboard can be accessed by authorized users through the dashboard menu and is used to manage users and work item access permissions within edoras one.

5.1.1. Tenants

An edoras one installation may provide one or more tenants. A tenant is an isolated environment for a specific group of users. No data can be shared between tenants and users are always logged into a specific tenant. The edoras one tenant support allows multiple edoras one environments to be provided by a single server instance.

Tenants are not directly visible in the administration dashboard and cannot be administered there, as the tenant selection has already been made when the user logged in, but it is perhaps useful to know that the administration operations described here take place within the context of a particular tenant.

5.1.2. Users

User entries define the people who are allowed to log into the system and their settings.

The following attributes may be set for a user:

Table 66. User attributes
Name Required Description

Display name

Yes

the name that will be shown in the edoras one application

Login name

Yes

the name used to login to edoras one

Email address

Yes

the user’s email address

Language

No

the user’s language

Password

Yes

the user’s password

Last name

No

the user’s last name

First name

No

the user’s first name

Address

No

the user’s address

Phone

No

the user’s phone number

Mobile

No

the user’s mobile phone number

Notes

No

additional notes

A user can also enable automatic email notification of specific events within the system:

  • when a task is assigned to the user

  • when a task assigned to the user is edited

  • when a task owned by the user is edited

The password may be changed at any time by entering a new password in the two password fields. The password will be changed only when the two fields contain the same value.

The non-required fields are provided for information purposes only and are not used by edoras one itself.

A user belongs to an account and can be moved between accounts using the move action.

User entries can be activated and deactivated. Deactivated users will not be able to log into the system and will not be shown in the application, for example when reassigning a work object.

Predefined users

Each tenant in edoras one has an administration user. This user has full access to all work items in the system and certain special access permissions for system administration tasks.

5.1.3. Accounts

Accounts are used to collect users and groups that are related to each other, making it easier to organize and locate particular entries.

The following attributes may be set for an account:

Table 67. Account attributes
Name Required Description

Name

Yes

the account name

Description

No

the account description

Main color

No

the main color

Bright Bg color

No

the first background color

Dark Bg color

No

the second background color

Logo URL

No

the URL for the application logo

The color and logo settings will be used when any user that belongs to the account is logged in.

If an account is activated or deactivated then all users and groups in that account will also be changed to the new state.

Predefined accounts

The following accounts are defined by default in edoras one:

admin

contains the standard users and groups provided by edoras one and needed for correct operation of the system

<tenant name>

a tenant-specific account which contains any additional tenant users and groups

5.1.4. Groups

Groups are used to control the accessibility of objects within edoras one.

The following attributes may be set for a group:

Table 68. Group attributes
Name Required Description

Name

Yes

the group name

Description

No

the group description

As with a user, the group belongs to an account and can be moved between accounts using the move action.

A group can also be activated and deactivated. Note that deactivating a group simply means that objects cannot be shared with that group in the future. Existing objects that are shared with a deactivated group will be unaffected.

Predefined groups
All users

A group for all users in the tenant.

edoras one Admin

The administration group with access to the administration dashboard.

edoras one Manager

The manager group with access to the management dashboard.

edoras one Modeler

The modeler group with access to the modeler dashboard. This is also the default group for sharing models.

edoras one User

The user group with access to the user dashboard.

edoras one Supervisor

Users that belong to the supervisor group can view all work items in the system, regardless of who they are assigned to or shared with.

5.1.5. Apps

It is possible to manage the properties from the app models with the apps. By clicking on one app, the same property view is showed as with the deploy action view, however, the static properties are read-only. Only dynamic properties might be changed.

5.1.6. Access permissions

Access permissions in edoras one are controlled by group membership. To change a user’s group membership settings, select the user to be updated and then use the Group membership action to add or remove groups as required.

Work item visibility

A work item in edoras one is visible to a particular user if one of the following conditions is satisfied:

  • the work item is owned by the user

  • the work item is assigned to the user

  • the user is a member of a group that appears in the work item’s sharing group list

If a work item is visible by a particular user then it can also be edited. No fine-grained access control mechanism is provided, for example to allow read-only access.

Dashboard visibility

A particular dashboard is visible to a given user if the user is a member of the relevant group. For a list of the predefined dashboard groups please refer to the section Predefined groups.

5.2. Glossary

tenant

a tenant is an isolated environment in edoras one where users can work and processes can be executed without risk of affecting the contents of other tenants in the same server.

user

a user work item gives access to a particular tenant in edoras one, with associated attributes and permissions.

group

groups are used to control access to work items (and dashboards, in the case of the predefined groups).

accounts

accounts are used as a container for related users and groups. They can also be used to control the appearance of edoras one.

apps: a "singleton" work item from the app model. The app model properties are accessible from the app.

6. edoras one Operator Guide

6.1. Overview

This document describes the operational aspects of edoras one and its integration with other systems.

It is intended for system administrators responsible for configuring an edoras one installation. :imagesdir: operatorGuide/

6.2. Server Requirements

6.2.1. Fonts

The edoras one server requires a number of Microsoft TrueType fonts to be available to run correctly. On many systems the required fonts will already be installed by default, but in some cases this may not be the case. If the fonts are not available, the result may be a long wait while the server attempts to find the fonts somewhere in the filesystem and it may not be possible to start the server at all.

On Linux systems (e.g. CentOS) the required fonts can be obtained by installing the msttcorefonts package. :imagesdir: operatorGuide/

6.3. Tenant configuration

A new tenant in edoras one is created by adding a tenant definition file to the tenant data folder and restarting the server. The tenant data may either be built into the edoras one WAR file (see the :developer-guide: for more information) or placed in an external folder. The tenant data location can be configured using the tenant.data.location property. To use an external folder, the value of this property should have a file: prefix, for example:

file:${edoras-one.home}/tenants

6.3.1. Overall structure

The tenant definition is a JSON object definition, something like the following:

{
    "id": "tenantId",
    "name": "acme",
    "adminUserLogin": "testAdmin1",
    "adminUserEmail": "testAdmin@test.com",
    "accounts": [
        {
            "name": "account",
            "domain": "test.com",
            "mainColor": "#000011",
            "backgroundColor": "#000022",
            "highlightColor": "#000033",
            "logoUrl": "https://www.google.ch/images/srpr/logo11w.png",
            "groups": [ "group" ],
            "users": [
                {
                    "displayName": "John Smith",
                    "firstName": "John",
                    "lastName": "Smith",
                    "login": "john.smith",
                    "email": "john.smith@email.es",
                    "language": "en",
                    "memberGroups": ["group"]
                }
            ]
        }
    ]
}

The following sections describe the available attributes for each part of the definition.

6.3.2. Tenant information

The top level of the JSON contains information about the tenant. The following attributes are supported:

Table 69. Tenant attributes
Attribute name Description

accounts

a list of the initial accounts

adminUserLogin

admin user’s login

adminUserEmail

admin user’s email address

name

tenant name

6.3.3. Account information

An account entry defines an initial account within the tenant. The following attributes are supported:

Table 70. Account attributes
Attribute name Description

domain

domain name (used to create user email addresses automatically)

mainColor

main color property (optional)

highlightColor

highlight color property (optional)

backgroundColor

background color property (optional)

logoUrl

logo url used in page top left corner (optional)

groups

a list of group names

name

account name

users

a list of the initial account users

For each account entry, an account will be created with the given name, groups and users.

6.3.4. User information

A user entry defines an initial user within the account. The following attributes are supported:

Table 71. User attributes
Attribute name Description

displayName

user’s display name

firstName

user’s first name

lastName

user’s last name

login

user’s login

email

user’s email address

language

user’s language

memberGroups

groups that the user belongs to

For each user entry, a user will be created with the given information.

If no email address is provided and the account domain is set, then an email address will be created from the domain and user’s first and last names.

When defining a user’s group membership, both the default edoras one group names (edoras one Modeler etc.) and the group names explicitly defined in the account may be used.

6.4. Integrating edoras one with other systems

6.4.1. Mail integration

Outgoing mail

To send outgoing mails, edoras one uses a org.springframework.mail.javamail.JavaMailSender instance.

A suitable bean definition is therefore required in the installation-specific Spring configuration. For debugging purposes in a local development environment, a simple logging implementation is provided that simply logs all outgoing mails to the server log (no mail is actually sent):

    <!-- during development, just log outgoing emails -->
    <bean id="mailSender" class="com.edorasware.cloud.core.mail.LoggingMailSender"/>

For real servers, a full bean configuration will be required:

    <bean name="mailSender" class="org.springframework.mail.javamail.JavaMailSenderImpl">
        <property name="defaultEncoding" value="UTF-8"/>
        <property name="host" value="smtp.gmail.com"/>
        <property name="port" value="465"/>
        <property name="username" value="${mail.smtp.username}"/>
        <property name="password" value="${mail.smtp.password}"/>
        <property name="javaMailProperties">
            <props>
                <prop key="mail.debug">${mail.debug}</prop>
                <prop key="mail.transport.protocol">smtp</prop>
                <prop key="mail.smtp.auth">true</prop>
                <prop key="mail.smtp.socketFactory.port">465</prop>
                <prop key="mail.smtp.socketFactory.class">javax.net.ssl.SSLSocketFactory</prop>
                <prop key="mail.smtp.socketFactory.fallback">false</prop>
                <prop key="mail.smtp.quitwait">false</prop>
            </props>
        </property>
    </bean>

In a typical installation where the bean configuration is stored on the server where it will be used, the property placeholders shown in this example can be directly replaced with the appropriate values.

For details about mail sender configuration please refer to the Spring documentation. :imagesdir: operatorGuide/

6.5. Standard properties

edoras one provides a number of configuration properties that can be used to control the behaviour of the application. These properties can be set in a number of different ways:

  • in a property file built in to the on-premise application (this can only be changed by a developer)

  • in an external property file: one.properties

  • as a system property, for example on the application command line

These are listed in order of increasing precedence, so a property value defined by the application can be overwritten by an external property file, for example.

Note

Properties are used to configure individual settings within edoras one’s Spring configuration. As an on-premise project may replace some or all of this Spring configuration, the properties that may be used in any given installation may also vary. The remainder of this section assumes that the installation uses the standard edoras one Spring configuration files.

6.5.1. Locating the external property file

edoras one looks for external property file in the edoras one configuration folder. By default this is the folder .edoras-one in the user’s home directory. An alternative configuration folder can be selected by setting the system property edoras-one.home.

If no external property file can be found then a message to that effect will be produced by edoras one during the application startup.

6.5.2. Basic configuration properties

Table 72. Basic configuration properties
Property Default value Description

edoras-one.home

${user.home}/.edoras-one

the location of the edoras one configuration folder. This can only be set as a system property.

application.endpoint

http://localhost:8080

the base URL where the application is running

6.6. Tomcat configuration

6.6.1. Connector configuration

To allow to send UTF-8 characters in URIs (e.g. the search requests) we need to allow UTF-8 URI encoding in tomcat (server.xml).

    <!--
        Define a non-SSL HTTP/1.1 Connector on port 8080
        URIEncoding is set to UTF-8
    -->
    <Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443"
               URIEncoding="UTF-8"
               />
    
    <!--
        Define an AJP 1.3 Connector on port 8009
        URIEncoding is set to UTF-8
    -->
    <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" URIEncoding="UTF-8"/>

6.7. Clustering

To deploy edoras one on two or more nodes you will need the following prerequisites:

  • Apache HTTPD Server with mod_jk/mod_proxy or any other load balancing component ({apache-httpd-page}, {apache-mod-jk-page}, {apache-mod-proxy-page})

  • Two or more Apache Tomcat instances (or any other application server)

  • File content synchronization (like {gluster-page})

  • Redis 2.8 and greater ({redis-page})

    NOTE

    mod_jk is only needed if you use an Apache Tomcat application server. If you use another application server, you will need mod_proxy.

As a load balancer we use the Apache HTTPD Server which sends the request to the Apache Tomcat nodes as configured in the mod_jk configuration. The file content (like documents in edoras one) are stored on the file system which is synchronized beteween the nodes with the help of Gluster. The last part is the Redis server which acts as central point for the distributed caches and the user sessions.

The following diagram shows this setup graphically:

cluster configuration

This guide will just show you how to configure edoras one to use Redis as distributed cache and as a distributed session store. Please refer to the following guides on how to install, configure these tools:

  • {using-apache-with-mod-proxy}

  • {using-apache-with-mod-jk}

  • {redis-page}

  • {gluster-page}

6.7.1. Configuration of edoras one

The only things we need to configure is to use Redis as distributed cache and as a distributed session store. To do this you need to enable the cache-redis and session-redis Spring profiles. You can either add these profiles to the spring.profiles.active system property, or add it in the web.xml as spring.profiles.default context parameter.

Next you need to configure the Redis connection. To do this add the following system properties with the appropriate values.

Table 73. Redis connection properties
Property Description

redis.hostname

the hostname where the Redis server is running (defaults to: localhost)

redis.port

the port where the Redis server is listening to incoming connections (defaults to: 6379)

redis.password

the password of the Redis server (default is empty)

After that edoras one is configured to run in a clustered environment.

7. edoras one Developer Guide

7.1. Developing on-premise applications

This document describes best practices for developing a customized on-premise application based on edoras one. It is mainly intended for software developers, although some topics address system installation and configuration and may therefore also be of interest to system administrators.

Various topics are presented, grouped by general theme, with each topic describing a specific development task. The topics are generally independent of each other and can be read on their own without having to read the entire document. A working knowledge of the concepts underlying edoras one is required, and some topics will also require knowledge of edoras gear or some additional external technologies.

7.2. Obtaining the example project

Many of the examples described in this document are demonstrated in an example on-premise project. To obtain a copy of this project please contact {edw-support}.

7.3. Overview of the example project

The example project demonstrates the following aspects of developing an on-premise application with edoras one:

  • the basic configuration needed to start edoras one based on the core dependencies. The files involved here are generally identified with bootstrap, either in the file path or filename.

  • additional customizations for a specific project (in our examples we use the fictitious domain com.acme). The files involved here are generally identified with acme, either in the file path or filename.

7.4. Creating a clean skeleton project

The example project is pre-packaged with example services, listeners etc. already in the project. If you need a bare project for your own development you can strip out all of this additional example code:

  • obtain the example project

  • go to the project root folder

  • execute the following Gradle command on the command line:

Create the skeleton project on Mac/Linux
  ./gradlew createSkeletonProject
Create the skeleton project on Windows
  gradlew.bat createSkeletonProject

The project will then be a clean skeleton project.

7.5. Setting up a development environment

Developing on-premise applications with edoras one is fairly straightforward if you are familiar with standard Java development workflows:

  • create a new project using a build tool (e.g. Maven or Gradle)

  • include the edoras one artifacts in your project as normal dependencies

  • provide the basic configuration needed to start edoras one

  • extend edoras one by adding your own configurations and / or classes as required

This section describes setting up your development environment and including edoras one into your project. The following sections describe the edoras one extension points and how they can be used to customize edoras one and integrate it into your IT infrastructure.

7.5.1. Configuring the build system

Basic project build information

The example project provides build configurations for both the Maven and Gradle build systems. You can use whichever build system you prefer. For the Maven build Maven 3.0.0 or greater is required. The Gradle build is based on the Gradle wrapper and downloads the appropriate version automatically.

You can download the build tools here:

  • {mvn-page}

  • {gradle-page}

Basic Maven configuration

The project information and compiler version (Java 7 is required) should be set in the Maven pom.xml:

Project information in pom.xml
  <project xmlns="http://maven.apache.org/POM/4.0.0"
                   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                   xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
                               http://maven.apache.org/xsd/maven-4.0.0.xsd">
          <modelVersion>4.0.0</modelVersion>
  
          <groupId>com.edorasware.one</groupId>
          <artifactId>edoras-one-bootstrap</artifactId>
          <version>1.0.0</version>
          <packaging>war</packaging>
Compiler configuration in pom.xml
  <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-compiler-plugin</artifactId>
      <version>3.1</version>
      <configuration>
          <source>1.7</source>
          <target>1.7</target>
      </configuration>
  </plugin>

You should also configure the resource encoding to avoid warnings during the build:

Resource configuration in pom.xml
  <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-resources-plugin</artifactId>
      <version>2.6</version>
      <configuration>
          <encoding>UTF-8</encoding>
      </configuration>
  </plugin>
Basic Gradle configuration

First you need to apply the war plugin and define the project information:

Project information in build.gradle
  apply plugin: 'war'
  
  allprojects {
    group = 'com.edorasware.one'
    version = '1.0.0'
  }
Artifact repository configuration

To build the project you will need access to a Maven repository containing the edoras one artifacts so that the edoras one dependencies can be resolved. The artifacts can either be uploaded to a repository manually, or if you have access to the edorasware public repository then the connection details may be configured into your local repository and artifacts will be downloaded from edorasware automatically when they are needed.

After adding the edoras one artifacts to your repository you will also need to provide a suitable repository configuration and access credentials.

Maven repository configuration

The Maven repository configuration will typically be located in the pom.xml file. The following snippet shows the configuration for the edoras repository. The configuration for your local repository will look similar.

Repository configuration in pom.xml for Maven builds
  <repositories>
      <repository>
          <id>repo.edorasware.com</id>
          <url>https://repo.edorasware.com/edoras-repo</url>
      </repository>
  </repositories>
  
  <pluginRepositories>
      <pluginRepository>
          <id>repo.edorasware.com</id>
          <url>https://repo.edorasware.com/edoras-repo</url>
      </pluginRepository>
  </pluginRepositories>

Add the repository credentials to the servers section in your local Maven configuration file. By default you can find the Maven configuration file at <USER_HOME>/.m2/settings.xml.

Add this block to the settings file
  <servers>
      <server>
          <id>repo.edorasware.com</id>
          <username>customer-username</username>
          <password>customer-password</password>
      </server>
  </servers>

Additional details of how to configure a Maven repository can be found on the {mvn-page}.

Gradle repository configuration

The Gradle repository configuration will typically be located in the build.gradle file. The following snippet shows the configuration for the edoras repository. The configuration for your local repository will look similar.

Repository configuration in build.gradle for Gradle builds
  repositories {
    maven {
      credentials {
        username DOWNLOAD_REPO_USERNAME
        password DOWNLOAD_REPO_PASSWORD
      }
      url "https://repo.edorasware.com/edoras-repo"
    }
    mavenCentral()
  }

Add the credentials that you received from edorasware to your local gradle.properties file. By default you can find the Gradle configuration file at <USER_HOME>/.gradle/gradle.properties.

Add this block to the gradle.properties file
  DOWNLOAD_REPO_USERNAME=customer-username
  DOWNLOAD_REPO_PASSWORD=customer-password
edoras one dependency configuration

The edoras one artifacts can now be added to the project as a dependency.

Maven dependency configuration

By adding a property to the <properties> tag of the Maven pom.xml file the edoras one version can be set in a single place:

edoras one version property in the Maven pom.xml
  <properties>
      <com.edorasware.one.version>@projectVersion@</com.edorasware.one.version>
  </properties>

This property can then be used in the dependency configuration to import the required dependencies:

edoras one dependencies in the Maven pom.xml
  <!-- Compile dependencies -->
  <dependency>
      <groupId>com.edorasware.one</groupId>
      <artifactId>edoras-one-client</artifactId>
      <version>${com.edorasware.one.version}</version>
  </dependency>
  
  <dependency>
      <groupId>com.edorasware.one</groupId>
      <artifactId>edoras-one-server-rest</artifactId>
      <version>${com.edorasware.one.version}</version>
      <exclusions>
          <!-- TODO fix for https://issues.apache.org/jira/browse/BATIK-1038 -->
          <exclusion>
              <groupId>org.apache.xmlgraphics</groupId>
              <artifactId>batik-extensions</artifactId>
          </exclusion>
      </exclusions>
  </dependency>
  
  <dependency>
      <groupId>com.edorasware.one</groupId>
      <artifactId>edoras-one-server-test</artifactId>
      <version>${com.edorasware.one.version}</version>
      <scope>test</scope>
  </dependency>
  
  <dependency>
      <groupId>com.edorasware.one</groupId>
      <artifactId>edoras-one-server-core</artifactId>
      <classifier>test</classifier>
      <version>${com.edorasware.one.version}</version>
      <scope>test</scope>
  </dependency>
  
  <dependency>
      <groupId>com.edorasware.license</groupId>
      <artifactId>edoras-license-one</artifactId>
      <classifier>development</classifier>
      <version>1.0.4</version>
  </dependency>
Gradle dependency configuration

By adding a property to the <properties> tag of the Gradle build.gradle file the edoras one version can be set in a single place:

edoras one version property in the Gradle build.gradle
  def edorasOneVersion = '@projectVersion@'

This property can then be used in the dependency configuration to import the required dependencies:

edoras one dependencies in the Gradle build.gradle
  compile "com.edorasware.one:edoras-one-client:$edorasOneVersion"
  compile("com.edorasware.one:edoras-one-server-rest:$edorasOneVersion") {
    //TODO fix for https://issues.apache.org/jira/browse/BATIK-1038
    exclude group: 'org.apache.xmlgraphics', module: 'batik-extensions'
  }
  
  testCompile "com.edorasware.one:edoras-one-server-test:$edorasOneVersion"
  testCompile "com.edorasware.one:edoras-one-server-core:$edorasOneVersion:test"
  testRuntime "com.edorasware.license:edoras-license-one:1.0.4:development"
Database dependency configuration

In addition to the edoras one dependencies, at least one database dependency is also required. For convenience the build configurations from the example project already contain dependencies for some commonly-used databases:

Database dependencies in pom.xml for Maven builds
  <dependency>
      <groupId>com.h2database</groupId>
      <artifactId>h2</artifactId>
      <version>1.3.168</version>
  </dependency>
Database dependencies in build.gradle for Gradle builds
  compile "com.h2database:h2:1.3.168"
Logging dependency configuration

edoras one uses the Simple Logging Facade for Java (SLF4J) for logging. SLF4J is a framework that allows the end user to plug in the desired logging framework at deployment time. You can learn more about slf4j on the {slf4j-page}.

Maven dependency configuration

By adding a property to the <properties> tag of the Maven pom.xml file the slf4j version can be set in a single place:

Logging version property in the Maven pom.xml
  <org.slf4j.version>1.7.7</org.slf4j.version>

This property can then be used in the dependency configuration to import the required dependencies. The following dependencies instruct SLF4J to send all logging output from components used in edoras one to slf4j:

Basic logging dependencies in pom.xml for Maven builds
  <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>jul-to-slf4j</artifactId>
      <version>${org.slf4j.version}</version>
  </dependency>
  
  <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>jcl-over-slf4j</artifactId>
      <version>${org.slf4j.version}</version>
  </dependency>
  
  <dependency>
      <artifactId>commons-logging</artifactId>
      <groupId>commons-logging</groupId>
      <version>1.1.3</version>
      <scope>provided</scope>  <!-- globally replace commons-logging with jcl-over-slf4j -->
  </dependency>

A suitable adapter can then be configured to send the slf4j output to a particular logging system. In the bootstrap project we use log4j, so we include the slf4j-to-log4j adapter:

Logging adapter in pom.xml for Maven builds
  <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-log4j12</artifactId>
      <version>${org.slf4j.version}</version>
  </dependency>
Gradle dependency configuration

By adding a property to the <properties> tag of the Gradle build.gradle file the slf4j version can be set in a single place:

Logging version property in the Gradle build.gradle
  def slf4jVersion = '1.7.12'

This property can then be used in the dependency configuration to import the required dependencies. The following dependencies instruct SLF4J to send all logging output from components used in edoras one to slf4j:

Basic logging dependencies in the Gradle build.gradle
  runtime "org.slf4j:jul-to-slf4j:$slf4jVersion"
  runtime "org.slf4j:jcl-over-slf4j:$slf4jVersion"
  
  // globally replace commons-logging with jcl-over-slf4j
  providedRuntime "commons-logging:commons-logging:1.1.3"

A suitable adapter can then be configured to send the slf4j output to a particular logging system. In the bootstrap project we use log4j, so we include the slf4j-to-log4j adapter:

Logging adapter in the Gradle build.gradle
  runtime "org.slf4j:slf4j-log4j12:$slf4jVersion"

7.5.2. Configuring edoras one

edoras one is configured using Spring. For details of the Spring project, please refer to the {spring-page}. In the default configuration, environment-specific components are selected using Spring profiles, and the component settings are generally either fixed in the configuration or made configurable using properties.

The starting point for the Spring configuration is defined in the web.xml file that is deployed with the application. This will typically contain the location of the root Spring configuration file and the default profiles that will be used:

webapp/WEB-INF/web.xml
  <!-- Spring context configuration -->
  <context-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>classpath*:/com/edorasware/acme/config/acme-context.xml</param-value>
  </context-param>
  
  <context-param>
      <param-name>spring.profiles.default</param-name>
      <param-value>database-h2,integration-development,security-basic</param-value>
  </context-param>

To override the default Spring profiles, the active profiles may be set from the command line when the application server is started: -Dspring.profiles.active="…​".

The majority of the necessary Spring configuration is provided by the edoras one artifacts and can be reused by an on-premise project, but a small amount of configuration is required for each specific installation.

To configure the on-premise project you will need to provide a Spring application context configuration, for example:

com/edorasware/bootstrap/config/one-application-context.xml
  <?xml version="1.0" encoding="UTF-8"?>
  <beans xmlns="http://www.springframework.org/schema/beans"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns:context="http://www.springframework.org/schema/context"
         xsi:schemaLocation="http://www.springframework.org/schema/beans
                             http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
                             http://www.springframework.org/schema/context
                             http://www.springframework.org/schema/context/spring-context.xsd">
  
      <context:property-placeholder
              location="classpath:/com/edorasware/bootstrap/config/one.properties,file:/${edoras-one.home}/one.properties"
              system-properties-mode="OVERRIDE"
              ignore-resource-not-found="true"/>
  
      <!-- configure JUL-SLF4J rerouting as Activiti logs to Java Util Logging API -->
      <bean id="julReroute"
            class="com.edorasware.commons.core.util.logging.JulToSlf4jBridgeHandlerInstaller"
            init-method="init"/>
  
      <!-- import the edoras one, edoras vis and edoras cmmn context -->
      <import resource="classpath*:/config/one-core-application-context.xml"/>
      <import resource="classpath:/com/edorasware/vis/config/vis-application-context.xml"/>
  
      <!-- import the database configuration -->
      <import resource="classpath:/com/edorasware/bootstrap/config/database-config.xml"/>
  
      <!-- import the security configuration -->
      <import resource="classpath:/com/edorasware/bootstrap/config/security/*-config.xml"/>
  
      <!-- import the integration configuration -->
      <import resource="classpath:/com/edorasware/bootstrap/config/integration-config.xml"/>
  
      <!-- import the content configuration -->
      <import resource="classpath:/com/edorasware/bootstrap/config/content-config.xml"/>
  
  </beans>

In this configuration, the properties are read in using application defaults (provided by the on-premise WAR file) and can be overridden by system properties.

The files one-core-application-context.xml and vis-application-context.xml are provided by edoras one and contain the default edoras one Spring configuration.

The remaining imports are provided by the example on-premise project and contain the configuration for the database connection, security context, basic integration services and the content provider. These configurations are described separately.

The application context can naturally contain any additional Spring configuration needed by the on-premise implementation. The one-application-context.xml can also be included in a project-specific configuration file to improve readability and maintainability (see com/edorasware/acme/config/acme-context.xml in the example project).

Database configuration

In the example project, the database is configured in the com/edorasware/bootstrap/config/database-config.xml file, which is loaded from the classpath:

com/edorasware/bootstrap/config/database-config.xml
  <?xml version="1.0" encoding="UTF-8"?>
  <beans xmlns="http://www.springframework.org/schema/beans"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://www.springframework.org/schema/beans
                             http://www.springframework.org/schema/beans/spring-beans-3.2.xsd">
  
      <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
          <property name="dataSource" ref="dataSource"/>
      </bean>
  
      <beans profile="database-h2">
          <bean id="h2HikariConfig" class="com.zaxxer.hikari.HikariConfig">
              <property name="poolName" value="edorasOnePool"/>
              <property name="connectionTestQuery" value="SELECT 1"/>
              <property name="driverClassName" value="org.h2.Driver"/>
              <property name="jdbcUrl" value="${h2Url}"/>
              <property name="username" value="sa"/>
              <property name="password" value=""/>
              <property name="connectionTimeout" value="30000"/>
              <property name="idleTimeout" value="30000"/>
              <property name="maxLifetime" value="1800000"/>
              <property name="maximumPoolSize" value="4"/>
          </bean>
  
          <bean id="dataSource" class="org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy">
              <property name="targetDataSource">
                  <bean class="com.zaxxer.hikari.HikariDataSource" destroy-method="close">
                      <constructor-arg name="configuration" ref="h2HikariConfig"/>
                  </bean>
              </property>
          </bean>
      </beans>
  
      <beans profile="database-jndi">
          <bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
              <property name="jndiName" value="${databaseJndiName}"/>
          </bean>
      </beans>
  
  </beans>

The active database is selected using a Spring profile which should be set when the application is started. In the bootstrap project the following database profiles are supported by default:

Table 74. Default bootstrap project database Spring profiles

Profile

Description

database-h2

Use a local h2 database. By default the database content will be stored in the edoras-one.home directory.

database-jndi

Use a JNDI database provided by the application server. The default database JNDI name is java:comp/env/jdbc/edorasone.

If you want to externalize the database properties then they can be added to the com/edorasware/bootstrap/config/one.properties properties file, and can then be overwritten either in the installation-specific one.properties file or from the command line.

Security configuration

The application security is configured by files in the folder com/edorasware/bootstrap/config/security. Standard configurations are provided for basic authentication (security-basic-config.xml):

com/edorasware/bootstrap/config/security/security-basic-config.xml
  <?xml version="1.0" encoding="UTF-8"?>
  <beans xmlns="http://www.springframework.org/schema/beans"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns:security="http://www.springframework.org/schema/security"
         xsi:schemaLocation="http://www.springframework.org/schema/beans
                             http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
                             http://www.springframework.org/schema/security
                             http://www.springframework.org/schema/security/spring-security-3.2.xsd">
  
      <import resource="security-config-base.xml"/>
  
      <security:http pattern="/login.html" security="none"/>
      <security:http pattern="/login-error.html" security="none"/>
  
      <security:http pattern="/rest/documents/*/content">
          <security:intercept-url pattern="/**" access="ROLE_USER"/>
          <security:custom-filter ref="browserRedirectionFilter" position="FIRST"/>
          <security:session-management session-fixation-protection="none"/>
          <security:custom-filter ref="sessionManagementFilter" position="SESSION_MANAGEMENT_FILTER"/>
          <security:http-basic/>
          <security:logout/>
      </security:http>
  
      <security:http>
          <security:intercept-url pattern="/**" access="ROLE_USER"/>
          <security:custom-filter position="SWITCH_USER_FILTER" ref="switchUserProcessingFilter"/>
          <security:intercept-url pattern="/j_spring_security_switch_user" access="ROLE_USER"/>
          <security:custom-filter ref="browserRedirectionFilter" position="FIRST"/>
          <security:session-management session-fixation-protection="none"/>
          <security:custom-filter ref="sessionManagementFilter" position="SESSION_MANAGEMENT_FILTER"/>
          <security:http-basic/>
          <security:logout/>
          <security:form-login login-page="${application.endpoint}/login.html"
                               default-target-url="${application.endpoint}/"
                               always-use-default-target="true"
                               authentication-failure-url="${application.endpoint}/login-error.html"/>
      </security:http>
  
      <bean id="sessionManagementFilter"
            class="org.springframework.security.web.session.SessionManagementFilter">
          <constructor-arg name="securityContextRepository"
                           ref="httpSessionSecurityContextRepository"/>
          <property name="invalidSessionStrategy">
              <bean class="com.edorasware.cloud.security.CloudInvalidSessionStrategy">
                  <constructor-arg name="invalidSessionUrl" value="${application.endpoint}/login.html"/>
              </bean>
          </property>
      </bean>
  
      <bean id="httpSessionSecurityContextRepository" class="org.springframework.security.web.context.HttpSessionSecurityContextRepository"/>
  
      <bean id="browserRedirectionFilter" class="com.edorasware.cloud.filter.BrowserRedirectionFilter">
          <property name="loginPageUrlPattern" value="${application.endpoint}/login.html(.*)"/>
          <property name="landingPageUrlPattern" value="${application.endpoint}(.*)"/>
      </bean>
  
      <!-- default authentication manager which uses the default one user details service to get
      the user work objects. The passwords are also encoded with the default password encoder -->
      <security:authentication-manager>
          <security:authentication-provider user-service-ref="userDetailsService">
              <security:password-encoder ref="passwordEncoder"/>
          </security:authentication-provider>
      </security:authentication-manager>
  
  </beans>

In the example root configuration file (com/edorasware/bootstrap/config/one-application-context.xml), the security configuration is imported with the help of the security.type system property which defaults to basic. If you want to create your own security configuration you need to set the security.type property (e.g. to extended) and then create a corresponding security configuration file inside the com/edorasware/bootstrap/config/security folder (e.g. security-extended-config.xml).

Integration configuration

The integration of edoras one with external systems is configured in the file com/edorasware/bootstrap/config/integration-context.xml:

com/edorasware/bootstrap/config/integration-config.xml
  <?xml version="1.0" encoding="UTF-8"?>
  <beans xmlns="http://www.springframework.org/schema/beans"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://www.springframework.org/schema/beans
                             http://www.springframework.org/schema/beans/spring-beans-3.2.xsd">
  
      <beans profile="integration-development">
          <bean id="mailSender" class="com.edorasware.cloud.core.mail.LoggingMailSender"/>
      </beans>
  
      <beans profile="integration-production">
          <bean id="mailSender" class="org.springframework.mail.javamail.JavaMailSenderImpl">
              <property name="host" value="${mail.smtp.host}"/>
              <property name="port" value="${mail.smtp.port}"/>
              <property name="username" value="${mail.smtp.username}"/>
              <property name="password" value="${mail.smtp.password}"/>
              <property name="defaultEncoding" value="${mail.smtp.encoding}"/>
              <property name="javaMailProperties">
                  <props>
                      <prop key="mail.debug">${mail.smtp.debug}</prop>
                      <prop key="mail.transport.protocol">${mail.smtp.transport.protocol}</prop>
                      <prop key="mail.smtp.auth">${mail.smtp.auth}</prop>
                      <prop key="mail.smtp.socketFactory.port">${mail.smtp.port}</prop>
                      <prop key="mail.smtp.socketFactory.class">javax.net.ssl.SSLSocketFactory</prop>
                      <prop key="mail.smtp.socketFactory.fallback">false</prop>
                      <prop key="mail.smtp.quitwait">false</prop>
                  </props>
              </property>
          </bean>
      </beans>
  
  </beans>

This example uses two Spring profiles to define the mail sender bean which is used to send emails.

Details of specific integration options and how they can be configured are provided in the relevant sections of this document.

Content configuration

The storage of file content in edoras one is configured in the file com/edorasware/bootstrap/config/content-config.xml:

com/edorasware/acme/config/content-config.xml
  <?xml version="1.0" encoding="UTF-8"?>
  <beans xmlns="http://www.springframework.org/schema/beans"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns:mongo="http://www.springframework.org/schema/data/mongo"
         xsi:schemaLocation="http://www.springframework.org/schema/beans
                             http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
                             http://www.springframework.org/schema/data/mongo
                             http://www.springframework.org/schema/data/mongo/spring-mongo.xsd">
  
      <!-- configure the content provider -->
      <bean id="contentProvider"
            class="com.edorasware.cloud.core.content.internal.FileBasedContentProvider">
          <constructor-arg value="${fileBasedContentProvider.contentDir}"/>
          <constructor-arg value="${contentProvider.tempDir}"/>
          <constructor-arg value="2"/>
      </bean>
  
      <beans profile="cloud">
          <mongo:db-factory
                  id="mongoDbFactory"
                  host="${mongo.host}"
                  port="${mongo.port}"
                  dbname="${mongo.dbname}"
                  username="${mongo.username}"
                  password="${mongo.password}"/>
          <mongo:mapping-converter id="mongoConverter"/>
  
          <bean class="org.springframework.data.mongodb.gridfs.GridFsTemplate">
              <constructor-arg ref="mongoDbFactory"/>
              <constructor-arg ref="mongoConverter"/>
          </bean>
  
          <bean id="contentProvider" class="com.edorasware.acme.patch.MongoDbContentProvider"/>
      </beans>
  
  </beans>

This example stores content using a file-based content provider by default, or can be overridden using a Spring profile to use MongoDB-based content storage.

7.5.3. Tenant initialization

To use edoras one, you also need to create a tenant. An edoras one installation may support multiple tenants using a single database, where each tenant is completely isolated from the others (i.e. it has it’s own set of users, work objects, Apps etc.).

On startup, edoras one looks for tenant JSON configuration files in the locations configured by the tenant.data.location property. If a configuration file is found for a tenant not already present in the database then that tenant will be initialised according to the tenant configuration file:

Tenant initialization in acme.json
  {
      "name": "acme",
  
      "accounts": [
          {
              "name": "acme",
              "domain": "acme.com",
              "groups": [ "Manager" ],
              "users": [
                  {
                      "firstName": "John",
                      "lastName": "Smith",
                      "login": "john",
                      "email": "john.smith@acme.com",
                      "memberGroups": [ "Manager" ],
                      "language": "en"
                  }
              ]
          }
      ]
  }

Once a tenant is initialised it will not be updated, even if the tenant configuration file is changed at a later date.

For a full description of the tenant JSON format, please refer to the edoras one administration guide.

7.5.4. Logging configuration

In the bootstrap project, we are using log4j as the logging framework. The default logging configuration for log4j is provided by the log4j.properties file in the root package.

Default logging configuration in log4j.properties
  # Comment this line and uncomment the following to allow log writing to a local file
  log4j.rootLogger=INFO, A
  # log4j.rootLogger=INFO, A, local.file
  
  log4j.appender.A=org.apache.log4j.ConsoleAppender
  
  log4j.appender.A.layout=org.apache.log4j.PatternLayout
  log4j.appender.A.layout.ConversionPattern=%d{ISO8601} %-5p %-85.85c - %m%n
  
  ## Spring framework
  #log4j.logger.org.springframework=WARN
  #log4j.logger.org.springframework.integration=DEBUG
  
  ## Spring web
  #log4j.logger.org.springframework.web=DEBUG
  
  ## Project
  #log4j.logger.com.edorasware.customer.acme=DEBUG
  
  log4j.appender.local.file=org.apache.log4j.FileAppender
  log4j.appender.local.file.append=false
  log4j.appender.local.file.file=/tmp/edoras.log
  log4j.appender.local.file.threshold=INFO
  log4j.appender.local.file.layout=org.apache.log4j.PatternLayout
  log4j.appender.local.file.layout.ConversionPattern=%-5p %c: %m%n

If an alternative logging configuration is needed in a given environment, this can be specified without rebuilding the WAR file by setting the appropriate system property when the application server is started, for example by adding -Dlog4j.configuration=file:C:/tmp/log4j.properties to the application server command line.

7.5.5. Property settings

The default property settings are defined in the one.properties file located in the com/edorasware/bootstrap/config folder.

Installation-specific property values can be set using system properties, for example by adding them to the application server command line.

For a full description of the standard edoras one properties, please refer to the edoras one administration guide.

7.5.6. edoras vis configuration

The edoras vis configuration is optional as the defaults should be sufficient for most project. If you still need to change the palette or adapt the edoras vis editor then please read on.

Palette configuration

Palette configuration should be added into the project’s Spring configuration as shown below.

  <bean id="paletteConfiguration" class="com.edorasware.bpm.modeler.config.PaletteConfiguration">
      <property name="paths">
          <list>
              <value>classpath:com/edorasware/vis/palette</value>
          </list>
      </property>
      <property name="baseProcessPalette" value="classpath:com/edorasware/vis/palette/base.process.palette.xml"/>
      <property name="baseFormPalette" value="classpath:com/edorasware/vis/palette/base.form.palette.xml"/>
      <property name="baseCasePalette" value="classpath:com/edorasware/vis/palette/base.case.palette.xml"/>
      <property name="defaultProcessPaletteName" value="default.process.palette.xml"/>
      <property name="defaultFormPaletteName" value="default.form.palette.xml"/>
      <property name="defaultCasePaletteName" value="default.case.palette.xml"/>
  </bean>
Table 75. Supported palette configuration bean properties
Name Description

paths

List of palette folder paths

baseProcessPalette

Fully qualified base process palette file name

baseFormPalette

Fully qualified base form palette file name

baseCasePalette

Fully qualified base case palette file name

defaultProcessPaletteName

Fully qualified default process palette file name

defaultFormPaletteName

Fully qualified default form palette file name

defaultCasePaletteName

Fully qualified default case palette file name

Editor configuration

Editor configuration should be added into the project’s Spring configuration as shown below.

  <bean id="editorConfiguration" class="com.edorasware.bpm.modeler.config.EditorConfiguration">
      <property name="disableSaveDialog" value="true"/>
      <property name="showSystemFormPalette" value="false"/>
      <property name="showSystemProcessPalette" value="false"/>
      <property name="showSystemCasePalette" value="false"/>
      <property name="saveNotificationUrl" value="../rest/modeler"/>
  </bean>
Table 76. Supported editor configuration bean properties
Name Description

disableSaveDialog

Flag to enable or disable showing the save dialog for each save operation

showSystemFormPalette

Flag to show/hide system form palette in the form designer

showSystemProcessPalette

Flag to show/hide system process palette in the process designer

showSystemCasePalette

Flag to show/hide system case palette in the case designer

saveNotificationUrl

URL to which the save notification will be posted

7.5.7. License file

To start edoras one you will need a valid edoras one license file. The license location is configured in the one.properties file located in the com/edorasware/bootstrap/config folder.

com/edorasware/bootstrap/config/one.properties
  edorasware.license = file:${user.home}/.edorasware/edorasware.license

7.5.8. Build and deploy the on-premise WAR file

The project WAR file can now be build using the Maven package target or the Gradle war task. The generated war can then be deployed in a suitable application server.

7.5.9. Developing with Eclipse

This section guides you through the basic setup needed to develop edoras one on-premise projects using the Eclipse IDE. It assumes that you have a copy of the edoras one bootstrap project and have access to the edorasware repository.

If you plan to use the project for real development then you should place the project under some form of source control (e.g. Git or Subversion) before importing it into the IDE and making your own modifications. This will not be covered here.

Add the edoras repository credentials to your Maven configuration

All artifacts required to build and run your edoras one are available in the edoras repository. This repository is usually configured in the POM of your edoras one bootstrap project. The only missing information is the repository access credentials. Add the credentials that you received from edorasware to the servers section in your local Maven configuration file. By default you can find the Maven configuration file at <USER_HOME>/.m2/settings.xml.

    ...
    <servers>
        ...
        <server>
            <id>repo.edorasware.com</id>
            <username>customer-username</username>
            <password>customer-password</password>
        </server>
        ...
    </servers>
    ...
Install Eclipse

Download an appropriate package for your system from the Eclipse project page and install it in a suitable location.

Start Eclipse

Start the installed Eclipse and select or create a workspace:

createWorkspace
Configure the Java runtime

edoras one runs inside an application server and requires larger memory settings than the ones that are available by default in a Java VM. In the following steps we change the memory settings to a suitable value.

Select the WindowPreferences menu item to open the Preferences dialog. Choose the Installed JREs preferences in the Java section. Select the default JRE (that is the one highlighted in bold) and press the Edit…​ button to show the Edit JRE dialog. Add the option -XX:MaxPermSize=128M to the Default VM arguments text field. This increases the maximum permanent generation size to a value that is sufficient for edoras one.

javaMaxPermSize
Import the bootstrap project into your workspace

Select the FileImport…​ menu item to open the project import dialog. Then select Existing Maven Projects:

importMavenProject

Then select the folder where you have extracted the bootstrap project:

importProject
Configure an application server

edoras one is deployed to standard Servlet 3.0 web application servers. The following shows how to integrate the Tomcat 7.0 web application server into Eclipse. Please refer to the Tomcat installation guide to learn how to install Tomcat in your local environment.

Integration of other application servers is done in a similar way.

When you have installed Tomcat to a local directory, select the FileNewOther…​ menu item to open the Select a Wizard dialog. Choose the Server option in the Server section and press the Next button.

serverSelectWizard

In the Define a New Server page of the wizard, choose the Tomcat 7.0 Server type and press the Next button.

serverSelectServerType

In the Tomcat Server page of the wizard, press the Browse…​ button and choose the home directory of your installed Tomcat web application server.

Press the Finish button to create the application server inside Eclipse.

serverDefineTomcatServer

Select the WindowShow ViewServers menu item. This brings you to the Servers view.

serverServersView

Double click the Tomcat 7.0 Server option to open the editor for that server. Change the timeouts in the Timeouts section: change Start to 450 seconds and Stop to 150 seconds. This is required as it takes the application server some time to start up and stop edoras one and the default settings are too small.

serverSetTimeouts

Switch to the Modules tab by clicking the Modules label at the bottom of the editor. In the Modules tab press the Add Web Module…​ button to open the Add Web Module dialog. Choose your project module and press the OK button to deploy your project into the application server.

serverAddWebModule

When you have added the web module, you can change the deployment path by editing the table entry (setting the path when creating the web module does not currently work). Change your deployment path to '/acme' and select the FileSave All menu item to save your changes.

Start the application server

Select the application server in the Servers view and press the Start the server toolbar button.

serverStart

Congratulations, you now have started your edoras one application!

Direct your browser to http://localhost:8080/acme and enjoy!

7.5.10. Developing with IntelliJ IDEA

This section guides you through the basic setup needed to develop edoras one on-premise projects using the IntelliJ IDEA IDE. It assumes that you have a copy of the edoras one bootstrap project and have access to the edorasware repository.

If you plan to use the project for real development then you should place the project under some form of source control (e.g. Git or Subversion) before importing it into the IDE and making your own modifications. This will not be covered here.

Add the edoras repository credentials to your Maven configuration

All artifacts required to build and run your edoras one are available in the edoras repository. This repository is usually configured in the POM of your edoras one bootstrap project. The only missing information is the repository access credentials. Add the credentials that you received from edorasware to the servers section in your local Maven configuration file. By default you can find the Maven configuration file at <USER_HOME>/.m2/settings.xml.

    ...
    <servers>
        ...
        <server>
            <id>repo.edorasware.com</id>
            <username>customer-username</username>
            <password>customer-password</password>
        </server>
        ...
    </servers>
    ...
Install IntelliJ IDEA

Download an appropriate package for your system from the IntelliJ IDEA project page and install it in a suitable location.

Start IntelliJ IDEA

Start the installed IntelliJ IDEA.

Import the bootstrap project into your workspace

Either select Import project from the IDEA start screen, or use the FileOpen…​ menu item from an existing project.

Select the Maven pom.xml (or Gradle build.gradle) from the folder where you extracted the bootstrap project:

importProject

You may see some information dialogs that alternative build files have been detected. These dialogs may be safely ignored.

Check the project language level

Open the project settings dialog with FileProject Structure…​ and check that the Project language level is set to 7:

languageLevel
Configure an application server

edoras one is deployed to standard Servlet 3.0 web application servers. The following shows how to integrate the Tomcat 7.0 web application server into IDEA. Please refer to the Tomcat installation guide to learn how to install Tomcat in your local environment.

Integration of other application servers is done in a similar way.

When you have installed Tomcat to a local directory, select the RunEdit Configurations…​ menu item to open the run configuration dialog:

runConfiguration

Select the + icon to add a new run configuration, and then select Tomcat ServerLocal.

Give the run configuration a suitable name (e.g. bootstrap), and configure the installed Tomcat application server using the Configure…​ button.

bootstrapRunConfiguration

Add the bootstrap deployment artifact by selecting the Deployment tab and adding the deployment artifact edoras-one-bootstrap:war exploded:

deployArtifact

When it has been added, you can change the Application context setting to /acme by selecting the artifact in the deployment list.

Start the application server

Select the bootstrap run configuration from the selection widget in the IDEA toolbar, and then press the green arrow to the right to start the server.

runServer

Congratulations, you now have started your edoras one application!

Direct your browser to http://localhost:8080/acme (if it has not been opened automatically) and enjoy!

7.6. Customizing edoras one

This section describes the extension points for common on-premise use cases. If no suitable extension point is documented for a feature that you require then contact {edw-support} to find a solution.

7.6.1. Overriding edoras one bean definitions

The default edoras one Spring bean configurations can be overridden if a new Spring bean is defined with the same ID after the base configuration has been imported (if two bean definitions have the same ID then the last definition wins).

7.6.2. Action event listeners

Custom work object action event listeners can be added to the edoras one configuration by creating the action listener implementation and then registering it as a bean with a bean ID that matches the following pattern:

customer{First|Last}{Task|Case|Process|WorkObject}ActionListener

customerFirst listeners will be invoked before all other listeners, customerLast listeners will be invoked after all other listeners.

Note
If more than one listener is required, then the listeners can be combined using a composite action listener (Composite{Task|Case|Process|WorkObject}ActionListener) and the composite listener registered using the above naming convention.

As an example, a listener to log the creation of new task objects can be defined:

LoggingTaskActionListener.java
  package com.edorasware.acme.listeners;
  
  import com.edorasware.gear.core.task.support.TaskActionEvent;
  import com.edorasware.gear.core.task.support.TaskActionListener;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  public class LoggingTaskActionListener implements TaskActionListener {
      private static final Logger LOG = LoggerFactory.getLogger(LoggingTaskActionListener.class);
  
      @Override
      public void actionWillBePerformed(TaskActionEvent event) {
          if (event.isCreationEvent()) {
              LOG.info("About to create task {}", event.getNewTask().getId());
          }
      }
  
      @Override
      public void actionPerformed(TaskActionEvent event) {
          if (event.isCreationEvent()) {
              LOG.info("Created task {}", event.getNewTask().getId());
          }
      }
  }

and then the bean can be defined with a suitable ID to register it with the task service:

acme-context.xml
  <bean id="customerLastTaskActionListener"
        class="com.edorasware.acme.listeners.LoggingTaskActionListener"/>

For details on action listener implementation, please refer to the edoras gear documentation.

7.6.3. Automatic App updates

When a new tenant is initialized an App called the System App is automatically installed. This App contains the models required for correct operation of the edoras one software, and is typically loaded from the edoras one artifacts as it is strongly tied to a specific edoras one release.

NOTE

It is strongly recommended that the System App be updated automatically to make sure that the latest version is active after the edoras one dependency has been updated to a newer version (which may also include a new System App).

Section [_app_development_workflow] describes how to configure workflows to support transfer of Apps between systems with automatic updates, and the section [_incoming_adapter_lifecycle] describes how the pre-defined system app adapter can be configured to perform automatic updates of the System App. :acme-root-dir: ../../../edoras-one-bootstrap/ :acme-src-dir: ../../../edoras-one-bootstrap/src/main/

7.6.4. Expressions and service beans

The powerful expression resolver used in the edoras one server supports access to arbitrary Spring bean property values and methods. This capability can be used in on-premise projects to integrate custom Java code with process models via expressions: service beans can be written in Java and registered with the expression resolver, and the appropriate methods can then be invoked from a process model at the appropriate time. There are many potential ways to use this capability, but some typical use cases might be:

  • data initialization

  • data conversion

  • encapsulation of complex business logic

  • integration with external systems

Defining service beans

Service beans are simply plain Java objects registered as a Spring bean with a particular ID.

As an example, if we want to create a service to generate new task names, we can create the Java class:

TaskNameService.java
  package com.edorasware.acme.expression;
  
  import org.activiti.engine.delegate.DelegateExecution;
  
  import java.util.UUID;
  import java.util.concurrent.atomic.AtomicInteger;
  
  /**
   * An example service bean that allows UUID strings to be created from a process model.
   */
  public class TaskNameService {
  
      private static AtomicInteger taskCount = new AtomicInteger();
  
      /**
       * Returns a new task name based on the given base name.
       *
       * @param baseName the base name
       * @return the task name
       */
      public String nextTaskName(String baseName) {
          return baseName + "-" + taskCount.incrementAndGet();
      }
  }

and use this to create a corresponding Spring bean with a suitable ID, either explicitly:

acme-context.xml
  <bean id="taskNameService"
        class="com.edorasware.acme.expression.TaskNameService"/>

or by adding an appropriate Spring annotation (e.g. @Service) to the class definition and enabling the Spring component scan for the relevant package:

acme-context.xml
  <!-- Scan the project-specific classes to locate REST controllers etc. -->
  <context:component-scan base-package="com.edorasware.acme"/>

To allow open access to all available Spring beans from a process definition would be a big security problem, so edoras one only allows the expression resolver to access a limited set of beans. These beans are defined by the expression.bean.whitelist property. To allow access to the bean that we have defined we therefore have also to override the default value of this property to include the new bean ID:

`com/edorasware/bootstrap/config/one.properties
  # Allows beans to be resolved in the expression resolver.
  # CAUTION: this means that methods may be executed on the bean from
  # user-provided expressions, so watch out for security loopholes!
  expression.bean.whitelist = convert2PdfActivityExecutor,documentActivityExecutor,\
    mailActivityService,processVariables,restVariableService,date,caseActivityExecutor,\
    commentService,identityManager,modelManager,taskNameService,acmeService
Accessing service beans

Once the bean has been defined and made accessible, it can be used by expressions within process definitions. As an example we can use the taskNameService bean to generate task names for user tasks:

serviceBean
Passing values using method parameters

The simplest way to pass values to a service bean method is to simply use the expression to provide the relevant values to the method call. We used this approach in the previous example when we passed in the name of the parent case:

Method invocation with method parameters
  #{taskNameService.nextTaskName(case.name)}

This approach has a number of advantages:

  • the input values are modeled, and it is therefore relatively easy for the modeler to understand what is happening

  • the same method may be used in different contexts where the source values come from different places

  • changes to variable names can be made without affecting running processes (old processes will continue to use the old variable name)

It works well for simple utility function when only a few parameters are required, but as the number of parameters increases, the method invocations become complex and therefore awkward to write and maintain.

Passing values using the execution context

For more complex use cases, an alternative is to provide the current Activiti execution context as a parameter to the service task method and allow the method itself to extract the information it needs and make any changes that may be required. When doing this, it is useful to extend the AbstractActivityService class, as this provides a number of useful utility methods for working with execution context, such as locating the current work object. The following example service uses the execution context to add a message to a variable in the root work object:

AcmeService.java
  /**
   * An example service bean that uses an execution context.
   */
  @Service
  public class AcmeService extends AbstractActivityService {
  
      // type-safe variable definition for the service message
      private static final VariableName<String, String> SERVICE_MSG =
              VariableName.create("serviceMsg", String.class);
  
      @Autowired
      private GenericWorkObjectService workObjectService;
  
      /**
       * Adds a service invocation message to the root work object.
       *
       * @param execution the execution context
       */
      public void invoke(DelegateExecution execution) {
          WorkObject<?, ?, ?> workObjectInScope = getWorkObjectInScope(execution);
          AnyWorkObject rootObject = getRootObject(workObjectInScope);
  
          WorkObjectUpdate.Builder<?, ?, ?, ?, ?> updateBuilder =
                  this.workObjectService.createUpdateBuilder(rootObject.getGlobalId());
  
          String message = "AcmeService was invoked at " + getTimestamp()
                  + " with tag " + getTag(execution);
  
          updateBuilder.putVariable(SERVICE_MSG, message);
  
          this.workObjectService.apply(updateBuilder.build(), "ACME service invocation");
      }
  
      /**
       * Returns the value of the tag property in the task definition.
       *
       * @param execution the execution context
       * @return the value of the tag property
       */
      private String getTag(DelegateExecution execution) {
          Map<String, String> properties = getCurrentTaskStringProperties(execution);
          String value = properties.get("edoras:tag");
          return hasText(value) ? value : "UNDEFINED";
      }
  
      /**
       * Returns a string with the current timestamp.
       *
       * @return the current timestamp
       */
      private String getTimestamp() {
          DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm");
          return dateFormat.format(new Date());
      }
  }

The execution context is passed into the method call by using the execution keyword within the expression:

Method invocation with execution context
  #{acmeService.invoke(execution)}

This approach delegates full control to the service implementation, allowing it to navigate the work object hierarchy to read or modify whatever it needs to. Obviously, this is very powerful, but it also means that the values used by the method are not visible to the modeler, making it hard to see what is really going on. It is therefore important to try to make the behaviour as intuitive as possible and to provide good documentation for the available services to the modelers.

As the data used by the implementation is coded in Java rather then being part of the model, it is also more complicated to change the way that values are stored in the work object hierarchy. Any change to the values used by the service will take effect immediately for all processes that use that service in the future, including processes that are already running.

Managing database transactions in service beans

Service bean method invocations are typically executed from within a process. The process engine already manages the transactions used during process execution, and so no additional transaction handling needs to be provided by service bean implementations.

Security

As service beans have access to the full functionality of Java and edoras one, it is also important that security is considered when creating a new service bean implementation. Users should not be able to use a service bean to gain access to confidential information to which they would not otherwise have access, to corrupt or delete information, or to interfere with the normal functioning of edoras one.

Creating custom service tasks in the process palette

We have already seen how service beans can be invoked using expressions. When specific service beans are used on a regular basis, it may be convenient to extend the process palette in the process modeler to include customized service tasks for common use cases.

Details of how to customize the process palette can be found in the section Customizing palettes.

As an example, we start by creating a patch to the default edoras one process palette (thereby reusing all of the existing palette configuration):

acme.process.palette.xml
  <palette id="acme-process-palette"
           resource-bundle="translation"
           apply-patch-to-palette="edoras-one-process-palette"
           xmlns="http://www.edorasware.com/schema/vis/process-palette"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://www.edorasware.com/schema/vis/process-palette
                               http://www.edorasware.com/schema/vis/edoras-vis-process-palette-2.0.43.xsd">
  
      <!-- palette configuration... -->
  </palette>

To make our custom tasks easy to find, we will create a separate group to contain them:

acme.process.palette.xml
  <group id="acme-tasks">
      <component id="acmeservicetask" extends="ServiceTask"
                 attribute-groups="acmeServiceTaskAttributes"/>
  </group>

In the process modeler, this group is now added to the existing ones:

acme group

We can then define the attributes that should appear when the service task is edited. For our example, we will provide a default name and background color, set the expression to invoke the service (not visible in the modeler), define a new tag attribute, and suppress some of the additional service task attributes that are not needed:

acme.process.palette.xml
  <attribute-groups>
      <attribute-group id="acmeServiceTaskAttributes">
  
          <!-- Provide suitable default values for base attributes -->
          <attribute category="common" id="name" type="SimpleText"
                     value="ACME service task"
                     readonly="false" optional="true"
                     ref-to-view="text_name"/>
          <attribute id="bgcolor" category="common" type="Color"
                     value="#ccffcc" index="190"
                     readonly="false" optional="false"
                     ref-to-view="fill_el" fill="true" stroke="false"/>
  
          <!-- use a fixed expression to invoke the service -->
          <attribute id="expression" category="edoras" type="SimpleText"
                     value="#{acmeService.invoke(execution)}"
                     readonly="true" optional="false" visible="false"/>
  
          <!-- add a custom attribute that we can access in the service -->
          <attribute id="tag" category="edoras" type="SimpleText"
                     value="default" export="true"
                     readonly="false" optional="false"/>
  
          <!-- suppress attributes that aren't needed -->
          <attribute id="delegateExpression" category="edoras" type="SimpleText"
                     value=""
                     readonly="false" optional="true" visible="false"/>
          <attribute id="resultVariable" category="edoras" type="SimpleText"
                     value=""
                     readonly="false" optional="true" visible="false"/>
          <attribute id="class" category="edoras" type="SimpleText"
                     value=""
                     readonly="false" optional="true" visible="false"/>
  
      </attribute-group>
  </attribute-groups>

Note that the tag attribute is marked as 'exported', meaning that it will be added to the final task definition as a property, where it can be accessed by the service bean. The resulting definition property key has the edoras: prefix to avoid possible collisions with other properties used in the process engine.

The display strings used in the modeler are supplied by the corresponding resource bundle, allowing them to be translated:

translation.properties
  acme-tasks.title = ACME tasks
  acmeservicetask.title = ACME service task
  acmeServiceTaskAttributes.tag.title = Tag
  acmeServiceTaskAttributes.tag.description = Tag

When we add the custom task to a process in the modeler, we can see that the default attribute values are applied, and that some of the normal service task attributes are no longer available:

acme attributes

7.6.5. REST services

REST controller classes in edoras one are defined as plain Java classes annotated with the standard Spring Web annotations:

ReferenceDataController.java
  /**
   * Controller that provides basic application reference data.
   */
  @Controller
  @RequestMapping(value = "/referencedata")
  public final class ReferenceDataController {
      // ...
  }

The individual REST endpoints are defined within a REST controller using annotations on the endpoint implementation methods. For example, the following method defines a REST endpoint for the GET HTTP request method, with the URL /referencedata/domains (the concatenation of the class and method request mappings). It also supports the optional typedText request parameter and returns a list of domains which will be encoded in JSON format:

ReferenceDataController.java
  /**
   * Look up domains that match the given typed text.
   *
   * @param typedText the text typed (usually from an auto-complete field)
   * @return a list of matching domains with an OK response code
   */
  @RequestMapping(value = "domains", method = RequestMethod.GET)
  @ResponseBody
  public ResponseEntity<List<Domain>> getDomains(
          @RequestParam(required = false) String typedText)
  {
      LOG.debug("Requesting domains for '{}'", typedText);
  
      List<Domain> domains = this.referenceDataManager.getDomains(typedText);
  
      return new ResponseEntity<>(domains, RestUtils.JSON_HTTP_HEADERS, HttpStatus.OK);
  }

To activate the REST service controller, you can either declare it explicitly as a bean in the Spring configuration, or use a Spring component scan to automatically locate annotated beans in a given package:

acme-context.xml
  <!-- Scan the project-specific classes to locate REST controllers etc. -->
  <context:component-scan base-package="com.edorasware.acme"/>

In both cases, the REST endpoints defined by the REST controller will be published under the path '/rest' relative to the edoras one base URL. So if the main edoras one application is accessible under the URL http://localhost:8080/acme, the domains endpoint shown above will be accessible under http://localhost:8080/acme/rest/referencedata/domains.

Managing database transactions in REST services

edoras one uses Spring Transaction Management to manage transactions.

If your REST controller methods access a database then you should add the appropriate Spring transaction annotations so that Spring can correctly manage the transactions. You can either add the annotations to the REST controller methods directly, or to classes invoked by the REST controller. Wherever the annotations are added, it is important that the scope of the annotated methods is sufficient, so that multiple database operations will be grouped correctly within a single transaction.

As an example, the REST controller for the reference data service delegates calls to a ReferenceDataManager implementation:

DatabaseReferenceDataManager.java
  /**
   * Manages reference data using persistent database storage.
   */
  public class DatabaseReferenceDataManager implements ReferenceDataManager {
  
      @Override
      @Transactional(readOnly = true)
      public List<Domain> getDomains(String typedText) {
          // ...
      }
  
      // ...

Note that only the database-driven implementation needs to be annotated in this way. The alternative StaticReferenceDataManager implementation used for testing is driven by static data and therefore does not need any transaction annotations.

Supporting edoras form REST-based widgets

The REST APIs needed to support specific form widgets in edoras one are described in detail in the {modeler-guide}.

The Dynamic Link Button widget requires a REST endpoint with a specific response (see the {modeler-guide} for details). To simplify the implementation of REST endpoints to support this widget, a Java class is provided that directly supports this response (NavigationResponse):

NavigationController.java
  import com.edorasware.bootstrap.rest.util.NavigationResponse;
  import com.edorasware.one.permission.view.ViewName;
      // ...
  
      /**
       * Create a navigation response for the next (oldest) task.
       *
       * @return the navigation response or NOT_FOUND if no task is available
       */
      @RequestMapping(value = "nextTask", method = RequestMethod.GET)
      @ResponseBody
      public ResponseEntity<NavigationResponse> getNextTask() {
          LOG.debug("Requesting next task navigation");
  
          TaskQuery query = TaskQuery.builder()
                  .predicate(Task.STATE.isActive())
                  .sorting(Task.CREATION_TIME.orderDesc())
                  .limit(1)
                  .build();
  
          Task task = this.taskService.findTask(query);
          if (task == null) {
              return new ResponseEntity<>(HttpStatus.NOT_FOUND);
          }
  
          NavigationResponse response = NavigationResponse.get(task, ViewName.BROWSE);
          return new ResponseEntity<>(response, RestUtils.JSON_HTTP_HEADERS, HttpStatus.OK);
      }

7.6.6. Other project-specific Spring beans

Any project-specific Spring beans can also be configured directly in the application context (or in a file that is included by the application context). For example we can configure a ReferenceDataManager instance:

acme-context.xml
  <bean class="com.edorasware.acme.services.referencedata.internal.StaticReferenceDataManager">
      <constructor-arg>
          <bean class="com.edorasware.cloud.core.config.DerivedStringListFactoryBean">
              <property name="sourceString" value="Switzerland,Swaziland,Spain"/>
          </bean>
      </constructor-arg>
  </bean>

This can be injected automatically where it is needed, for example into a reference data REST controller.

7.7. Customizing palettes

edoras vis allows the palette used to design processes, forms and cases to be customized. One or more customized palettes of form, process or case widgets can be created by extending the generic widgets supported by edoras vis. Once created, each custom palette can be associated with one or more models of the appropriate type. When a custom palette has been associated with a model all the widgets from the palette will be available in the appropriate editor view.

A custom form palette has to be configured in an XML file that has the .form.palette.xml file extension. It should conform to the edoras-vis-form-palette XML schema definition.

A custom process palette has to be configured in an XML file that has the .process.palette.xml file extension. It should conform to the edoras-vis-process-palette XML schema definition.

A custom case palette has to be configured in an XML file that has the .case.palette.xml file extension. It should conform to the edoras-vis-case-palette XML schema definition.

7.7.1. Configuring palette locations

edoras vis can be configured with multiple locations containing the palette definitions for a workspace. To configure additional palette locations the paletteConfiguration bean will need to be overwritten with an updated paths property:

modeler-config.xml
  <!-- configure the modeler palettes -->
  <bean id="paletteConfiguration" class="com.edorasware.bpm.modeler.config.PaletteConfiguration">
      <property name="paths">
          <list>
              <value>classpath:com/edorasware/vis/palette</value>
              <value>classpath:com/edorasware/acme/palette</value>
          </list>
      </property>
  
      <property name="baseProcessPalette"
                value="classpath:com/edorasware/vis/palette/base.process.palette.xml"/>
      <property name="baseFormPalette"
                value="classpath:com/edorasware/vis/palette/base.form.palette.xml"/>
      <property name="baseCasePalette"
                value="classpath:com/edorasware/vis/palette/base.case.palette.xml"/>
      <property name="defaultProcessPaletteName"
                value="default.process.palette.xml"/>
      <property name="defaultFormPaletteName"
                value="default.form.palette.xml"/>
      <property name="defaultCasePaletteName"
                value="default.case.palette.xml"/>
  </bean>

Palette locations may be specified either with classpaths as shown here, or with absolute filesystem paths. Classpath locations simplify deployment but mean that palette changes will need to be deployed by rebuilding the deployment artifact. Filesystem locations may be system-specific but would allow palettes to be updated without rebuilding the deployment artifact. The latter may be especially useful in a development environment where palettes are being modified regularly.

All of the custom palette XMLs located directly under the configured folder will be available and can be associated with an edoras vis model. One model can only be associated with one custom palette configuration.

If a custom palette XML is deleted or if any of its custom widgets are deleted, the widgets in models associated with that custom palette will automatically fallback to their respective generic widget parent(s) when the models are subsequently opened for editing.

7.7.2. Configuring a custom palette

The root element of a custom palette XML file supports the following attributes:

Table 77. Attributes and elements supported by the palette element
Name Mandatory Description

id

true

Id of the palette.

title

false

The title of custom palette that is displayed in edoras vis editor view.

parent-palette

false

The parent palette from which a custom palette is extended.

edoras vis supports following parent process palettes:

  • edoras-gear (default value)

  • bpmn2.0

edoras vis supports following parent form palette:

  • edoras-forms (default value)

hide-parent-palette-elements

false

The boolean value to hide stencils of the parent palette. Defaults to true.

apply-patch-to-palette

false

The name of a palette to be patched. The contents of the named palette will be modified / extended instead of a new palette being created.

resource-bundle

false

Resource Bundle File containing language specific translations. All resource bundle files should be placed under an i18n directory in the same directory as the palette XML file. Refer to Defining resource keys for language specific attributes in custom palette for more details.

See Sample custom palette for an example.

Defining widget groups in a custom palette

edoras vis supports grouping of widgets within custom palettes.

Table 78. Attributes supported by the group element
Name Mandatory Description

id

true

Unique Id of the group

title

false

Title of the group

description

false

Description of the group

index

false

Index of the group, Sorted in ascending order

Sample group element is shown below.

<group id="Events" title="Events" description="Events description" index="5" >
    <component id="events_start" extends="StartNoneEvent"
               attribute-groups="commonAttributes, formControlAttributes"/>
    <component id="events_end" extends="EndNoneEvent" description="End Event">
        <attribute id="custom_event_attribute" title="Custom Event Attribute" value="custom"/>
    </component>
</group>
Defining widgets in a custom palette

edoras vis supports creating of custom widgets in a custom palette by extending/referencing available generic widgets.

To add a widget to the custom palette, a widget element has to be added to the group element.

The widget element in turn may contain one or more attribute elements which define the widget specific properties.

Table 79. Attributes supported by the widget element
Name Mandatory Description

id

true

Unique Id of the widget

title

false

Title of the widget

description

false

Description of the widget

visible

false

Configure visibility of the widget in editor.

extends

false

Id of the widget that is extended

ref

false

Id of the widget that is referenced

roles

false

Comma separated list of roles supported by the widget, Which are inturn used by rules. See Defining rules in a custom palette for more details.

attribute-groups

false

Comma separated list of attribute group id that need to be added to the widget. See Defining attribute groups in a custom palette for more details.

presentation-id

false

Presentation reference required to render the widget SVG/Image on the editor canvas. See [defining-custom-component-presentation-in-a-custom-palette] for more details.

default-lane-id

false

Applicable only for Pool type.Configure default lane Id for a pool.

shortcut-menu-index

false

Applicable only for Process palette. Configure index for short cut menus from process widgets.

index

false

Index of the widget, sorted in ascending order

behaviour-type

false

Applicable only for Form palette. Describes the behaviour of the referred widget.

1. Widget extending StartNoneEvent with attribute-groups:
<component id="events_start" extends="StartNoneEvent" attribute-groups="commonAttributes, formControlAttributes" ></component>

2. Widget extending Task with custom widget-presentation and short cut menu index:
<component id="formtask" extends="Task" presentation-id="presentation.task" shortcut-menu-index="1" ></component>

3. Widget referencing EndNoneEvent with a custom attribute and index:
<component id="events_end" description="End Event" ref="EndNoneEvent" index="5">
    <attribute id="custom_event_attribute" title="Custom Event Attribute" value="custom"/>
</component>

4. Widget behaving like a password widget:
<component id="base-password" extends="component" presentation-id="password.presentation" behaviour-type="Password" />
Defining custom categories in a custom palette

edoras vis supports defining of custom categories for attributes. These categories can be used to display the attributes of a widget under different headings in the property window of the editor view.

Table 80. Attributes supported by the category element
Name Mandatory Description

id

true

Unique Id of the category

title

false

Title of the category

index

false

Index of the category. It decides the position of category in property window

visible

false

Boolean value to hide the attributes category in property window

Sample custom-categories element is shown below.

<custom-categories>
    <category id="custom_category_1" index = "101" title="custom category 1(en)" />
    <category id="custom_category_2" title="custom category 2(en)"/>
    <category id="custom_category_3" title="custom category 3(en)" visible="false"/>
</custom-categories>

Sample application of custom categories to attributes is shown below.

<attribute id="namekey"  value=""   type="SimpleText" category= "custom_category_1" />
<attribute id="name" title="Name" description="Name" value="" type="SimpleText" category="custom_category_2"/>
<attribute id="behavior" type="ComboBox" category= "custom_category_1"  index="3" title="behavior">
    <items>
        <item id="none" title="none" value="none" />
        <item id="all" title="all" value="all" />
        <item id="one" title="one" value="one" />
        <item id="complex" title="complex" value="complex" />
    </items>
</attribute>
Defining attributes in a custom palette

edoras vis supports defining custom attributes in the custom palette configuration.

To add an attribute with in custom palette, an attribute element has to be used. This element can only exist with in any of the following parent elements:

  • attribute-group

  • custom-attributes-group

  • model-attributes

  • component

Table 81. Attributes supported by the attribute element
Name Mandatory Description

id

true

Unique identifier of the attribute

title

false

Title of the attribute

description

false

Description of the attribute

value

false

Value of the attribute

type

false

Type of the attribute. Check Attribute types supported in a custom palette for more details.

category

false

Name of the category under which the attribute should be displayed in the property window of editor view.

export

false

Boolean value to specify whether an attribute should be exported to the xml and json(only for forms).

index

false

Integer value to define position of the attribute in the property window of the editor view.

ref-to-view

false

Id of the SVG element to which attribute value is mapped. Know more ref-to-view

readonly

false

Boolean value to make the attribute readonly.

optional

false

Boolean value to make the attribute mandatory.

visible

false

Boolean value to make the attribute visible in the property window.

filter

false

Applicable only for TreeView type. A comma separated list of file extensions. Know more filter

multilanguage

false

Boolean value to specify whether the attribute supports multilanguage. Know more multilanguage

fill

false

Applicable only for Color type. Know more fill

url

false

Applicable only for RestComboBox type.

stroke

false

Applicable only for Color type.

multiselect

false

Applicable only for RestComboBox type.

constant

false

Boolean value to make attribute value as constant.

length

false

Integer value to support maximum length for value.

popular

false

Boolean value to make attribute to has higher priority while displaying.

field-map

false

Applicable only for RestComboBox type. To map item display-name and value returned in the REST end point’s response.

item-icon-visible

false

Applicable only for RestComboBox type. To make each item’s icon visible.

select-all

false

Applicable only for RestComboBox type. To pre-select all the items returned by the REST end.

custompalette

false

Applicable only for TreeView type. To provide name of the palette for default selection in tree view editor’s drop-down list.

model-id

false

Applicable only for TreeView type.

runtime

false

Applicable for all type attributes. To make run time attribute visible. Know more runtime and runtime-value

runtime-value

false

Provide default value to run time attribute. Know more runtime and runtime-value

readonly-editor

false

Applicable only for RestComplex and Complex type. To provide non editable editor.

placeholder

false

Applicable only for TreeView type.

output-mapping

false

Applicable only for complex editor type.

edoras vis support multiple languages.

Table 82. Language specific attributes supported by the attribute element
Name Mandatory Description

title

false

If title is Resource bundle key, Then title will be picked form resource bundle file.

description

false

If description is Resource bundle key, Then description will be picked form resource bundle file

value

false

If value is Resource bundle key, Then value will be picked form resource bundle file

Defining resource keys for language specific attributes in custom palette

edoras vis supports defining language specific for custom attributes/widgets in the custom palette configuration.

edoras vis follows below format while generating translation keys.

Table 83. Tranlation keys for supported elements
Element Translation Key Format

Component

Component supports title and description as language specific property

<component-id>.title
<component-id>.description

Group

Group supports title and description as language specific property

<group-id>.title
<group-id>.description

Attribute

Attribute support only title, description and value as language specific property

If attribute element is within the widget element then the format of key is

<component-id>.<attribute-id>.title
<component-id>.<attribute-id>.description
<component-id>.<attribute-id>.value

If attribute element is within the attribute-goup element then the format of key is

<attribute-group-id>.<attribute-id>.title
<attribute-group-id>.<attribute-id>.description
<attribute-group-id>.<attribute-id>.value

If attribute element is within the custom-attributes-goup element then the format of key is

<custom-attributes-group-id>.<attribute-id>.title
<custom-attributes-group-id>.<attribute-id>.description
<custom-attributes-group-id>.<attribute-id>.value

If attribute element is within the model-attributes element then the format of key is

<model-attributes-id>.<attribute-id>.title
<model-attributes-id>.<attribute-id>.description
<model-attributes-id>.<attribute-id>.value

Category

Category support only title as language specific property

<category-id>.title
Defining validation rules for attributes in a custom palette

edoras vis supports defining validation rules for custom attributes in the custom palette configuration.

edoras vis supports following validation rules.

Table 84. Validation rules
Rule Description Example

Range

This rule can be applied to integer attributes to specify minimum and maximum allowed values.

<attribute id="cCardCvv" type="Integer"  title="Credit Card CVV" description="Credit Card CVV" optional="false">
    <validation-rules>
        <range min="100" max="999" />
    </validation-rules>
</attribute>

Length

This rule can be applied to specify the minimum and maximum length of value.

<attribute id="cCard" type="SimpleText"  title="Credit Card Number" description="Credit Card Number" optional="false">
    <validation-rules>
        <length min="0" max="16" />
    </validation-rules>
</attribute>

Expression

This rule can be applied to validate value against JavaScript expression.

<attribute id="cCard" type="SimpleText"  title="Credit Card Number" description="Credit Card Number" optional="false">
    <validation-rules>
        <expression pattern="^4[0-9]{12}(?:[0-9]{3})?$" />
    </validation-rules>
</attribute>
Attribute types supported in a custom palette

edoras vis supports following attribute types.

Table 85. Attribute types
Type Field Type Example

SimpleText

Text Field

<attribute id="name" title="Name" description="Name" value="" type="SimpleText"/>

TextArea

Text Area

<attribute id="summary" title="Summary" description="Summary" value="" type="TextArea" />

Integer

Number Field

<attribute id="age" title="Age" description="Age" value="15" type="Integer" category="common" />

Float

Number Field

<attribute id="price" title="Price" description="Price" value="1.11" type="Float" category="common" />

Boolean

Check Box

<attribute id="isValid" title="Is Valid" description="Is Valid" value="true" type="Boolean"/>

ComboBox

ComboBox

<attribute id="selectnumber" title="Select Number" description="Select Number" value="2" type="ComboBox">
    <items>
        <item id="1" title="One" value="1" />
        <item id="2" title="Two" value="2" />
        <item id="3" title="Three" value="3" />
    </items>
</attribute>

Check Special attribute items for more details.

RestComboBox

ComboBox

<attribute id="company" title="Company" description="Company" url="http://www.company.com" type="RestComboBox"/>

TextEditor

Text Editor

<attribute id="notesShort" title="NotesShort" type="TextEditor" value="" description="Short notes."/>

RichText

Rich Text Editor

<attribute id="documentation" title="Documentation" description="Documentation" value="" type="RichText"/>

TreeView

Tree view dialog

<attribute id="form_entry" title="FormRef" type="TreeView" value="" description="Form referenced by Form Task." filter="xfm" optional="false" readonly="true" />

For supported key event Click here [key-events-for-treeview-dialog]

Date

Date field

<attribute id="duedate" title="Due Date" description="Due Date" value="" type="Date"/>

ComplexDate

Date Editor

<attribute id="min-date" type="ComplexDate" value="" readonly="false" path="extraSettings.minDate" export="true" category="edoras" index="105"/>

Color

Color field

<attribute id="bgcolor" title="BackgroundColor" type="Color" value="#ffffcc" description="" ref-to-view="fill_el" fill="true" stroke="false" optional="false"/>

Complex

Complex dialog

<attribute id="in" title="In" type="Complex" description="Execution element for data input." value="">
    <complex-items>
        <complex-item id="source" name="Source" type="SimpleText" value="" width="150" />
        <complex-item id="target" name="Target" type="SimpleText" value="" width="150" />
    </complex-items>
</attribute>

For supported key event Click here [key-events-for-complex-dialog]

ComplexKeyValue

Complex dialog

<attribute id="in" title="In" type="ComplexKeyValue" description="Property for Complex Key Value editor." value="">
    <complex-items>
        <complex-item id="key" name="Key" type="SimpleText" value="" width="150" />
        <complex-item id="value" name="Value" type="SimpleText" value="" width="150" />
    </complex-items>
</attribute>

For supported key event Click here [key-events-for-complex-dialog]

ComplexForm

Form dialog

<attribute id="email" title="Email Properties" type="ComplexForm" description="Email properties" value="">
    <complex-items>
        <complex-item id="from" name="From" type="SimpleText" value="" width="250" vtype="expressionOrEmail" optional="false" />
        <complex-item id="to" name="To" type="SimpleText" value="" width="250" vtype="expressionOrEmailList" optional="false" />
        <complex-item id="cc" name="Cc" type="SimpleText" value="" width="250" vtype="expressionOrEmailList" />
        <complex-item id="bcc" name="Bcc" type="SimpleText" value="" width="250" vtype="expressionOrEmailList" />
        <complex-item id="subject" name="Subject" type="SimpleText" value="" width="250" />
        <complex-item id="html" name="Html" type="RichText" value="" width="250" />
    </complex-items>
</attribute>

Check Special Attribute complex-items for more details.

Link

Link field

<attribute id="link" title="SimpleLink" type="Link" value="http://www.google.com" />

ComplexTrigger

Complex dialog

<attribute id="ComplexTrigger" type="ComplexTrigger" title="Complex Trigger" value="" optional="true" readonly="false" category="edoras" />

RestComplex

ComboBox

<attribute id="RestComplex" readonly-editor="false" type="RestComplex" title="RestComplex" value="" url="../rest/one-groups" readonly="false" optional="true" category="edoras">
        <complex-items>
            <complex-item id="id" type="SimpleText" value="" name="ID" optional="false" />
            <complex-item id="title" type="SimpleText" value="" name="Title" optional="true" />
            <complex-item id="value" type="SimpleText" value="" name="Value" />
        </complex-items>
</attribute>
Defining custom widget presentation in a custom palette

edoras vis supports defining the SVG and Image for a widget.

Table 86. Attributes supported by the widget-presentations element
Name Mandatory Description

base-palette-icon-path

true

Base folder path where all related images are present.

base-editor-view-path

true

Base folder path where all related SVG file are present.

Table 87. Attributes supported by the widget-presentation element
Name Mandatory Description

id

true

Id of the widget presentation.

palette-icon-path

false

Path of image file used in presentation.

editor-view-path

false

Path of SVG file used in presentation.

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<component-presentations base-palette-icon-path="icons" base-editor-view-path="view">
    <component-presentation id="presentation.number" palette-icon-path="number.png" editor-view-path="number.svg"/>
    <component-presentation id="presentation.password" palette-icon-path="password.png" editor-view-path="password.svg"/>
</component-presentations>

For detailed explanation on oryx related elements and attributes in SVG view check here SVG view features

Defining rules in a custom palette

edoras vis supports defining the rules for a widget behaviour.

Table 88. Rules supported by an edoras vis custom palette widgets
Name Mandatory Description

containment-rules

This rule can be applied to widgets to specify containment rules based on roles.

<containment-rules>
    <containment-rule role="base_container" contains="standard_container,core_form_controls"/>
    <containment-rule role="standard_container" contains="core_form_controls"/>
</containment-rules>

morphing-rules

This rule can be applied to widgets to specify morphing rules (Changing from one type to another) based on roles.

<morphing-rules>
    <morphing-rule role="controls_morph" base-morphs="Input"/>
</morphing-rules>

connection-rules

This rule can be applied to widgets to specify connection rules between widgets based on roles (Used Only in Process Custom Palettes).

<connection-rules>
    <connection-rule role="SequenceFlow">
        <connects from="sequence_start" to="sequence_end"/>
    </connection-rule>
    <connection-rule role="MessageFlow">
        <connects from="messageflow_start" to="messageflow_end"/>
    </connection-rule>
</connection-rules>
  1. SequenceFlow widgets can be used to connect between widgets having roles "sequence_start" and "sequence_end"

  2. MessageFlow widgets can be used to connect between widgets having roles "messageflow_start" and "messageflow_end"

cardinality-rules

This rule can be applied to widgets to specify number of incoming and outgoing connector rules based on roles (Used Only in Process Custom Palettes).

<cardinality-rules>
     <cardinality-rule role="Startevents_all">
        <incoming role="SequenceFlow" maximum="0"/>
    </cardinality-rule>
    <cardinality-rule role="Endevents_all">
        <outgoing role="SequenceFlow" maximum="0"/>
    </cardinality-rule>
</cardinality-rules>
  1. Widgets having "Startevents_all" as role cannot have incoming sequence flows.

  2. Widgets having "Endevents_all" as role cannot have outgoing sequence flows.

Defining attribute groups in a custom palette

edoras vis supports defining custom attribute groups, which can be used to assign a set of attributes to multiple widgets. .Attributes supported by an attribute-group element

Name

Mandatory

Description

name

true

Unique name of the attribute group

An attribute-groups element can contain one or more attribute-group elements. Similarly an attribute-group element can contain one or more attribute elements, as shown in the sample below.

<attribute-groups>
    <attribute-group id="commonAttributes">
        <attribute id="id" title="Id" value=""/>
        <attribute id="nameKey" title="NameKey" description="NameKey" type="SimpleText" value="" />
    </attribute-group>
    <attribute-group id="formControlAttributes">
        <attribute id="label" runtime="false" multilanguage="false" category="common"/>
        <attribute id="description" title="Description" type="SimpleText" visible="false"/>
    </attribute-group>
</attribute-groups>
Defining custom attributes in custom palette

edoras vis supports defining custom attributes.

Table 89. Attributes supported by the custom-attributes-group element
Name Mandatory Description

id

true

Id of the custom-attributes-group.

ref

false

Id of the widget to which the custom attributes are applied to. If this attribute is skipped then the attributes are applied to all the widgets.

A custom-attributes element can contain one or more custom-attributes-group elements. Similarly, a custom-attributes-group element can contain one or more attribute elements, as shown in the sample below.

<custom-attributes>
    <custom-attributes-group id="customAttributeGroup1">
        <attribute id="id" title="Id" value=""/>
        <attribute id="nameKey" title="NameKey" description="NameKey" type="SimpleText" value="" />
    </custom-attributes-group>
    <custom-attributes-group id="customAttributeGroup2" ref="Task">
        <attribute id="label" runtime="false" multilanguage="false" category="common"/>
        <attribute id="description" title="Description" type="SimpleText" visible="false"/>
    </custom-attributes-group>
</custom-attributes>
Defining model attributes in a custom palette

edoras vis supports defining model attributes in a custom palette.

To add model attributes a model-attributes element has to be added.

A model-attributes element can contain one or more attribute elements. All attributes which are defined within the model-attributes element will be added to the model. See the sample below.

<model-attributes id="modelAttributes" >
    <attribute id="expressionid" title="Label Expresion" value=""/>
    <attribute id="guid" title="GUID" description="GUID of the model" type="SimpleText" value="" />
</model-attributes>

7.7.3. Special attributes of attribute element in palette

ref-to-view

ref-to-view specifies an id of SVG element in the graphical representation of a widget. If this attribute is set, the property will manipulate the graphical representation at run-time, for example, changing the color or rendering text. Depending on the property’s type you can reference different types of SVG elements as show below.

  • ref-to-view="text_name" (for text)

  • ref-to-view="fill_el" (for color)

<attribute id="name"   value="" category="common"  type="SimpleText" ref-to-view="text_name"/>
filter

The filter attribute is applicable only for TreeView type and is used to filter the nodes displayed in the tree view.

edoras vis supports following filters :

  • mod is used for Processes

  • xfm is used for Forms

<attribute id="form_entry" title="FormRef" type="TreeView" value="" description="Form referenced by Form Task." filter="xfm" optional="false" readonly="true" />
fill

fill is an optional attribute applicable only for Color type. If fill attribute is set to true the background color of a shape can be set.

fill
multilanguage

multilanguage is an optional attribute used to show or hide the language specific attributes for a given attribute in the property window.

multilanguage
runtime and runtime-value

runtime is an optional attribute used to show or hide runtime-value of an attribute in the property window of the editor.

<attribute id ="description" runtime-value="" runtime="true" multilanguage="true"> </attribute>
runtime
wrapLines

wrapLines is an optional attribute used to customize attributes of type String.

If wrapLines is set to false, the text field shown will be of single line.

If wrapLines is set to true, the text field shown will be of multi line.

items

items can be used as a child element of the attribute element of type ComboBox. This is used to define the list of values for the combobox.

<attribute id="behavior" type="ComboBox" category= "edoras"  index="3" title="behavior">
    <items>
        <item id="none" title="none" value="none" />
        <item id="all" title="all" value="all" />
        <item id="one" title="one" value="one" />
        <item id="complex" title="complex" value="complex" />
    </items>
</attribute>
complex-items

complex-items can be used as child element of attribute element if the attribute is of one of the following types:

  • Complex

  • ComplexForm

complex-items element can contain one or more complex-item elements.

Table 90. Attributes supported by the complex-item element
Name Mandatory Description

id

true

Id of the complex-item

name

false

Title of the complex-item

value

false

Value of the complex-item

type

false

Type of the complex-item, It can only support following types:

  • SimpleText

  • TextArea

  • ComboBox

  • RestComboBox

  • Boolean

  • Integer

  • Float

  • Date

  • ComplexDate

  • Color

  • Link

  • TextEditor

  • RichText

  • TreeView

value

false

Used for validating values, Following are the supported vtype values:

  • singleEmail

  • emailList

  • expressionOrEmail

  • expressionOrEmailList

width

false

Width of the complex-item

optional

false

Used to make the complex-item optional or otherwise

<attribute id="email" title="Email Properties" type="ComplexForm" description="Email properties" value="">
    <complex-items>
        <complex-item id="from" name="From" type="SimpleText" value="" width="250" vtype="expressionOrEmail" optional="false" />
        <complex-item id="to" name="To" type="SimpleText" value="" width="250" vtype="expressionOrEmailList" optional="false" />
        <complex-item id="cc" name="Cc" type="SimpleText" value="" width="250" vtype="expressionOrEmailList" />
        <complex-item id="bcc" name="Bcc" type="SimpleText" value="" width="250" vtype="expressionOrEmailList" />
        <complex-item id="subject" name="Subject" type="SimpleText" value="" width="250" />
        <complex-item id="html" name="Html" type="RichText" value="" width="250" />
    </complex-items>
</attribute>
dependency

dependency can be used as a child element of the attribute element. This is used to make attribute visible/hide based on another attribute value.

<attribute id="thumbnail-max-height" type="SimpleText" title="Thumbnail Maximum Height" value="" >
    <dependency ref="preview" value="thumbnail" />
</attribute>

7.7.4. SVG view features

oryx:magnet

With oryx:magnet you can define special points on a node where you can dock other nodes or edges to connect them.

You can connect a docker to any point on a node, but magnets help the user creating nicer looking models. If you do not define a magnet for a node, it will a have default magnet in the center. Magnets are specified with 'magnet' elements, for example:

<oryx:magnets>
    <oryx:magnet oryx:cx="1" oryx:cy="20" oryx:anchors="left" />
    <oryx:magnet oryx:cx="1" oryx:cy="40" oryx:anchors="left" />
    <oryx:magnet oryx:cx="1" oryx:cy="60" oryx:anchors="left" />

    <oryx:magnet oryx:cx="25" oryx:cy="79" oryx:anchors="bottom" />
    <oryx:magnet oryx:cx="50" oryx:cy="79" oryx:anchors="bottom" />
    <oryx:magnet oryx:cx="75" oryx:cy="79" oryx:anchors="bottom" />

    <oryx:magnet oryx:cx="99" oryx:cy="20" oryx:anchors="right" />
    <oryx:magnet oryx:cx="99" oryx:cy="40" oryx:anchors="right" />
    <oryx:magnet oryx:cx="99" oryx:cy="60" oryx:anchors="right" />

    <oryx:magnet oryx:cx="25" oryx:cy="1" oryx:anchors="top" />
    <oryx:magnet oryx:cx="50" oryx:cy="1" oryx:anchors="top" />
    <oryx:magnet oryx:cx="75" oryx:cy="1" oryx:anchors="top" />

    <oryx:magnet oryx:cx="50" oryx:cy="40" oryx:default="yes" />
  </oryx:magnets>

The oryx:magnets element is a direct child of the svg element.

magnet

The magnets are shown as small light red circles light_magnet

oryx:docker

dockers are the other part of the dockers-magnets concept. A docker is a control object to connect an edge to a node or in this case to connect two nodes. A node can have at most one docker that can be defined by using a docker element:

<oryx:docker oryx:cx="16" oryx:cy="16" />
docker

A docker is shown as a small green circle green_docker

A docker element needs only position information. Docking nodes on nodes can be used as a shortcut for connecting two nodes with an edge. In this figure, the rectangular shape is the source and the circular shape is the target.

Text

Text is rendered with SVG text elements.

<text
       font-size="12"
       id="text_name"
       x="50"
       y="40"
       oryx:align="middle center"
       oryx:fittoelem="text_frame"
       stroke="black">
</text>
text

Oryx extends these element with attributes for the alignment and the rotation of the text. The alignment uses the specified coordinates (attributes x and y) as the reference point. A value of middle center means that the horizontal center and vertical middle point of the text will be positioned on the reference point.

Note
If you want to set the text element’s value using a property, you have to set the id of the text element by using ref-to-view attribute in custom palette.
<attribute id="name"   value="" category="common"  type="RichText" ref-to-view="text_name"/>
oryx:minimumSize
  • Value: float float

  • Initial: 1 1

  • Optional: yes

  • Applies to: SVG elements

minimumSize defines the minimum size the node can be resized to, if the node is resizable. The first value defines the minimum width, the second the minimum height.

<g pointer-events="fill" oryx:minimumSize="200 80" >
oryx:maximumSize
  • Value: float float

  • Initial: -

  • Optional: yes

  • Applies to: SVG elements

maximumSize defines the maximum size the node can be resized to, if the node is resizable. The first value defines the maximum width, the second the maximum height.

<g pointer-events="fill" oryx:minimumSize="80 60" oryx:maximumSize="200 160" >

7.7.5. Removing widgets from the main palette using fragment/patch palettes

edoras vis supports defining model attributes in a custom palette.

This feature can be used in cases where some of the default widgets provided in the default palettes should not been shown (since similar custom widgets have been added). To remove certain widgets, add the widget IDs as a comma separated list to the stencil-ids attribute of the <remove-stencils> element in the patch/fragment palette as shown in the below sample:

    <remove-stencils
            stencil-ids="cloud-attachment,cloud-upload"/>

Note this only works for elements contained in the main palette to which the patch palette is applied to.

7.7.6. Sample custom palette

A sample custom palette is given below:

<?xml version="1.0" encoding="UTF-8"?>
<palette id="custom-process-palette"
     xmlns="http://www.edorasware.com/schema/vis/process-palette"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="http://www.edorasware.com/schema/vis/process-palette
                         http://www.edorasware.com/schema/vis/edoras-vis-process-palette-2.0.43.xsd"
     resource-bundle="translation">

    <attribute-groups>
        <attribute-group id="baseAttributes">
            <attribute id="id" value="" title="" />
            <attribute id="name" value="" title="Name"
                       multilanguage="false" category="common" ref-to-view="text_name"/>
            <attribute id="documentation" value="" title="Documentation"
                       multilanguage="false" category="common"/>
        </attribute-group>

        <attribute-group id="usertask">
            <attribute id="notesShort" title="" value="" />
            <attribute id="notesShortKey" title="" value="" />
            <attribute id="candidateusers" title="CandidateUsers" value=""
                       category="edoras" type="RestComboBox" url="/rest/modeler-users"/>
            <attribute id="candidategroups" title="CandidateGroups" value=""
                       category="edoras" type="RestComboBox" url="/rest/modeler-groups"/>
            <attribute id="support" title="Support Link" type="Link"
                       value="http://www.edorasware.com/support" />
        </attribute-group>
    </attribute-groups>

    <model-attributes id="modelAttributes">
        <attribute id="guid" value="" title="" />
        <attribute id="labelExpression" value="" title="" />
        <attribute id="version" value="" title="" />
        <attribute id="author" value="" title="" />
        <attribute id="language" value="" title="" visible="false"/>
        <attribute id="namespaces" value="" title="" visible="false"/>
    </model-attributes>

    <group title="Custom Tasks" id="custom-shapes">
        <component id="CustomFormTask" extends="FormTask"
                   attribute-groups="baseAttributes,usertask"/>
        <component id="CustomManualTask" extends="ManualTask"
                   attribute-groups="baseAttributes,usertask"/>
        <component id="CustomEndNoneEvent" extends="EndNoneEvent"
                   attribute-groups="baseAttributes"/>
        <component id="CustomSequenceFlow" extends="SequenceFlow"
                   attribute-groups="baseAttributes"/>
        <component id="CustomTextAnnotation" extends="TextAnnotation"
                   attribute-groups="baseAttributes"/>
        <component id="CustomAssociationUndirected" extends="Association_Undirected"
                   attribute-groups="baseAttributes"/>
        <component id="CustomExclusiveDatabasedGateway" extends="Exclusive_Databased_Gateway"
                   attribute-groups="baseAttributes"/>
        <component id="CustomParallelGateway" extends="ParallelGateway"
                   attribute-groups="baseAttributes"/>
    </group>

    <group title="Activities" id="activities">
        <component ref="Task" id="RefTask"/>
        <component ref="Subprocess" id="RefSubprocess"/>
        <component id="RefFormTask" extends="FormTask">
            <attribute id="formInit" value="" title="Init Form Ref"
                   type="TreeView" filter="xfm" category="common" export="true"/>
            <attribute id="guid-ref" value="" title="Init Form Ref (ID)"
                   type="SimpleText" category="common" export="true" visible="false"/>
        </component>
        <component ref="CollapsedSubprocess" id="RefCollapsedSubprocess"/>
        <component ref="ExpandedSubProcess" id="RefExpandedSubProcess"/>
        <component ref="CallActivity" id="RefCallActivity"/>
    </group>

    <group title="Gateways" id="gateways">
        <component ref="Exclusive_Databased_Gateway" id="RefExclusive_Databased_Gateway"/>
        <component ref="ParallelGateway" id="RefParallelGateway"/>
    </group>

    <group title="Events" id="events">
        <component ref="StartNoneEvent" id="StartNoneEvent"/>
        <component ref="EndNoneEvent" id="EndNoneEvent"/>
    </group>

    <group title="Connectors" id="connectors">
        <component ref="SequenceFlow" id="RefSequenceFlow"/>
        <component ref="Association_Undirected" id="RefAssociation_Undirected"/>
        <component ref="Association_Unidirectional" id="RefAssociation_Unidirectional"/>
        <component ref="MessageFlow" id="RefMessageFlow"/>
        <component ref="Association_Bidirectional" id="RefAssociation_Bidirectional"/>
    </group>

</palette>

7.8. Defining App development workflows

7.8.1. App development overview

edoras one supports configurable app development workflows, allowing automatic or semi-automatic exchange of Apps between different edoras one installations and tenants.

To transfer an App out of an edoras one tenant, the App content is written to the local filesystem using an outgoing channel adapter. The adapter can then send a message to a Spring Integration channel to trigger additional processing (e.g. to copy the App to another system).

Spring Integration can also be used to receive incoming Apps and send them to the Incoming App Service. This documentation will describe the interface between edoras one and Spring Integration. For details of how to configure Spring Integration to implement a given App distribution workflow please refer to the {spring-int-page}.

Here is a high level overview of the App workflow processing:

AppWorkflow

7.8.2. Outgoing adapter configuration

An outgoing adapter can be declared as a Spring bean which implements the interface:

com.edorasware.cloud.core.transfer.OutgoingAppChannelAdapter

edoras one provides the following standard channel adapter implementations:

com.edorasware.cloud.core.transfer.internal.LocalDirectoryOutgoingAppChannelAdapter

writes the App to the local filesystem as a file tree within a root directory.

com.edorasware.cloud.core.transfer.internal.LocalZipFileOutgoingAppChannelAdapter

writes the App to the local filesystem as a ZIP file.

In all channel adapter implementations the App will be written using an ID that is constant across all systems where the App is installed.

The outgoing channel adapter should be declared as a bean in the installation-specific Spring configuration. In addition to the required bean constructor parameters, the following bean properties are supported:

Table 91. Supported outgoing channel bean properties
Property Description

baseDirectory

the App storage base directory

name

a descriptive channel name

outputChannel

a Spring Integration channel to receive outgoing App notification messages

useTenantSubdirectories

store each App in a subdirectory according to the originating tenant

To send Apps to selected output channels automatically whenever an App is deployed, define a list with id deployedAppChannelAdapters that includes references to the required output channel adapters. When an App is deployed it will be sent to all of the defined adapters without additional user intervention.

As an example, a typical outgoing channel adapter configuration for deployed Apps could look like the following:

  <util:list id="deployedAppChannelAdapters"
             value-type="com.edorasware.cloud.core.transfer.OutgoingAppChannelAdapter">
      <ref bean="outgoingAppChannelAdapter"/>
  </util:list>
  
  <bean id="outgoingAppChannelAdapter"
        class="com.edorasware.cloud.core.transfer.internal.LocalZipFileOutgoingAppChannelAdapter">
      <constructor-arg name="name" value="Local Directory"/>
      <constructor-arg name="baseDirectory" value="${transfer.outgoing.location}"/>
      <property name="useTenantSubdirectories" value="true"/>
      <property name="outputChannel" ref="loggingChannel"/>
  </bean>
  
  <int:channel id="loggingChannel"/>
  
  <int:logging-channel-adapter channel="loggingChannel" log-full-message="true"/>

This configuration will save each deployed App as a Zip file in the given directory, and then send a message to the logging channel (where the full message content will be logged).

7.8.3. Outgoing adapter message format

By setting the outputChannel property outgoing channel adapter implementations can send a message to a Spring Integration channel whenever an App is saved. These messages can be used to trigger the additional processing needed for App distribution, and have the following format:

  • the message payload is a org.springframework.core.io.Resource describing the App location

  • the message has additional headers with information that may be useful in subsequent processing

Table 92. Outgoing Spring Integration message headers
Header Type Description

appName

String

the current App name (this may change over time)

appSourceId

String

the App source ID (constant across all systems)

comment

String

a comment supplied when the App distribution was triggered

sourceTenantName

String

the name of the originating tenant

7.8.4. Incoming adapter configuration

The incoming Spring Integration configuration can import an App by sending a message to the pre-defined App channel:

resourceAppChannel accepts messages with org.springframework.core.io.Resource payloads describing the App location. This channel supports:

  • remote App Zip resources

  • local App Zip files

  • local App file trees

Per default an incoming adapter configuration (like in the example below) is already set and you just need to set the apps.preinstalled.location system property to the correct path where the apps are located. If you want to override this adapter then follow this example:

An incoming channel adapter configuration to automatically load pre-installed apps from a given input directory could look like the following:

  <!-- Update pre-installed Apps in all tenants when the App is modified -->
  <int:resource-inbound-channel-adapter id="incomingAppChannelAdapter"
                                        channel="resourceArrayAppChannel"
                                        auto-startup="false"
      pattern="${apps.preinstalled.location}">
  
      <!-- classpath items won't change so reduce polling to a minimum -->
      <int:poller fixed-delay="86400" time-unit="SECONDS" />
  </int:resource-inbound-channel-adapter>
  
  <int:channel id="resourceArrayAppChannel" datatype="org.springframework.core.io.Resource[]"/>
  
  <int:splitter input-channel="resourceArrayAppChannel" output-channel="resourceAppChannel"/>

This configuration polls the given directory, filters the results to leave modified App Zip files and passes these on to the fileAppChannel. The messages with a `File payload are then converted to a corresponding `Resource* messages and passed onto the pre-defined resource channel to trigger the import processing.

7.8.5. Incoming App service message format

The incoming App service accepts messages from a Spring Integration channel as follows:

  • the message payload is a org.springframework.core.io.Resource describing the App location

  • the message has additional headers with information to control the App import process

Table 93. Incoming Spring Integration message headers
Header Type Default Description

isSystemApp

Boolean

false

Set to true if the incoming app is the system app

isEditableApp

Boolean

true

Set to true if the app should be editable after import

removeAfterImport

Boolean

false

Set to true if the incoming app should be removed after import

forceAppImport

Boolean

false

Set to true if the incoming app should always be installed (no version checking will be performed)

tenantNames

String

<empty>

A comma-separated list of tenant names for the tenants where the app should be installed. If no tenant is specified then the app will be installed on all tenants.

7.8.6. Incoming adapter lifecycle

Unfortunately, although the standard Spring Integration channel adapters will be started automatically as part of the Spring lifecycle, the lifecycle phase cannot be modified and is not compatible with the edoras one lifecycle phases. This means that by default some channel adapters may be started before edoras one is ready to receive Apps.

As a workaround for this problem, the channel adapter auto-startup property can be set to false, and the bean reference added to the managedChannels list of the incomingChannelManager bean:

  <bean id="incomingChannelManager"
        class="com.edorasware.cloud.core.transfer.IncomingAppChannelAdapterLifecycleBean">
      <property name="managedChannels">
          <list>`
              <ref bean="systemAppAdapter"/>
              <ref bean="incomingAppChannelAdapter"/>
          </list>
      </property>
  </bean>
Note
The systemAppAdapter reference is a default adapter provided by edoras one to update the system App to the latest version whenever the server is restarted. This should always be included when overwriting the incomingChannelManager bean definition.

7.8.7. App version checking

When an App is imported into edoras one, a version check will normally be performed. This makes sure that the currently installed version is a direct ancestor of the version that is being imported. If this is not the case then the App being imported is based on a different version of the App than the one that is installed. This means that changes currently installed on the system may be lost.

This version check may be disabled using the incoming message headers, but this should only be done when the scenarios where this may be necessary have been established and the possible consequences of silently ignoring version conflicts have been considered.

7.8.8. Spring Integration extensions

The following implementations are provided by edoras one and can be used in Spring Integration configurations to support some common integration use cases:

com.edorasware.cloud.core.transfer.integration.ModifiedFilesFilter

a FileListFilter implementation that tracks the incoming files and accepts files that are either new or have been modified. This is useful for polling an incoming App directory where the Apps aren’t removed after import.

com.edorasware.cloud.core.transfer.integration.FileToResourceTransformer

transforms messages with a java.io.File payload into a message with a corresponding springframework.core.io.Resource payload, preserving all message headers.

7.9. Deployment

7.9.1. Deploying to Cloud Foundry

This appendix gives step by step instructions on how to deploy your application to a Cloud Foundry server. Cloud Foundry is an open Platform as a Service (PaaS) infrastructure.

Maven support

Cloud Foundry provides a Maven plugin that allows you to deploy applications with Maven goals. This appendix only gives basic instructions, please refer to http://docs.cloudfoundry.org/buildpacks/java/build-tool-int.html to learn more about Maven support for Cloud Foundry.

Install Maven plugin

To install the Cloud Foundry Maven plugin, add the corresponding section to your pom.xml file:

Cloud Foundry plugin installation in pom.xml
  <plugin>
      <groupId>org.cloudfoundry</groupId>
      <artifactId>cf-maven-plugin</artifactId>
      <version>1.0.3</version>
  </plugin>

Cloud Foundry does not have a persistent file system (the file system is cleared before an application restart), therefore it is not possible to persistently store work item and document content data in the file system. In this environment the application uses Cloud Foundry services to persistently store data.

Usually the application loads edorasware license from the file system (e.g. from the user’s home directory). Again, this approach is not possible with Cloud Foundry as there is no persistent file system, so we supply the edorasware license as an environment variable.

Configure Maven plugin

The configuration section of the plugin defines the default values used for the Cloud Foundry goals. It is possible to overwrite the default values with system variables.

server

Used to look up the Cloud Foundry credentials in the Maven settings.xml file. By default the settings.xml file is stored in ~/.m2/settings.xml. See http://maven.apache.org/settings.html#Servers to learn more about the Maven settings.xml file.

target

Defines the URL of your Cloud Foundry infrastructure API.

org

Defines your organization inside the Cloud Foundry infrastructure.

space

Defines the space to be used inside your organization.

env

Defines the environment variables needed by edoras one. The only environment variable we define here is the edorasware license.

services

Defines the Cloud Foundry services needed by edoras one: a relational database named datadb that stores the work items and a mongo DB named contentdb that stores the document content.

Putting all this together, the Cloud Foundry plugin section looks as follows:

Cloud Foundry plugin configuration in pom.xml
  <plugin>
      <groupId>org.cloudfoundry</groupId>
      <artifactId>cf-maven-plugin</artifactId>
      <version>1.0.3</version>
      <configuration>
          <server>${cloudfoundry.server}</server>
          <target>${cloudfoundry.target}</target>
          <org>${cloudfoundry.org}</org>
          <space>${cloudfoundry.space}</space>
          <memory>1024</memory>
          <env>
              <EDORASWARE_LICENSE>${edorasware.license}</EDORASWARE_LICENSE>
          </env>
          <services>
              <service>
                  <name>datadb</name>
                  <label>${cloudfoundry.service.datadb.label}</label>
                  <plan>${cloudfoundry.service.datadb.plan}</plan>
              </service>
              <service>
                  <name>contentdb</name>
                  <label>${cloudfoundry.service.contentdb.label}</label>
                  <plan>${cloudfoundry.service.contentdb.plan}</plan>
              </service>
          </services>
      </configuration>
  </plugin>

To ease the handling of the Cloud Foundry plugin configuration in the pom.xml file you can use Maven properties. The property values are then stored in two places: properties that are not security critical are defined inside a properties element at the top of the pom.xml file, security critical properties are defined in the Maven settings.xml file.

Properties in pom.xml
  <!-- Cloud Foundry properties -->
  <cloudfoundry.server>run.pivotal.io</cloudfoundry.server>
  <cloudfoundry.target>http://api.run.pivotal.io</cloudfoundry.target>
  <cloudfoundry.org>edorasware</cloudfoundry.org>
  <cloudfoundry.space>development</cloudfoundry.space>
  
  <cloudfoundry.service.datadb.label>elephantsql</cloudfoundry.service.datadb.label>
  <cloudfoundry.service.datadb.plan>turtle</cloudfoundry.service.datadb.plan>
  
  <cloudfoundry.service.contentdb.label>mongolab</cloudfoundry.service.contentdb.label>
  <cloudfoundry.service.contentdb.plan>sandbox</cloudfoundry.service.contentdb.plan>

Make sure that the server id in the Maven settings.xml file matches the server configuration value of Cloud Foundry plugin (run.pivotal.io in our case).

Settings in Maven settings.xml
  <servers>
      <server>
          <id>run.pivotal.io</id>
          <username>user</username>
          <password>pass</password>
      </server>
  </servers>
Use Maven plugin

To access your Cloud Foundry infrastructure, the Cloud Foundry Maven plugin offers different Maven goals. The most important is the one to deploy your application:

mvn cf:push

Congratulations! You have deployed your first edoras one application to a Cloud Foundry infrastructure.

Command line support

The recommended way to work with Cloud Foundry is to use the Maven integration (see above) or the Gradle integration. However for special cases it is possible to use the Cloud Foundry command line tool. One such special case is when your Cloud Foundry infrastructure does not support recent versions of the Maven / Gradle integration plugins.

Note

It is not possible to define environment variables that contain line feeds with the Cloud Foundry command line tool. Therefore you cannot use an environment variable to supply the edorasware license to the Cloud Foundry infrastructure.

In this case you have to place the edorasware license file inside your application and access it from there, e.g. in the WEB-INF folder.

First make sure to install the correct version of the Cloud Foundry command line tool that matches your Cloud Foundry infrastructure. Then open a command line and invoke the Cloud Foundry commands as required. The following batch file lists the commands needed to deploy an edoras one application:

Batch file
  REM stop application
  cf stop edoras-one-bootstrap
  
  REM deploy application without start
  cf push edoras-one-bootstrap ^
     -d beta.swisscloud.io ^
     -m 1G ^
     -p target\edoras-one-bootstrap.war ^
     -t 150 ^
     --no-start
  
  REM create and bind relational database service
  cf create-service mariadb 512mb datadb
  cf bind-service edoras-one-bootstrap datadb
  
  REM create and bind document database service
  cf create-service mongodb default contentdb
  cf bind-service edoras-one-bootstrap contentdb
  
  REM start application
  cf start edoras-one-bootstrap

See http://docs.cloudfoundry.org/devguide/#cf to learn more about the Cloud Foundry command line tool.

7.9.2. How to integrate edoras one in your own html

It’s possible to use edoras one in your own html inside a div.

First of all, you should know some limitations:

  • edoras one app must be the unique angularJS app in your html.

  • The div which contains edoras one requires a defined height, that is, not auto, with percentage or pixels.

  • The minimum size supported in edoras one is 1024x768. That means that the div which contains edoras one must fit this measure.

The following html code must be inserted in your page:

<div ng-app="oneModule" class="edoras-one" >
    <link type="text/css" media="all" href="./one/less/main-1.5.0.S70.less" rel="stylesheet/less"/>
    <script type="text/javascript" src="./forms/app/fms.files-1.5.0.S70.js"></script>
    <script type="text/javascript" src="./one/app/one.files-1.5.0.S70.js"></script>
    <script type="text/javascript" src="./forms/css/resources.css"></script>
    <script type="text/javascript" src="./one/css/resources.css"></script>
    <link rel="stylesheet" href="./forms/libs/upload/css/bootstrap.min.css" />
    <link rel="stylesheet" href="./forms/libs/upload/css/external/bootstrap-image-gallery.min.css" />
    <link rel="stylesheet" href="./forms/libs/upload/css/jquery.fileupload-ui.css" />
    <script type="text/javascript" src="./forms/libs/less/less-1.7.5.min.js"></script>
    <div e-application-loader>
        <div id="mainDiv">
            <div e-menu></div>
            <div e-header></div>
            <div class="viewContainer">
                <div class="ng-view"></div>
                <div e-actions-pane></div>
            </div>
            <div e-version ></div>
        </div>
        <div e-notification-bar></div>
        <div e-select-dashboard-pane ></div>
        <div e-global-message></div>
    </div>
</div>

You must have the production files in the correct directory structure with "./one" and "./forms" directories and the backend endpoints.

If you want and you are able to do, you can define these css and scripts tags in your head element instead of the body, in order to be loaded synchronously before the page is rendered.

Customizing edoras one elements

You can avoid showing some elements like the menu, the header or the actions pane by simply deleting it from the html above. These elements are:

<div e-menu></div>
<div e-header></div>
<div e-actions-pane></div>
Placing our html in a different place than the resources

In the html we can set a variable APP_CONTEXT to specify where the resources are.

For example if we have our resources in http://domain.com/resources but our html is in http://domain.com/web, we can set the APP_CONTEXT to "../resources/":

<script>window.APP_CONTEXT = '../resources/';</script>

This script tag must be placed before the fms.files.js script tag:

<script>window.APP_CONTEXT = '../resources/';</script>
<script type="text/javascript" src="./forms/app/fms.files-1.5.0.S70.js"></script>

We can set the APP_CONTEXT with relative or absolute values:

<script>window.APP_CONTEXT = '../resources/';</script>
<script>window.APP_CONTEXT = '/resources/';</script>
<script>window.APP_CONTEXT = 'http://domain.com/resources';</script>
Note
If you want to set a context of an external domain you can experience problems with CORS browser security. Contact your administrator in that case.

7.9.3. Application logging

The bootstrap project should log correctly using application-specified logging without changes.

For JBoss application servers, an additional deployment configuration file is included in the bootstrap project to prevent logging-related classes from the application container being added to the application classpath and interfering with the application-specific logging:

JBoss deployment configuration WEB-INF/jboss-deployment-structure.xml
  <?xml version="1.0" encoding="UTF-8"?>
  <jboss-deployment-structure>
      <deployment>
      <exclusions>
            <module name="org.apache.log4j"/>
            <module name="org.slf4j"/>
            <module name="org.jboss.logging"/>
            <module name="commons-logging"/>
            <module name="org.apache.commons.logging"/>
            <module name="org.jboss.logging.jul-to-slf4j-stub"/>
      </exclusions>
      </deployment>
  </jboss-deployment-structure>

Application logging is configured as described in the section Logging configuration.

7.9.4. Container logging

The logging configuration shown in the bootstrap project uses log4j and a local logging configuration within the deployed WAR file. The resulting logs will typically be written to an application-specific log file, separate from any log files that may be used by the container in which the application is deployed.

An alternative logging approach is to delegate the logging to the container, allowing the logs from both the container and the application to be written to a single log file and controlled using a single logging configuration. The following sections describe the required configuration for this setup using some common application containers.

Tomcat container logging

The Tomcat application server uses a custom logging implementation for server logging that is not supported by an slf4j adapter. For this reason we cannot simply delegate the logging to the container in the default configuration. To use container logging, the server must be reconfigured to use log4j as described on the following web page:

When the server has been reconfigured in this way, you just need to remove the log4j library from the bootstrap project by excluding it explicitly when the slf4j-to-log4j adapter is included. The slf4j-to-log4j adapter will then find the log4j implementation from the Tomcat lib directory and use it for all edoras one logging. You will also need to remove the bootstrap project log4j.properties file, and transfer any required edoras one logging configuration to the container log4j.properties file.

If you have unit tests within the bootstrap project then you will also need to add a testing runtime dependency for the log4j library, as the unit tests will not have access to the log4j library in the Tomcat container.

Maven dependency configuration
Tomcat container logging configuration in the Maven pom.xml
  <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-log4j12</artifactId>
      <version>${org.slf4j.version}</version>
      <scope>runtime</scope>
      <exclusions>
          <exclusion>
              <groupId>log4j</groupId>
              <artifactId>log4j</artifactId>
          </exclusion>
      </exclusions>
  </dependency>
  
  <dependency>
      <groupId>log4j</groupId>
      <artifactId>log4j</artifactId>
      <version>1.2.17</version>
      <scope>test</scope>
  </dependency>
Gradle dependency configuration
Tomcat container logging configuration in the Gradle build.gradle
  // Route the logs to log4j ...
  runtime("org.slf4j:slf4j-log4j12:$slf4jVersion") {
    // ... where log4j itself is supplied by the Tomcat container
    exclude group: 'log4j', module: 'log4j'
  }
  
  // we still need a local log4j library for testing
  testRuntime "log4j:log4j:1.2.17"
JBoss container logging

The JBoss application server supports all of the logging frameworks used in edoras one, so to use JBoss container logging it is only necessary to do the following:

  • remove the logging dependencies

  • remove the julReroute bean definition from the application’s Spring configuration

  • remove the logging configuration file log4j.properties (or move it to the test source tree if it is still needed for unit testing)

  • remove the file WEB-INF/jboss-deployment-structure.xml

If the project contains unit tests, however, the logging dependencies will need to be retained and moved to the test scope as shown below.

Maven dependency configuration
JBoss container logging configuration in the Maven pom.xml
  <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>jul-to-slf4j</artifactId>
      <version>${org.slf4j.version}</version>
      <scope>test</scope>
  </dependency>
  <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>jcl-over-slf4j</artifactId>
      <version>${org.slf4j.version}</version>
      <scope>test</scope>
  </dependency>
  <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-log4j12</artifactId>
      <version>${org.slf4j.version}</version>
      <scope>test</scope>
  </dependency>
Gradle dependency configuration
JBoss container logging configuration in the Gradle build.gradle
  // Retain test dependencies for unit testing ...
  testRuntime "org.slf4j:jul-to-slf4j:$slf4jVersion"
  testRuntime "org.slf4j:jcl-over-slf4j:$slf4jVersion"
  testRuntime "org.slf4j:slf4j-log4j12:$slf4jVersion"

8. CMMN Documentation

8.1. Design & Implementation Notes

8.1.1. CMMN Specification Issues (and interpretations) 

During the course of implementing a working CMMN engine based on CMMN specification version 1.0, a number of specification issues (omissions, ambiguities) were identified.

ManualActivationRule

The item control is called ManualActivationRule. The symbol is modeled after a tape recorder play button. Having the symbol on a model element means that there is a manual activation rule configured for the element. By naming (and table 5.40 in the spec), a rule evaluating to 'true' means manual activation. Not specifying a rule should, according to the specification (figure 5.12) also evaluate to a value of 'true'. This leads to the situation where both of the following tasks have manual activation. This looks like a contradiction and is confusing for modelers as they, in order to configure automatic activation, must define a manual activation rule that resolves to false.

manualActivation

We decided to handle this differently. In the current implementation, a missing manual activation rule defaults to false, meaning automatic activation. Thus, the task on the left auto starts. Another advantage of this is that all missing plan item controls (manual activation, repetition, required) default to false in the case of a missing rule.

In addition, Automatic activation by default is, in our experience, the better choice for the case plan model itself (no symbol) and a better default (because more common) for stages, process tasks and case tasks.

Sentry Evaluation

The order and logic related to the evaluation of sentries of a CMMN model is not at all specified in CMMN specification version 1.0. Because of this, the runtime-evaluation of CMMN models in our implementation is likely to differ from other vendors. edoras one CMMN works as follows:

Triggering plan items

A plan item transitions from AVAILABLE to ACTIVE (or ENABLED) based on its entry sentry configuration and the transitions the sentry’s on-part items undergo (if any).

Plan items are created and made AVAILABLE as their parent container (Case Plan Model or Stage) starts. From that point on, a plan item will listen to a trigger event. Possible trigger events (conditions) are:

  • the plan item has no entry sentry. In this case it will immediately trigger (as part of the same event processing cycle that created it).

  • the plan item has at least one entry sentry that triggers (logical OR among multiple sentries).

An individual sentry triggers under the following conditions:

  • it has no condition (expression) and no on-parts.

  • it has a condition (expression) and no on-parts, and the condition evaluates to true.

  • it has one or more on-parts, the on-parts fire, and there is either no condition or a condition evaluating to true.

A sentry’s on-parts (one or more) fire under the following conditions:

  • if there is one on-part, it fires if the on-part item it listens to undergoes the desired transition as part of the current event processing cycle.

  • if there are more than one on-parts, the sentry fires if at least one of the on-parts fire and all other on-part items' last transition (possibly from an earlier event processing cycle) equal the one the on-part is configured for.

    In other words, multiple on-parts of a single sentry form a logical AND. In order for the sentry to trigger, at least one of the on-parts must trigger as part of the current event processing cycle and all (!) other on-part items must have undergone the desired transition as their last transition (but not necessarily during the same event processing cycle).

Repeated triggering of plan items

Plan items can be repeatable. The first instance triggers according to the rules defined above. Subsequent instances only trigger if one or more on-parts trigger. (There is an extension in place allowing to manually trigger repeatable plan items if they are configured for manual activation.) Repeatedly triggering on merely a condition (no on-part) is not supported.

Current implementation

Our current CMMN engine implementation behaves according the the rules described above, with the following exception:

  • The first instance of a plan item with one or more on-parts may trigger merely based on a sentry condition turning to true, provided the on-parts are satisfied.

For example: We have a plan item with a sentry holding a condition and an on-part. The condition is false. At some point, the on-part item undergoes the desired transition. Now, during some later event processing cycle, the condition switches to true. The plan item triggers.

For simplicity and predictability we are currently considering to disable this exception. There is also a forum topic on this https://forum.edorasware.com/t/cmmn-sentry-evaluation-semantics/89

Implementing the RepetitionRule

The specification is not clear on how to handle the repetition rule in combination with sentry AND-conditions (multiple on-parts creating an AND condition an a sentry). Our implementation is based on an algorithm that distinguishes between current and past event transitions. See also "Repeated triggering of plan items" in the previous section.

On-Parts and Repetition (Multiplicity)

The specification is not clear on how to handle plan item or case file item on-parts that refer to potentially multiple runtime instances. Our implementation is based on an "at least one" approach.

The sentryRef Dilemma

The following is a legal diagram fragment:

exitSentries

The modeler’s intention in this example is: The ExitSentry of Task 2 has a PlanItemOnPart referring to Task 1 having a sentryRef pointing to the ExitSentry of Task 1. The meaning: Task 1 exiting via the one ExitSentry triggers the exit of Task 2. The problem: just by looking at the diagram, it is not clear which ExitSentry holds the PlanItemOnPart. There should be an arrow or some other symbol indicating the direction of the connector (the PlanItemOnPart).

Parent Termination of Event Listeners and Milestones

The CMMN 1.0 XSD doesn’t include a PARENT_TERMINATE transition as depicted in table 7.9 of CMMN specification 1.0. The edoras one CMMN engine uses the EXIT transition instead.

Discretionary Items and Sentries

5.4.9.2 of the specification mentions entry and exit criteria with respect to discretionary items as supported.Figure 5.12 states that DiscretionaryItems are not associated with Sentries. Entry and exit criteria references are modeled in the XML (XSD) only on PlanItems. Not on DiscretionaryItems. The current implementation of the CMMN engine does not support sentries on Discretionary Items.

Parameters

Formal modeling of CMMN Parameters is not yet included in the current CMMN implementation. We left it out due to time constraints and due to the fact that the CMMN specification doesn’t support parameter mappings for HumanTasks.

As an alternative to the CMMN modeling framework for Parameters, a simple generic run-time parameter concept was implemented. This custom parameter concept does not interfere with the CMMN specification and can co-exist with any formal CMMN Parameters implementation that may follow in the future. It works as described in section Parameters on page CMMN Extensions.

8.1.2. CMMN Extensions 

Custom Attributes

An arbitrary map of custom String/String properties can be edited during design time and stored on select CMMN elements:

The map is serialized to JSON and stored under attribute customAttributes in the XML. The purpose of these properties is to configure certain CMMN extensions like repeatable user event listeners. Customer implementation extensions can also make use of these properties to implement custom logic based on arbitrary design-time properties.

The elements for which custom attributes are supported are:

  • Case (case plan model)

  • Stage

  • Process task

  • Service task

  • Human task

  • Case task

  • Milestone

  • User event listener

  • Event listener (external events)

  • Case file item (stored in 'caseFileItemDefinition' XML element)

Except for the case file item, the customAttribute is stored as part of the item’s PlanItemDefinition.

Manual Repetition

The CMMN repetiion rule allows to create multiple instances of a given model plan item. The triggering of a plan item (START, ENABLE or OCCUR) is controlled by sentries. Only a Sentry with on-parts that fire may lead to the creation and triggering of an additional instance of the plan item.

By introducing the Manual Repetition extension, it is possible to manually trigger the creation of a new instance of a plan item. For this to work, a plan item must be configured as follows:

  • The manual activation rule must resolve to true

  • The repetition rule must resolve to true

If this is the case, a plan item can repeatedly be created by means of an action button under Available Actions in the case HTML template.

The CMMN specification 1.0 states, that a plan item’s repetition expression is evaluated at the point of the plan item’s creation (transition to AVAILABLE) and maintained throughout the life cycle of the plan item. In the context of the Manual Repetition extension, this rule has been adjusted. The engine evaluates the rule on demand, i.e. each time a new plan item instance may potentially be created, the rule is evaluated.

Event Listeners and Stage Completion

In the edoras one CMMN implementation, event listeners can be configured to be "completion neutral". If checked, the event listener will not prevent a stage from completing, meaning that such an event listener is neutral with respect to stage completion evaluation. The rationale to introduce this option is as follows:

Following the specification, a stage with an event listener in state Available does not complete unless we set the stage’s auto-complete flag to true. In many modeling situations, this forces us to set a stage to auto-completion as soon as we use an event listener in the stage. By treating event listeners as neutral, as now possible by setting the Completion Neutral option on the event listener, we gain modeling flexibility in the sense that we can still use the auto-completion flag to model some other condition of a stage.

Repetition of User Events and External Events (User event listener, Event listener)

User events and external events support a custom property manualRepetition (simply named Repetition in the VIS properties editor). The value can be any back-end condition. If the condition evaluates to true, a new user event instance is created upon occurrence of the preceding one. This allows the modeler to create a user event button in the GUI which remains visible and can be invoked multiple times. For external event listeners this means that there is always an instance of the event listener available to receive an OCCURRED transition.

Cross-Stage PlanItemOnParts

There is no conceptual reason to disallow plan item on-parts referring to plan items outside the sentry’s stage (as CMMN spec 1.0 does). The current implementation of the CMMN engine allows such connections. For example, the CMMN engine evaluates the following model correctly:

crossStagePlanItemOnParts

Parameters

Every plan item which can be modeled with an entry sentry automatically receives a standardized input parameter map during run-time. The map contains key/value pairs of type String/String with entries representing the plan item(s) that triggered the current plan item:

  • A single entry in this map represents a single input parameter.

  • Each parameter represents a source plan item (or case file item), namely the item of each on-part that triggered the target plan item.

  • A map entry key holds the source plan item (or case file item) model ID.

  • A map entry value holds the item’s run-time client ID (edoras Work Item ID).

This map is available to developers of plan item extensions via the TransitionData parameter. In edoras one Work Items (Human task, process task and Case task Work Items), the map is available to modelers under variable name _triggerItems.

Example:

humanTasks

As task B-ID is triggered (started or enabled), it will receive the following input parameter map:

"_triggerItems" : {
  "<A-ID>" -> "<A-ID's client ID, i.e. Work Item ID>"
}

Current limitation: only the trigger items leading to the start/enable/occur of the target plan item are included in the _triggerItems map. If we have a sentry with multiple on-parts, only the on-parts whose transition triggers the enable/enable/occur event on the target plan item are included.

In addition, please note a possible ambiguity with respect to multiple sentries: Multiple sentries represent a logical OR. If multiple such ORed sentries trigger simultaneously, the exact sentry passed in "_triggerItems" is not defined.

8.2. Developer Documentation

8.2.1. Integrating Custom Extension Code 

There are two ways to call custom extension code from a running CMMN case:

  1. Invoke custom code by means of a service task (by evaluating a runtime expression).

  2. Provide low-level custom implementations for plan item transitions.

The following sections describe the two options in more detail:

Execute custom code by means of a Service Task

Service tasks are available in the palette and can be placed into the model like any task. By means of a runtime expression configured on the service task, arbitrary code can be executed as the runtime representation of the task starts. The result of the expression evaluation is stored in the case scope under the variable name provided by the modeler. Service tasks are non-blocking, meaning, as soon as they trigger, they evaluate the expression, store the result and complete.

Provide custom implementations for plan item transitions

This is a very flexible and powerful yet low-level way of integrating custom Java code into a case model. Using this approach, the complete edoras one implementation of the CMMN engine can be replaced with custom implementation code. Replacing or extending a select sub-set of the existing implementation classes is possible too. In order to introduce a custom transition handler for a given plan item, the following steps are necessary:

  1. Define a bean with name cmmnExtensions implementing interface CmmnExtensions or extending DefaultCmmnExtensions.

  2. In your CmmnExtensions class, implement the method of you choice to return your handler implementation. E.g. implement method getOccurExtension(..) to return your implementation of an OccurHandler.

  3. Your implementation of the handler must extend the appropriate abstract handler (e.g. the OccurHandler) and implement the handler method responding to the handler’s plan item transition. (The occur method for the OccurHandler - there is a consistent naming pattern for the handlers and their methods.)

It is possible to selectively return handlers. The methods in interface CmmnExtensions all have the same signature: they receive

  • the type of plan item for which to return the custom handler

  • an arbitrary string. The string is called extensionType and can be provided in your model as a custom property with name extensionType. (E.g. define the custom attribute (key/value pair) extensionType=MyExtension on your model element)

Based on these parameters you can decide which custom handler implementation (if any) you want to return. By returning null, the default edoras one handler implementation will be used.

If you decide to return a custom handler, you have the option to completely replace the default implementation or to invoke your handler code before or after the default implementation code. Check out method getExtensionPoint() in class BaseHandler. If you don’t override getExtensionPoint(), your code will replace the default implementation code.

Please refer to the table on page Standard CMMN State Transition Implementations in order to get an overview of the available default edoras one implementation handlers and their purpose.

8.2.2. Standard CMMN State Transition Implementations 

The edoras one CMMN engine implementation is split into two parts:

  • A generic CMMN engine implmenentation

  • Edoras one integration code

The edoras one integration code is implemented per PlanItem type and CMMN transition type. The following table outlines which particular integration methods are provided for the various plan item types and transitions. The table also shows the purpose of the available edoras one implementations.

The main goal of the table is to support edoras one customer code developers in the task of implementing CMMN extensions. The table shows which standard implementations are available. Based on the table, developers of extensions can decide which hooks they want to extend or override.

Empty cells in the table mean that there is no edoras one implementation needed. There is code executed for the cell (transition and plan item type) within the CMMN state model, however, there is no need to take an action on edoras one Work Items.

CasePlanModel Stage HumanTask ProcessTask CaseTask Milestone UserEventListener

create

Creates and starts a Case Work Item for the CMMN case represented by the CasePlanModel.

enable

manualStart

Starts the HumanTask (see 'start').

Starts the ProcessTask (see 'start').

Starts the CaseTask (see 'start').

start

Creates and starts the Task Work Item referenced by the HumanTask. The Task is based on an AdHoc Task Definition. AdHoc Task Definitions for HumanTasks are created during deployment of the CMMN Case model.

Creates and starts a Process Work Item.

Creates and starts a new CMMN StateModel for the Case referenced by the CaseTask. This leads to 'create' of the CaseTask’s CasePlanModel.

occur

complete

Completes the Case Work Item (archives the case).

Collects output parameters from the completed case task and stores them on the case.

exit

Sets the sub-state of the HumanTask work object to INTERRUPTED.

Cancels the process.

Terminates the case represented by the CaseTask.

fault

reactivate

terminate

Completes the Case Work Item.

Terminates the case represented by the CaseTask.

Case auto-completion handling

After having applied an external event to a StateModel (like after applying a state transition), a StateModel’s auto-completion is evaluated. If a StateModel completes due to auto-completion (or otherwise is considered exited/terminated), and if the StateModel represents a CaseTask, then a suitable transition (complete, exit or terminate) is triggered in the parent Case for the CaseTask that was just completed/exited/terminated.

8.2.3. REST API

Available CMMN REST Calls
Plan Item Transition
/rest/cmmn/CAS/{caseClientId}/{sourcePlanItemId}/{planItemTransition}/planItemTransition (method GET)

Trigger a transition on a plan item and subsequently re-evaluate the state model.

Plan Item Transition with Form Data
/rest/cmmn/CAS/{caseClientId}/{sourcePlanItemId}/{planItemTransition}/planItemTransitionWithFormData (method PUT)

Similar to the method above, but allows to pass form data in the request body. For example, this call is used to manually start a task with an init form.

Case File Item Transition
/rest/cmmn/CAS/{caseClientId}/{caseFileItemId}/{caseFileItemTransition}/caseFileItemTransition (method GET)

Trigger a transition on a case file item and subsequently re-evaluate the state model.

Case File Item Transition with Form Data
/rest/cmmn/CAS/{caseClientId}/{caseFileItemId}/{caseFileItemTransition}/caseFileItemTransitionWithFormData (method PUT)

Similar to the method above, but allows to pass form data in the request body. For example, this call is used to manually create a case file item of type edorasDataItem.

Discretionary Item Planning
/rest/cmmn/CAS/{caseClientId}/{sourcePlanItemId}/discretionaryItemPlanning (method GET)

Plan a discretionary item (move it from the planning table into the run-time model by creating a corresponding plan item in the run-time model).

State Update
/rest/cmmn/CAS/{caseClientId}/stateUpdate (method GET)

This re-evaluates the state model (entry criteria, exit criteria, applicability rules, (auto-)completion of stages). Calling this method makes sense when you have a state change in the back-end which is used in a back-end expression in your CMMN model and you want the back-end expression to be re-evaluated.

Current States
/rest/cmmn/CAS/{caseClientId}/currentStates (method GET)

Gets a JSON array of the model’s current states (milestones and stages).

Available Actions
/rest/cmmn/CAS/{caseClientId}/availableActions (method GET)

Gets a JSON array of the model’s available actions. Each entry in this array is an action. An example of such an action would be an entry to manually start a task in state ENABLED.

Description of REST Call Parameters
{caseClientId}

The edoras case Work Object ID.

{sourcePlanItemId}

The model element ID of the item. Append ~N (where N is the instance number) to address a particular plan item instance (e.g. of a repeatable plan item). If you pass a sourcePlanItemId without instance suffix, the engine will always find the instance with the highest index (i.e. the instance created most recently).

If there exists a work object for the plan item of interest, you can also pass the edoras Work Object ID.

{planItemTransition}

The transition to apply (the names of enum PlanItemTransition)

{caseFileItemId}

The model element ID of the case file item. Append ~N (where N is the instance number) to address a particular case file item instance. If there exists a work object for the case file item of interest, you can also pass the edoras Work Object ID.

{caseFileItemTransition}

The transition to apply (the names of enum CaseFileItemTransition). Currently only CREATE and UPDATE are supported.

8.2.4. CMMN Service

The standard edoras one Spring configuration contains a bean named "cmmnService". The bean exposes methods that allow client projects (like edoras one Bootstrap based projects) to invoke actions on CMMN case instances.

The TransitionData parameter in the method calls below can be used to pass init form data along with the call. Simply construct a new TransitionData object (using either the empty constructor or the one accepting a parameter map) and pass it along with your call.

Available CMMN Service Calls
Re-evaluate a case model
void handleStateUpdate(CaseId caseId);
Invoke a plan item transition on a CMMN case instance
void handlePlanItemTransition(CaseId caseId, String planItemId, PlanItemTransition planItemTransition, TransitionData transitionData);
Invoke a case file item transition on a CMMN case instance
void handleCaseFileItemTransition(CaseId caseId, String caseFileItemId, CaseFileItemTransition caseFileItemTransition, TransitionData transitionData);
Invoke a discretionary item planning action on a CMMN case instance
void handleDiscretionaryItemPlanning(CaseId caseId, String discretionaryItemId);
Get the list of current states (milestones and stages) for a CMMN case instance
List<CurrentState> getCurrentStates(CaseId caseId);
Get the list of available actions for a CMMN case instance
List<AvailableAction> getAvailableActions(CaseId caseId);

8.3. User Documentation

8.3.1. Current Feature Overview 

This page describes the scope of the initial edoras one CMMN engine implementation.

Important: When running CMMN cases, make sure the case template is selected (make sure your URL starts with /case.html#…​). If not, simply edit it in the browser’s address bar and reload the page.

Plan Items and Case File Items
Stages

A case can be structured by using stages. Stages can be nested. As of sprint 85, stages can be visuaized in the case template. See CMMN Stage and Milestone Visualization.

Plan fragments

Plan fragments can be put into a model. They allow to group individual plan items. Plan fragments do now have a run-time representation and no associated functionality other than to provide a means to enclose a bunch of plan items.

Process tasks

edoras one process tasks can be placed in a model and wired up using connectors and sentries. Input/output is handled via case variables only (see Parameters, below)

Service tasks

Service tasks can be placed into a model like any other task. When triggered, a service task evaluates a back-end expression and stores the evaluation result in the scope of the case under a variable name provided by the modeler. Service tasks are non-blocking. They immediately complete once the result is stored in the result variable.

Human tasks

Ad-hoc human tasks can be placed in a model and wired up using connectors and sentries. Input/output is handled via case variables only (see Parameters, below).

Human tasks have an init form and a work form. The work form is the main form. Where applicable, the init form is used (e.g. when starting a human task with manual activation from the case actions menu).

Case tasks

Case tasks can be placed in a model and wired up using connectors and sentries.

Case file items

Case file items can be placed in a model. Currently only the definitionType edorasDataItem is supported. These types of case file items are represented by their own run-time edoras work item. For these case file items, an init form and a work form can be defined. New case file items can be created via the Case Actions menu (in the case of case file items that have the necessary multiplicity property).

Milestones

Milestones can be placed in a model and configured via sentries. Reached milestones are visible in the case template. As of sprint 85, additional options for milestone visibility is available. See CMMN Stage and Milestone Visualization.

User events

Support for UserEvents. As of sprint 84, user event listeners can be configured to be invoked more than once. See See CMMN Extensions (Repetition of User Events).

TimerEvents

Support for TimerEvents is not yet implemented. Consider modeling timer events with the help of a Process task (edoras one BPMN Process).

Generic events

External system events can be modeled by means of a generic event listener symbol. Such events can be triggered by means of a REST call.

HTML template

The existing case.html template is supported. In particular the areas:

Active Work Items - For active human tasks and active work items from process tasks and case tasks.

Case File Items - For created case file items

Case Actions - For the starting of tasks with manual activation and for the creation of case file items (according to their multiplicity property)

Sentries and Events

Sentry If-conditions are supported (evaluated in the context of the case).

Connectors to tasks and stages are supported (plan item on-parts). Supported transitions:

  • CREATE

  • ENABLE

  • MANUAL_START

  • START

  • COMPLETE

  • EXIT

  • TERMINATE

Transitions DISABLE and RE-ENABLE are not yet supported. Neither are transitions SUSPEND, RESUME, PARENT-SUSPEND, PARENT-RESUME. CLOSE (on the level of the case) is not supported. Instead, once a case reaches COMPLETED or TERMINATED, it is archived (edoras one ARCHIVED state).

The events

  • FAULT

  • REACTIVATE,

are supported on the level of the REST API and custom transition implementations (extension code) for plan items (except the case itself). There is currently no GUI support or visual feed-back available for these transitions and resulting states.

The transition

  • OCCUR

is supported for Milestones and Event listeners:

Connectors to case file items are also supported (case file item on-parts). Supported transitions:

  • CREATE

  • UPDATE

Roles

Support for the formal modelling of CMMN roles is not implemented. However, support for access control based on the concept of edoras one "candidate users" and "candidate groups" is provided for Human tasks and the control of the visibility of action buttons (available actions).

Parameters

The configuration of formal CMMN parameters is not supported. Information can be passed via case variables. In addition to that, we implemented a type of implicit input parameters called "trigger items". See CMMN Extensions.

Discretionary Items and planning

Discretionary items planning has been partially implemented. However, support in the modeler and the Case Template (case.html) has been removed due to the limited value of the current implementation. The whole planning aspect will be re-addressed at a future point in time.

PlanItem properties

Repetition - the Repetition property is supported, including the dynamic evaluation of an expression to determine the property value. If repetition == true, new plan item instances are created depending on entry sentry evaluation.

Manual activation - the ManualActivation property is supported, including the dynamic evaluation of an expression to determine the property value. For manual activation items, menu entries are made available in the Case Actions menu for the starting of these items.

Required - the Required property is supported, including the dynamic evaluation of an expression to determine the property value. The Required property is relevant for the determination of a Stage’s completion status (AutoComplete flag on the Stage).

Blocking - By default, tasks are blocking, meaning their transition to COMPLETE is triggered by an event outside of the case engine (i.e. by a case worker explicitly completing a HumanTask). By modeling a task as non-blocking, the engine immediately completes the task after START (or MANUAL_START).

Autocomplete - the Autocomplete property is supported for stages.

Custom PlanItem properties

Custom properties (name/value pairs, both simple strings) are supported for the Case (casePlanModel), stages, and the three types of tasks (human, process, case).

Custom properties are needed to extend or qualify the execution semantics in the context of client integration projects

8.3.2. CMMN Stage and Milestone Visualization 

Intro

CMMN has a concept for stages and milestones. They somewhat overlap but don’t have exactly the same lifecycle. This secion describes how they can be visualized within the case template at runtime and how they can be configured/modeled within a CMMN case model.

Visualization options for stages and milestones

A stage can have the following visualization options (they might change over the lifecycle of the case):

  • Available, active or completed, but hidden in the case template (e.g. the CMMN model contains the stage, but it is not showed in the case template)

  • Available and shown in the case template, but not active at the moment

  • Active and shown in the case template

  • Completed and shown in the case template (completed means, the stage has been active and has been completed)

A milestone can have the following visualization options (they might change over the lifecycle of the case):

  • Available or reached but hidden in the case template

  • Available but not yet reached and shown in the case template

  • Reached and shown in the case template

In addition to whether a state and milestone is rendered within the case template or not, there is a certain order on how they will be rendered. As the case model does not provide a flow-kind of modeling but is rather event and condition driven, the order of stages and milestones must explicitly be modeled within the CMMN case model.

Visualized states

The current implementation of the case templates only knows a concept named availableStates, where milestones and stages are both rendered as states. States have three different options; available, active and completed managed with two boolean flags named isCurrentState and isCompleted.

First step implementation

As a first implementation due to simplicity and also to learn about the real-world requirements, the following new attributes are supported within an edoras vis CMMN case model.

New milestone attributes:

  • Show state → boolean, default is true

  • Show state (RT) → optional expression to define the availability of the milestone dynamically

  • Display order → integer

If the show state flag is true or the expression resolves to true, the milestone is rendered as an available state. As soon as the milestone has been reached, it is rendered as a reached state

New stage attributes:

  • Show state → boolean, default is true

  • Show state (RT) → optional expression to define the availability of the stage dynamically

  • Display order → integer

If the show state flag is true or the expression resolves to true, the stage is rendered as an available state. As soon as the stage is active, it is rendered as active. Once it completes, it is rendered as complete.

8.3.3. Modeling Options

CasePlanModel

Autocomplete

A case completes regardless of this option when there are no active children, no children waiting on a sentry, no children available for manual start, no discretionary items to plan and no un-triggered required milestones. With autocomplete enabled, there may be unplanned discretionary items and it will still complete. In addition, It will also complete if there are un-started manual activation children which are not required.

Stage

Autocomplete

A stage completes regardless of this option when there are no active children, no children waiting on a sentry, no children available for manual start, no discretionary items to plan and no un-triggered required milestones. With autocomplete enabled, there may be unplanned discretionary items and it will still complete. In addition, It will also complete if there are un-started manual activation children which are not required.

Required

When required, a stage is required to complete/exit once its entry criteria have fired (otherwise its parent stage won’t be able to complete). required is only relevant in combination with manual activation. Once started, a stage is always required to complete or exit, regardless of this option.

Manual activation

By default, a stage starts automatically once its entry criteria have fired. With manual activation evaluating to true, the stage must be manually started. The action button to start the stage becomes available as the stage’s entry criteria fire. Manual activation in combination with required has an impact on the auto-completion of the parent stage.

Repetition

With repetition evaluating to true, multiple instances of the stage can be created. A new instance is created each time its entry criteria fire. The custom property manualRepetition allows the user to create new instances by clicking an action button.

Discretionary

A discretionary stage is optional. If discretionary, the stage requires a user action to be promoted to a normal stage where it becomes subjected to the settings for manual activation, required and autocomplete. Sentries are currently not supported for discretionary items. To model an optional stage, consider using a normal stage that has manual activation and is not required.

All Tasks

Required

When required, a task is required to complete/exit once its entry criteria have fired (otherwise its parent stage won’t be able to complete). required is only relevant in combination with manual activation. Once started, a task is always required to complete or exit, regardless of this option.

Manual activation

By default, a task starts automatically once its entry criteria have fired. With manual activation evaluating to true, the task must be manually started. The action button to start the task becomes available as the task’s entry criteria fire. Manual activation in combination with required has an impact on the auto-completion of the parent stage.

Repetition

With repetition evaluating to true, multiple instances of the task can be created. A new instance is created each time its entry criteria fire. The custom property manualRepetition allows the user to create new instances by clicking an action button.

Discretionary

A discretionary task is optional. If discretionary, the task requires a user action to be promoted to a normal task where it becomes subjected to the settings for manual activation and required. Sentries are currently not supported for discretionary items. To model an optional task, consider using a normal task that has manual activation and is not required.

Blocking

Blocking is the default behavior. Use non-blocking if you want the task to immediately complete once it starts. Non-blocking can be used, for example, to model service tasks.

Milestone

Required

When required, a milestone is required to fire in order for its parent stage to complete.

Repetition

With repetition evaluating to true, multiple instances of the milestone can be created. A new instance is created each time its entry criteria fire.

9. edoras gear Documentation

9.1. Introduction

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

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.

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

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"/>
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"/>
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");
  }
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
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.

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

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.

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

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

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.

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.

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.

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.

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.

9.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?

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.

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.

9.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…​

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.

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.

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.

9.1.6. Extending the functionality of edoras gear

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.

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.

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.

9.1.7. Conclusions

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.

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.

9.2. Setup

9.2.1. Overview

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.

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.S91'
}
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.S91</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.

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.

9.2.2. Basic edoras gear configuration

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

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.

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.

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>
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"/>
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[] -->
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.

9.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());
      }
  }

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

9.3. User Guide

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

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>

9.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();
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.

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.

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.

Conversation Metadata

Work in progress.

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>

9.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();
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.

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.

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.

Conversation Metadata

Work in progress

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>

9.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();
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.

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.

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.

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

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>

9.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();
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.

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.

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.

Conversation Metadata

Work in progress

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>

9.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();
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.

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.

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.

Conversation Metadata

Work in progress

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.

9.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();
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.

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>

9.3.8. edoras gear Process Engine

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

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

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.

  TaskQuery taskQuery = TaskQuery.builder().predicate(predicate).build();
  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);
  }
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

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.

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.

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

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.

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"/>;
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.S91</version>
</dependency>
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.

9.4. Process Tutorial

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

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.

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");
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.

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.

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

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.

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.

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

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

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.

9.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).

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

9.5. FAQs

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

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

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

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

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

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

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

9.5.8. How can I configure the database aspects?

Please refer to the user guide, section Database Configuration.

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

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

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

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