Skip navigation
All Places > Alfresco BPM > Blog
1 2 Previous Next

Alfresco BPM

18 posts
salaboy

Activiti 6 is on the Way

Posted by salaboy Employee Apr 26, 2017

Activiti 6.0 Final release is around the corner, and I’m super excited of being part of this release because it is the first step towards a bright future. I’m looking forward to working with the community to shape up the next releases of the project to make sure that we push BPM to the next level.

 

On the Activiti 6.0 release you will find:

  • A new refactored Process Engine optimized and battle tested by several community users and organizations. The introduction of native BPMN support instead of the Process Virtual Machines offers better performance. This builds on top of Activiti’s stateless architecture to bring even higher levels of performance and scalability.
  • The only Open Source BPM framework with the potential and plan for further bringing Content and Process together. Content enriches process management making it true context-aware digital flow rather than just state transitions and service orchestrations. Knowledge and context become the driver for semi-autonomous process.
  • Improved & flexible persistence mechanism which give us the right tools to move highly distributed environments. We are actively working on solutions that when combined with stateless architecture of Activiti’s process engine create new-infinite linear scalability. With Activiti’s small footprint this is ideal for cloud-era on-demand architectures. We are also supporting this direction with our first take in modularizing Activiti’s process services
  • Support for ad-hoc BPMN activities that combined with our Content roots broaden the scope and depth of unstructured and dynamic processes
  • Introduction of modern App Design modeller web app that offers in addition to BPMN modelling, forms, decision tables, granular capability-driven identity management, as well as the ability to bundle models into deployable model-driven process applications

 

I’ve joined Alfresco almost 4 months ago after being for more than 10 years contributing with several Open Source projects on the BPM and Model Driven Frameworks landscape. (You can find my articles, tutorials, books and my thoughts about BPM, Case Management, Rule Engines and AI in my personal blog: http://salaboy.com). I truly believe that we can simplify users experiences by providing the right tools for the job. Also, Developers and DevOps experiences can be simplified by making Activiti project even more ready for the Cloud and continuously updating to the latest standard technologies. I’m looking forward to help the Activiti community to evolve beyond traditional BPM Process Engines to a full-fledged Cloud Ready Business Automation Platform.

 

When I joined Alfresco, I realized the importance of completing the Activiti v6 work already in progress. You’ll see more innovation beyond the original v6 scope in the near future. In contrast with the how the Open Source project was previously organized, I’m committed to make sure that the next releases are built collaboratively between partners, community members and companies that are already using it.

 

Going forward, you will be see me around a lot in the community (http://github.com/salaboy, http://salaboy.com and http://twitter.com/salaboy) and conferences spreading the word. Please get in touch if you want to get involved.

dougjohnson

The Future of Activiti

Posted by dougjohnson Employee Apr 24, 2017

My name is Doug Johnson. I’m responsible for the BPM strategy at Alfresco working with the Activiti project and separately Alfresco’s commercial offerings. As a 20-year industry veteran with 17-years of BPM-specific experience, I’ve seen a lot of new business and solution requirements challenge BPM technologies. Ever increasingly, requirements are demanding even faster and simpler development cycles, better interactions and integration with systems and services, and compelling user experiences and engagement… to name a few trends.

 

The future of Activiti is strong. While statistics are hard to precisely determine, due to Activiti’s flexible licensing terms and easy-of-embeddability, Activiti is absolutely a top Open Source BPM project worldwide—it’s likely the most popular. It’s used to help developers create process-related solutions every day. Some of the largest companies in the world are powered by Activiti.

 

Alfresco, as the main project sponsor, is resolutely backing Activiti with experienced committers and community support. We have added to the Activiti team with outstanding engineers with rich process management and process intelligence backgrounds—including contributors from Bonita BPM, jBPM, Drools, and Pega—as well as those with data science and machine learning experiences including a PhD in advanced mathematics.

 

You’ll be hearing from these new Activiti committers soon and more regularly. At the helm, Mauricio Salatino joins Activiti from a history of Open Source success. He is one of many persons bringing a fresh perspective as Activiti continues to deliver the most relevant process technologies for today and tomorrow’s needs.

 

For those attending BeeCon 2017, I look forward to meeting you this week. I and others will of course be sharing some of the latest in Activiti, including some exciting announcements. Just as important will be the conversations and discussions with you. For those unable to attend BeeCon 2017, watch the forums for pending announcements on further opportunities to interact with the Activiti team in May 2017.

Business Process Management (BPM) best practices often suggests that the BPM solution not be the system of record. In particular, the business data required for the Digital Business Solution should exist in other data stores outside of the persistence store being used by the BPM engine itself. Business data that is used and or created during the execution of a business process should exist and be maintained in one or more external data stores (e.g. RDBMS, NoSQL, etc...). Therefore to simplify and accelerate the development of enterprise scale Digital Business Solutions, Alfresco Process Services (Alfresco’s Enterprise Edition of the Activiti Community Edition (Open Source)) provides an important and valuable component called "Data Models" which is the focus of this blog.

Please note that I’ll be using the acronym APS throughout this post to refer to Alfresco Process Services.

Business Data Integration in BPM solutions

If you haven’t read this blog post (a bit old, but still very relevant) Storing data in automated business processes :: AirQuill  & More on Orchestration Data :: AirQuill  already, you must read it first (especially the first blog) before you go into my next section. This is a great post explaining why storing data outside of process engine tables is so important!

Integration options available in APS

Good news is, APS provides a variety of options for customers to do the business data integration. Listing down below all the data integration options that are available when implementing business processes using Activiti engine:

  1. BPMN Service Task Component
  2. Custom Java Logic wired into the process using listeners such as Execution Listener, Event Listener, Task Listener etc
  3. Execute Custom SQL
  4. REST Task Component (APS/Enterprise Only Feature)
  5. Data Model Component (APS/Enterprise Only Feature)

 

Since the purpose of this blog is to go through the Data Model component, I’ll only be focussing on the 5th item in the above list. Please checkout Activiti User Guide , Activiti Custom SQL User GuideAlfresco Documentation | Alfresco Documentation  and Alfresco Process Services Blogs for more about the other 4 options.

Advantages of Data Model over other alternatives

Given below are some of the pain points we hear from business process modelers, analysts, developers, etc quite often.

  • Systems in my organization are so difficult to integrate into our processes.
  • Data modeling capabilities in our existing BPM platform is highly technical and have got a steep learning curve.
  • As an analyst/modeler, I would love to have some features in the product that allow me to model my SoR (System of Records) data model in the process platform.
  • Our organization has very mature and well defined REST APIs around all our IT systems. However as a business analyst/process modeler, mapping REST API requests and responses are too technical a job for me!
  • We have well defined and re-usable web services based on standards like SOAP, POX etc in our organization. We wish our BPM system has inbuilt capabilities that allow us to write re-usable and business friendly components over these web services.
  • We have been using Activiti community version for a long time. We have a lot of reusable Java code that allow us to integrate our processes with our IT systems. In order to understand those external system integration we often have to dig into the Java source code associated with the process. When moving from community to enterprise, it would be really nice if we can visually represent those data structures in the BPMN modeler and have direct integration of those components to other process components such as forms, rules etc.
  • As a business person, when I review a BPMN diagram I see a lot of service tasks with hidden Java logic in it. Every time when I do this, I have to go to a developer to understand the java components and to find out the input and output fields of those components. This makes me look stupid!
  • We are an organization with a lot of old school two tier applications (client->database) with no APIs. We need our business processes to talk directly to our application databases!
  • We use Alfresco Content Services as our System of Records for documents. What are the integration capabilities of APS with Alfresco Content Services.

 

Data Model is the component that can address all the above mentioned concerns/pain points in an elegant, simple and user friendly fashion without the complexities of similar components that is normally found in other large BPM vendor products.

 

Enough of overview and description and let’s see it in action.

Data Model Demo

The crux of the blog is in the following video which I recorded to demonstrate the various capabilities of the Data Model component

Summary

Let me summarize the post highlighting the key features of Data Models:

  • Data Models allow you to separate the data integration from business process modeling, In other words, process modeling is made easy with Data Models where it allows you to hide the implementation complexity from process models.
  • Data Models are integrated with all other modeling components such as forms, decision tables etc available in APS thereby reducing the time to market of your business process solutions.
  • Data Models allow you build re-usable domain entity objects/components which in-turn can be reused in a uniform way across multiple processes.
  • Data Model is a business friendly integration alternative available in APS.

 

Hands-on time!

If you are new to the APS, this post will help you get started with it - Installing Alfresco Process Services Trial 

If you are new to the Data Model component, I highly recommend you to first read a couple of other posts which are given below, before trying out the demo I used in this post.

The complete source code associated with the above video presentation along with a detailed readme file is available at https://github.com/cijujoseph/activiti-examples/tree/master/activiti-custom-data-model-sample

 

I'll be creating more data model examples in the coming months! Stay tuned...

Table of Contents                   

Introduction

The Activiti Enterprise BPM solution comes with a number of preconfigured Activities that can be used to communicate with the Alfresco ECM system. The following picture shows what features are available:

These are basically specialized Service Task implementations that talk to the Alfresco content management system. In this article we will have a look at how to use them to:

 

  • Fetch content (files) from Alfresco and use in the workflow
  • Upload content to Alfresco from the workflow
  • Fetch properties for content stored in Alfresco
  • Update properties for content stored in Alfresco
  • Apply a Repo Action to a file stored in Alfresco

 

So why would you want to store content in Alfresco in the first place? It is useful to consolidate all content used in the Enterprise into one place instead of having it spread around in each enterprise application and system in use. This makes it easy to search for it, work with it, reuse it, and make it auditable (records management).

Source Code

Source code for the Activiti Developer Series can be found here.

Prerequisites

There is no need to set up an extension project when working with the Alfresco integration, it is all done via the Activiti Application UI.

Running Alfresco and Activiti alongside each other

To follow along with this article we need to have both the Activiti server and the Alfresco server running locally. I will assume that you already have an Activiti Enterprise server running locally. But if you are working mostly with Activiti, then you probably don’t have an Alfresco ECM installation available. If that is the case, then the easiest way to install Alfresco ECM is with the full installer, which you can download from here.

 

Now, by default both the Activiti Tomcat server and the Alfresco Tomcat server listens on the same ports (i.e. 8080, 8005, 8009). So if you have the Activiti server running then stop it as follows:

 

martin@gravitonian:/opt/activiti15$ /opt/activiti15/tomcat/bin/catalina.sh stop

martin@gravitonian:/opt/activiti15$ /opt/activiti15/h2/stop-h2.sh

 

Then open up the <activiti-install-dir>/tomcat/conf/server.xml file and change the port as follows:

 

<Server port="9005" shutdown="SHUTDOWN">

...

 <Service name="Catalina">

...

    <Connector port="9080" protocol="HTTP/1.1"

              connectionTimeout="20000"

              redirectPort="8443" />

...

    <!-- Define an AJP 1.3 Connector on port 8009 -->

    <Connector port="9009" protocol="AJP/1.3" redirectPort="8443" />

 

So what we are doing here is changing the ports as follows:

 

  • 8005 -> 9005
  • 8080 -> 9080
  • 8009 -> 9009

 

Now restart Activiti:

 

martin@gravitonian:/opt/activiti15$ ./start-activiti.sh

 

You should now have Activiti running on http://localhost:9080/activiti-app and Alfresco running on http://localhost:8080/share

Create a site in Alfresco for Activiti content

When uploading content files to Alfresco Repository from an Activiti workflow instance they need to go into a so called Site. So we will create a separate site to be used in this article. Because we are going to work with “case” files we will create a case management site as follows:

 

 

Here we are logged in to Alfresco as the admin user. The Alfresco account/user that will be used from Activiti need to have read and write access to this site, so we will be using the Alfresco admin user from Activiti.

Configure Alfresco Repository Location

Before we can do any type of integration with the Alfresco ECM system we need to tell Activiti about where to find the Alfresco Content Repository. This is done via the Identity Management application (note. you need to be logged in as admin):

 

 

Click on this application so you see this screen:

 

 

Now click on the Tenants menu item, this takes you to this screen:

 

 

Here click on Alfresco Repositories followed by a click on the + sign to the right to add a new repository location (note. you can have multiple repository locations configured):

 

 

Now, set up the repository parameters for your local Alfresco server. We are not using an Alfresco Share connector so you can leave that check-box disabled (it is used when you want to start and manage your processes and tasks from Alfresco Share, read more about it here).

 

Then click Save.

 

This repository configuration can now be used by Activiti when using any of the Activities mentioned in the introduction. It will also be available when using Attachment fields in the form designer.

Give Activiti Users Access to Alfresco Repository

You probably noted that when we told Activiti about the location of the Alfresco Repository we did not provide a username and password. So it is not enough to just tell Activiti about the Alfresco Repository location, we also need to give an Activiti user access to it. This user will be the one executing the Alfresco Activities. In our case we will use the Administrator user for this.

 

Navigate to the Profile for the Activiti user, either via the Profile App, or the Identity Management App and the Personal menu item, so you see something like this:

 

 

Click on the Alfresco Repository you want to set up access to, such as Alfresco 5.2 Local in this case:

 

 

Specify the Alfresco username and password that should be used for accessing the Repository as this Activiti user. The Administrator username and password is specified during installation of Alfresco ECM, it is common to use admin/admin for testing.

 

The admin user was used to create the Case Management site in Alfresco so it will have full access to the site when uploading files from Activiti. If we use another Alfresco user here, then we must make sure that it has read and write access to the site.

Uploading files to Alfresco

At some point in a workflow we might want to upload and store files in Alfresco. For example, let’s say you are processing a case file of some sort and it is now complete, and you want to store the file so it is easily accessible. We can do this with the “Publish to Alfresco” task:

 

 

However, we need a file attached to the workflow before we can store anything in Alfresco. Create a Start form:

 

 

The form should look like this:

 

 

The form starts off with two fields with Case information. This is just so we have some simulated metadata to apply to the “case” files. The Attach field for File A is configured as follows:

 

 

In this case we are allowing the user to only pick files from the local disk. It is possible to also allow the user to pick only from Alfresco, or from both Alfresco and the local file system. For File B we will allow picking either from local file system or from the Alfresco Repository:

 

 

Now save the form and go back to BPMN Editor.

 

We now have some files to upload to Alfresco via the Publish to Alfresco task, click on it so you can see the properties section:

 

 

Start by configuring which Alfresco Repository we want to upload to and what Site we want to use, click on the Alfresco destination property:

 

 

The properties have the following meaning:

 

  • Account: This is the Alfresco Repository that we want to store the files in
  • Destination: This is the folder where we want to store the files. in this case I have selected the Case Management site created initially in this article and the documentLibrary folder (this is the root folder for documents in a site)
  • Publish as: This is the Activiti user account that should be used to connect to Alfresco Repository, it contains the Alfresco username and password to use when connecting.
  • Subfolder & Based on field: This is an interesting property as it can be used to created sub-folders dynamically. In our case we want a new subfolder in the document library created for every case, we can achieve this by creating the subfolder based on the Case Name property set at the start of the workflow.

 

Now save the Alfresco destination configuration.

Using an out-of-the-box content model

Next we need to configure what files, out of those attached to the workflow instance, that we want to upload to Alfresco, and what metadata that should be applied to those files. Click on the Alfresco content property for the task:

 

 

Here we can choose between storing all files attached to the workflow, or just one individual file. We select to publish all files attached (i.e. Publish all content uploaded in process), basically File A and File B as set up in the Start form.

 

When we store the files in Alfresco it needs to know what type they should have and what properties (metadata) that should be applied. Properties are divided into groups called Aspects but they are also part of the Type definition. We can select what properties to supply by adding and removing aspects. Both the type and the aspects need to be part of a Content Model deployed to the Alfresco Repository.

 

Now, you might say, I know that the cm:content type is the default type for a file uploaded to Alfresco, but I don’t know what the cmis:document type is? Activiti uses the CMIS standard when it communicates with Alfresco. CMIS defines its own domain model with types and properties, and it calls aspects Secondary Types. The corresponding CMIS type for the Alfresco type cm:content is called cmis:document.

 

We want to have the Case ID and Case Name properties applied as metadata on the uploaded file. This can be done by specifying an aspect with properties that can be used to hold the Case ID and Name. In this first example we are using the standard out-of-the-box content model, and it has an aspect called cm:titled that we can use for demonstration purpose.

 

This aspect has two properties where we can store the Case ID and Name, click on the Properties mapping tab to set up properties for the configured aspects:

 

 

There is no automatic lookup of what properties that are part of the cm:titled aspect, you need to manually find this out, see the out-of-the-box content model, it has this definition for this aspect:

 

<aspect name="cm:titled">
  <title>Titled</title>
  <properties>
      <property name="cm:title">
          <title>Title</title>
          <type>d:mltext</type>
          <index enabled="true">
              <atomic>true</atomic>
              <stored>false</stored>
              <tokenised>both</tokenised>
          </index>
      </property>
      <property name="cm:description">
          <title>Description</title>
          <type>d:mltext</type>
          <index enabled="true">
              <atomic>true</atomic>
              <stored>false</stored>
              <tokenised>both</tokenised>
          </index>
      </property>
  </properties>
</aspect>

 

Here we are mapping the cm:title property to store the Case ID form property and the cm:description to store the Case Name property. It would of course have been more clear if I had used something like a cm:case aspect, but that is not available out-of-the-box. So let’s first see how this works.

 

Important!. There is one out-of-the-box aspect called cm:auditable that cannot be used by the “Publish to Alfresco” task. This is because its properties are completely managed by the Alfresco server and cannot be set by the end-user.

 

Now, save the content configuration.

 

If we add an End State to the workflow definition we should now be able to test it. For this you need to also create an Activiti Application that will hold the process model. See this info for how to do that.

 

If we now start a workflow instance based on this definition we will be presented with the following start form:

 

Note here that only the “File B” Attach field have the Alfresco Logo. This is because we configured “File A” Attach field to only be able to select files from the local file system. So for File B we can select from either the local file system (by clicking on the SELECT A FILE button) or from a Site in Alfresco (by clicking on the Alfresco Logo). Note that you can only pick files from a site in Alfresco, not from other places such as for example /Company Home/Guest Home.

 

Now fill in the Case ID and Number and select two files from the local filesystem, by clicking on the SELECT A FILE button, you should see something like this now:

 

Then click START PROCESS. The process should complete and if we jump over to Alfresco Share and the Case Management site’s document library we should see the following files in the newly created Case Number One folder:

 

 

We can see that the folder has been created properly and the files have had the cm:titled aspect applied with the Case ID and Name.

 

So this is quite handy, you can get specific case, or project, folders created dynamically on the fly, with metadata applied to the contained files, making it easier to find them.

 

Note. if the workflow instance does not complete after the “Publish to Alfresco” task, then it might have to do with uploading an image and having the Media Management Module installed in Alfresco, see this bug report.

Using a custom content model

In the previous example we used a type and an aspect from an out-of-the-box content model. In a real world scenario it is more likely that we will have to use a domain specific custom content model. So let’s do an example that uses a custom type and a custom aspect. We will continue with the Case example and create a new custom content model around that.

Implementing and deploying a custom content model

Begin by stopping your Alfresco ECM installation if it is running. To implement and deploy a custom content model we can use some out-of-the-box extension files. They need to be renamed before they can be used.

 

Spring context file that loads custom content model:

 

/opt/alfresco/tomcat/shared/classes/alfresco/extension$ mv custom-model-context.xml.sample custom-model-context.xml

 

The content model definition file:

 

/opt/alfresco/tomcat/shared/classes/alfresco/extension$ mv customModel.xml.sample customModel.xml

 

Then open up the customModel.xml file and implement the content model as follows:

 

<?xml version="1.0" encoding="UTF-8"?>
<model name="myc:customModel" xmlns="http://www.alfresco.org/model/dictionary/1.0">
  <description>Custom Model</description>
  <author></author>
  <version>1.0</version>

  <imports>
      <import uri="http://www.alfresco.org/model/dictionary/1.0" prefix="d"/>
      <import uri="http://www.alfresco.org/model/content/1.0" prefix="cm"/>
  </imports>

  <namespaces>
      <namespace uri="http://www.mycompany.org/model/content/1.0" prefix="myc"/>
  </namespaces>

  <types>
      <type name="myc:caseDocument">
          <title>Case Document</title>
          <parent>cm:content</parent>
          <mandatory-aspects>
              <aspect>myc:caseData</aspect>
          </mandatory-aspects>
      </type>
  </types>

  <aspects>
      <aspect name="myc:caseData">
          <title>Case Data</title>
          <properties>
              <property name="myc:caseId">
                  <title>Case ID</title>
                  <type>d:long</type>
              </property>
              <property name="myc:caseName">
                  <title>Case Name</title>
                  <type>d:text</type>
              </property>
              <property name="myc:caseOpenedDate">
                  <title>Case Opened Date</title>
                  <type>d:date</type>
              </property>
          </properties>
      </aspect>
  </aspects>
</model>

 

As we can see, we have one new type called myc:caseDocument, which has one mandatory custom aspect called myc:caseData. We are going to use this new type and aspect when we upload the files. Note that we have added a date property just to show that we can use dates.

 

Note. Activiti uses only date fields in the form editor. However, It is possible to use d:datetime properties in the content model if needed. It will just be displayed in Share as for example “Thu 8 Dec 2016 00:00:00” after initially being set by the “Publish to Alfresco” task.

 

Now start the Alfresco ECM server again and make sure there are no errors.

Updating the process model to use the new custom content model

We now got the custom case content model deployed. Let’s update the process model so it uses the custom type and aspect when uploading the files.

 

Start by adding a Case Opened Date to the start form:

 

 

Then change the Alfresco content property for the “Publish to Alfresco” task as follows, start by changing the type and aspect to the custom ones:

 

Then in the Properties mapping section configure the Case ID property as follows:

 

And the Case Name property:

 

And finally the Case Opened Date property is configured as follows:

 

Important! The Property type for the date property needs to be set as string even though it is a form field of type date. The reason for this is that Activiti internally represents the date field incorrectly and when the “Publish to Alfresco” task is executed we will see see exceptions such as ERROR com.activiti.alfrescoconnector.service.AlfrescoContentService  - error while updating meta data: java.lang.IllegalArgumentException: Invalid aspect value! If we set the Property Type to date, see this bug report.

 

This should be the only thing we need to change.

 

Now save and republish the application and process model.

Testing with a custom content model

If we now start a workflow instance based on this definition we will be presented with the following start form:

 

Fill in the Case ID, Name, and date. Then select two files from the local file-system by clicking on the SELECT A FILE button.

 

Then click START PROCESS. The process should complete and if we jump over to Alfresco Share and the Case Management site’s document library we should see the following files in the newly created Case Number 101 folder:

 

 

In this case we cannot actually see any custom metadata from the new content model. This is because we have not configured the Share UI with forms etc for this content model. However, there is a way to easily check if the properties have been set, click on the first file so you are opening the Document Details page and looking at the Properties section in the lower right corner:

 

 

Because we have not defined a form for the myc:caseDocument type or for the myc:caseData aspect Alfresco will just layout all available properties randomly. But we can see that it all worked as expected. If we want to be 100% sure about what type the doc has and what aspects have been applied, then navigate to the node in the Node Browser (logged in as admin click on Admin Tools |  Node Browser):

 

 

Scroll down a bit to see the rest of the properties and the aspects:

 

Retrieving properties for Alfresco content

Once we got some content in Alfresco that we are working with in the process instance there might be a need to fetch more properties for it. This might be properties such as created and modified dates, properties set by an end-user in Alfresco, properties set by rules in Alfresco etc.

 

To do this we can use the “Retrieve Alfresco properties” task. Add it to our process model as follows:

 

 

Then click on the “Alfresco properties” task property so we can configure what properties we want to fetch:

 

 

The first thing we will do in this form is to select what Alfresco content file we want to fetch properties for. In our case we can select one of the files via the form field that was used to upload it, which will contain info about the CMIS Object ID (i.e. node reference). We could also have a standard process variable containing the content file reference. This means that we could for example have a Java based Service Task that sets up the content file that we work with, without end-user interaction.

 

Note. properties can only be fetched for one content file at a time, if we wanted to fetch properties for both files, then we would have to use two “Retrieve Alfresco properties” tasks.

 

After specifying what content file we are interested in we should add a list of the content model properties that we want to fetch/retrieve and what process variables they will map to. The following shows the properties we have chosen to fetch in this example:

 

 

  • Content Model Property      Process Var            Data Type
  • cmis:name                 - fileAName              (string)
  • cmis:objectId             - fileANodeRef           (string)
  • cmis:creationDate         - fileACreated           (date)
  • cmis:createdBy            - fileACreator           (string)
  • cmis:lastModificationDate - fileAModified          (date)
  • cmis:lastModifiedBy       - fileAModifier          (string)
  • cm:title                  - fileATitle             (string)
  • cm:description            - fileADescription       (string)
  • myc:caseId                - fileACaseId            (number)
  • myc:caseName              - fileACaseName          (string)
  • myc:caseOpenedDate        - fileACaseOpenedDate    (date)

 

Here we got three different types of properties in different namespaces. First we got the standard CMIS properties in the cmis: namespace. For more info about available CMIS properties see this page and the 2.1.4.3.3 Property Definitions section for cmis:document. After that we are fetching some properties from the out-of-the box content model with the cm: namespace. And finally we fetch some properties from our custom model just to demonstrate how that is done.

 

To display the fetched properties let’s use a User Task:

 

 

Configure a form for it as follows:

 

 

So here we are using a number of Display value form fields that will just display value of corresponding process variable. We have grouped the properties with Header form fields.

 

Now, save the process model and republish it.

 

Start a process instance and fill in the start form as follows:

 

 

The new User Task should display the fetched properties as follows:

 

 

We can see that all properties have been fetched successfully, except the date properties. This has to do with the problem described earlier on when Activiti is representing dates incorrectly internally. It does not work to fetch the date properties as strings. To get around this we would have to implement a custom Java based Service Task that uses for example OpenCMIS to fetch the date properties.

 

Worth noting here also is that the title and description are usually populated via metadata extraction in Alfresco, and in this case the PDF file contained a description property that the cm:title was populated with.

 

Related bug report.

Updating properties for Alfresco Content

Ok, so we now know how to upload files from Activiti to Alfresco and how to retrieve properties for files in Alfresco from Activiti. There might also be scenarios where we select files in Alfresco repository for processing in an Activiti process instance (in contrast to selecting them from the local file system as done so far in this article), and at the end of the process instance we want to set some properties for these files.

 

This can be done with the “Update Alfresco properties” task. Let’s try it out by changing some of the properties we have for our files. Add first a User Task, which will be used to set new property values, and then add an Update Alfresco properties task. You should now have a process model looking something like this:

 

 

Configure a form for the Set new Prop values User task as follows:

 

 

The form will also show current values for reference.

 

Now, for the “Update Alfresco properties” task click on the Alfresco properties property for it and fill it in as follows:

 

 

Here we map the form fields we just created to the content model properties. This is straight forward, except for the date property, which has to be configured as a string for it to work as has been discussed before.

 

We also specify what user that should be used to make the update, remember that this user has to be mapped to an Alfresco user that has write permissions to the repository. In this case we just use the process initiator, which is admin.

 

Now, save the process model and republish the application.

 

Then start a new process instance and fill in the first start form as follows:

 

 

Complete the “Show retrieved props” task so you get to the Set new Prop values task:

 

 

Fill in some new values to the left:

 

 

Then click COMPLETE.

 

Now, in the Share UI navigate to one of the files and the Details page for it and verify that the update was successful:

 

 

Related bug report.

Calling Repository Actions in Alfresco

Ok, so we now know how to upload files to the Alfresco Repository and how to classify them at the same time, or at a later point. Next step might be to do some processing on the files we just uploaded. When we process files in Alfresco we usually execute something called a Document Library Action in the front-end (i.e. Share UI), which in turn invokes a so called Repository Action in the back-end.

 

From Activiti we can invoke Repository Actions via a task called “Call Alfresco Action”. Let’s walk through how to call a number of the out-of-the-box repository actions.

Apply an Aspect to a file

A common requirement is to be able to apply a new aspect to an existing content file in Alfresco. This can be done by invoking the add-features Repository Action. In this case it is not enough to just use the “Update Alfresco properties” task as it will not add a new aspect, just set properties. What we need to do is to first use the “Call Alfresco Action” task to apply the aspect and then set the properties.

 

For this example we will create a new process model as follows:

 

 

You might be wondering why we can not provide the aspect properties at the same time as we apply the aspect? That is not possible with the remote REST API that is used by the “Call Alfresco Action”. If you think about how it works from the Share UI when using “Manage Aspects”, or applying an aspect via rules. Then it’s also not possible to provide the aspect properties at the same time, you usually set them via Edit Properties afterwards. We need to use the “Update Alfresco properties” task to set the aspect properties.

 

For a solution on how to apply the aspect and its properties at the same time see next section on executing a script.

 

For the Start Event we create a form where we can select an Alfresco file that we want to execute the action on:

 

 

We also add some fields for the properties that are part of the cm:emailed aspect we want to apply to the file. Configure the Attach field so we can only select content files from Alfresco Repository and the Case Management site:

 

 

Then, for the “Call Alfresco Action” task, select Repository and Content (i.e. the file form field we just configured in the start form):

 

And then finally click on the Action field for the task and configure the Repository Action that we want to invoke on the file:

 

The Action field drop down contains some pre-configured actions that we can choose from, select the Add Aspect action, which will resolve to the action id add-features. You can also type in the Repository Action ID (i.e. the Spring Bean ID for the action) manually if it is not in the list. See this file for a list of actions and their IDs (look for beans with parent="action-executer").

 

When selecting the Add Aspect action a property called aspect-name is automatically added to the list of parameters that should be passed to the action invoker. It should be set to the aspect name that we want to apply to the file, in this case the out-of-the-box aspect cm:emailed.

 

Then we specify what aspect related properties we want to set via the “Update Alfresco properties task”, and from what form fields to grab the values:

 

Remember that Activiti represents dates incorrectly internally so when we set properties we need to define the date property as a string for it to work, see above.

 

Now let’s try this process model. After starting a process instance we get presented with the Start Form where we should select a file from the Case Management site and fill in some random email info:

 

Click START PROCESS to get the aspect applied. If you navigate to the file in Alfresco Share you should see the following under the Properties section:

 

 

The cm:emailed aspect has a form defined so that’s why we see these properties nicely laid out. But we still don’t have any form defined for myc:caseDocument so the other properties will not be visible. But you can always go via the Node Browser to see all properties.

 

Important, if the first “Call Alfresco Action” executes successfully but the second “Update Alfresco properties” task fails, then you would have a repository in an inconsistent state. To have both the apply aspect and set properties operations in the same transaction you would have to implement a custom service task and handle all the updates manually. Or use a script as in the next section.

Calling a Script

Another thing that is useful to be able to do from Activiti is to execute a JavaScript file in the Repository. This can be done by executing the script Repository Action. Create a new process model with a Start Event form that has an attachment field. Then add the “Call Alfresco Action” task.

 

Configure the Content and Repository properties for the task as follows:

 

 

So we now know what repository to talk to and what file to apply the script to. Next step is to configure the Action property for the task:

 

Select Execute a script from the Action drop down. This will add the mandatory script-ref parameter. The parameter has to be set to the Node Reference for the JavaScript file in the repository. For example: workspace://SpacesStore/fe60c4b5-ea73-4ef8-9b5e-33d2772c5bf6

 

To figure out what the script-ref should be add a JavaScript file called applyAspect.js to the /Company Home/Data Dictionary/Scripts folder in the Alfresco Repository:

 

 

Set the Mimetype to JavaScript.

 

It should have JavaScript code looking like this:

 

var props = new Array();
props['cm:originator'] = 'martin@mycompany.com';
props['cm:addressee'] = 'somebody@mycompany.com';
props['cm:subjectline'] = 'Testing Some stuff';
props['cm:sentdate'] = new Date();
document.addAspect("cm:emailed", props);

 

Now, to find out the Node Reference for this script have a look at the Details Page for the file in Alfresco Share, it will have a link property with this information:

 

 

Copy the nodeRef value and set it as the value for the script-ref action parameter.

Now run this process model. We should see a similar effect on the file as in the Apply Aspect action execution.

 

Being able to execute a script like this gives you almost unlimited possibilities in processing the Repository content. However, it is not possible to pass parameters to the script, so this action probably needs to work in conjunction with some other task.

Set Content Type for a file

Another very useful thing to be able to do from Activiti is to set a new type for a content file in Alfresco. This can be done by executing the specialise-type Repository Action. Create a new process model with a Start Event form that has an attachment field. Then add the “Call Alfresco Action” task.

 

Configure the Content and Repository properties for the task as follows:

 

 

So we now know what repository to talk to and what file to set the content type on. Next step is to configure the Action property for the task:

 

Select Specialise type from the Action drop down. This will add the mandatory type-name parameter. The parameter has to be set to the content model type name. For example: myc:caseDocument. Note that for this action to have any effect the type we set must extend the type already set on the content file.

 

Because the files in the Case Management site already have the myc:caseDocument type set, we will have to upload a new file to the Case Management site’s Document Library before we try this out.

Moving a file

One quite useful thing to be able to do from Activiti is to move a file in the Alfresco Repository after some processing have been done on it in the workflow, maybe something like a review and approval. This can be done by executing the move Repository Action. Create a new process model with a Start Event form that has an attachment field. Then add the “Call Alfresco Action” task.

 

Configure the Content and Repository properties for the task as follows:

 

 

So we now know what repository to talk to and what file to move. Next step is to configure the Action property for the task:

 

 

Select Move from the Action drop down. This will add the mandatory destination-folder parameter. The parameter has to be set to the Alfresco Node Reference for the folder where you want to move the file. For example: workspace://SpacesStore/2a8052aa-e5ec-4546-85bf-49f55f6b7e81

 

In this sample I have just created an Approved folder in the Case Management site’s Document Library:

 

 

Now, to find out the Node Reference for this folder have a look at the Folder Details Page in Alfresco Share, it will have a link property with this information:

 

 

Copy the nodeRef value and set it as the value for the destination-folder action parameter.

 

Now run this process model. We should see that the selected file is moved into the Approved folder.

Table of Contents                   

 

Introduction

Something that comes up quite often in Activiti Forums is how to add a new form field type and render it in the Activiti Application UI. Doing this is actually quite easy. We will look at a couple of examples. The first one is a simple Month field presented as a dropdown where the end-user can select a month. This is all static and involves no dynamic data. However, in many solutions we need to load a dropdown or a list with data from an external service. The second example shows one way of doing that via a REST call. And finally we will have a look at an example that also involves custom parameters specified by the form designer.

 

If you are working with an earlier version of Activiti that has the Explorer UI, then see this blog for how to do this.

 

This article covers the new Angular based UI.

Source Code

Source code for the Activiti Developer Series can be found here.

Prerequisites

There is no need to set up an extension project when working with custom form fields, it is all done via the Activiti Application UI.

Create a Custom Form Stencil

All the custom form fields that we create will be part of a custom form stencil. You can think of a custom form stencil as a custom Form Editor configuration.

 

Stencils are created via the Kickstart App:

 

 

Clicking on this application shows the following page:

 

 

Click on the Stencils menu item at the top and then on the Create a new Stencil now! Button:

 

 

Here I have to select Form editor from the Editor type drop down. We should now have a custom stencil as follows:

 

Implementing a Month Form Field

Let’s start with a simple example where we want to add a form field type that allows the user to select a month. Visually, we want to use a dropdown list for that on the UI side of things.

 

When finished we will have something that looks like this:

 

 

And whatever month is selected will be available in a process variable when the form is completed.

 

When designing a form the month field type will look like this:

 

Creating the Form Field in the Custom Stencil

Open up the “My Company Form Editor” stencil in the Stencil Editor:

 

 

Here we have the default form editor palette of fields that we can use when creating forms. Now, to add a new field type, click on the +Add new item in the upper right corner, this displays the following screen:

 

 

The properties in this screen have the following meaning:

 

  • Name: the name of this field in the Form Editor palette
  • Description: the description of this field in the Form Editor palette
  • Internal identifier: id that can be used to access the field control in JavaScript
  • Icon: the icon of this field in the Form Editor palette
  • Field width: how many columns should this field take up in the finished form shown to user
  • Form runtime template: the markup that makes up the UI representation of the field, as shown to the end-user
  • Form editor template: what you the form designer see when using this field in a form

 

Fill in the properties like this (to the right we can see where the properties are used in the form designer or in the finished form as shown to the end user):

 

 

In this case we have given the new field the name Month, and it will show up as that in the Form Editor palette just after the icon. The icon is a custom one that is based on the text field one but with some extra month list text in it.

 

The Form runtime template has been filled in with HTML markup that will be used to display the field in the form as seen by the end-user. The UI is written in Angular version 1, so there is some Angular directives in the code, such as ng-controller. It will bind this UI code to a Angular controller called monthController, which we have to define later on.

 

There is also the ng-model Angular directive that can be used to do a data binding so when you select something in the dropdown, the value is stored in the data.selectedMonth JavaScript variable. The data binding is bidirectional so we can initialize the dropdown (i.e. pre-select) in the controller if we want to.

 

We also need to define how the new field should be displayed in the form editor when dragged onto the form canvas. This is done with the Form editor template, keep this display static and simple as it does not support parameters.

 

Next step is to implement the Angular controller for the field, this is done by clicking on the Edit button in the field creator screen:

 

This brings up the following dialog where we can implement the controller:

 

 

The following code attaches this Controller to the HTML markup we just created (i.e. the View):

 

angular.module('activitiApp').controller('monthController'

 

The following function declaration starts the controller implementation:

 

function ($rootScope, $scope) {

 

Inside the controller we first need to set up the JavaScript object that is used for the data binding to the dropdown control:

 

$scope.data = {
    selectedMonth: null,
};

 

This is also where you could initialize more variables that should be used in the dropdown control, such as initially selected option. For more information about scopes, see Angular scope guide.

 

After this we hook into one of the extension points in the UI called formBeforeComplete, which will be called just before the end-user completes the user-task. There are a number of empty extension point functions that we can implement to get called back at certain points/events in the UI lifecycle. See these docs for a full list of extension points.

 

These extension functions are all defined in the ALFRESCO.formExtensions object. Our implementation looks like this:

 

ALFRESCO.formExtensions.formBeforeComplete =
    function(form, outcome, scope) {
        console.log('Before form complete');
        $scope.field.value = $scope.data.selectedMonth;
    };  

 

What this code does is making sure that what was selected in the month drop down box, such as the value feb, for month February, is set in the field value as specified by the form designer. Basically, when the form designer uses our control, they will give the field an ID, such as this:

 

The following code make sure that the selectamonth field will have the selected month value set:

 

$scope.field.value = $scope.data.selectedMonth;

 

This means also that there will be a process variable with the name selectamonth available with the value that the end-user selected.

Trying out the Month Field

Create a simple process model with a single User Task as follows:

Then create a form for the user task by clicking on the Referenced form property for it and then click New form button:

 

Call the new form Select Month and make sure to select the My Company Form Editor stencil that we have just customized with the Month field:

 

Drag-and-drop the new Month field into the form canvas and add also a standard text field so we can see how they are presented together. Fill in the Month field properties as follows:

 

Now, create an Application and add this process model to it so we can publish it and execute it. For more information about this see this blog section.

 

Start a process instance and you should see a User Task form as follows:

 

Select a month and fill in some text:

 

Then click the COMPLETE button.

 

If we were to install an execution listener on the sequence flow after the user task, and it printed process variables, then we would see something like this:

 

04:29:37,387 [http-nio-8080-exec-6] INFO  com.activiti.extension.bean.HelloWorldExecutionListener  - --- Process variables:

04:29:37,387 [http-nio-8080-exec-6] INFO  com.activiti.extension.bean.HelloWorldExecutionListener  -    [specifysometext = Activiti is cool!]

04:29:37,387 [http-nio-8080-exec-6] INFO  com.activiti.extension.bean.HelloWorldExecutionListener  -    [initiator = 1]

04:29:37,387 [http-nio-8080-exec-6] INFO  com.activiti.extension.bean.HelloWorldExecutionListener  -    [selectamonth = Aug]

Debugging an Angular Controller

Implementing Angular controllers can sometimes be a bit frustrating if you don’t know how to debug them. To debug JavaScript code we can for example use the FireBug plugin.

 

What we want to do first is to kick off a new process instance so we see the user task as follows:

 

 

Now, start FireBug in detached mode, or whatever other JS debugger you are using:

 

 

If you are not in the Script tab switch to it. To set a breakpoint in the controller code select the  controllers file from the drop down in upper left corner:

 

 

Set breakpoints by clicking in the area to the left of the line numbers. In this case I have set one breakpoint just at the entry of the controller and one inside the callback function, which is useful as I can then be sure the extension point is actually called.

 

Select a month and fill in some text and then click COMPLETE, the debugger should stop at the breakpoint inside the extension point callback code:

 

 

You can inspect variables to the right in the Watch area. This should give you an idea of how to debug your Angular Controllers.

Implementing a Dynamic Form Field

In this example we will see how we can populate a dropdown with data from a REST call.

What the end result look like

In this example we will use an external Web Service that can return a list of the US States.

 

We call the web service like this:

 

http://services.groupkt.com/state/get/usa/all

 

{
 "RestResponse" : {
   "messages" : [ "More webservices are available at

http://www.groupkt.com/post/f2129b88/services.htm",

"Total [56] records found." ],
   "result" : [ {
     "country" : "USA",
     "name" : "Alabama",
     "abbr" : "AL",
     "area" : "135767SKM",
     "largest_city" : "Birmingham",
     "capital" : "Montgomery"
   }, {
     "country" : "USA",
     "name" : "Alaska",
     "abbr" : "AK",
     "area" : "1723337SKM",
     "largest_city" : "Anchorage",
     "capital" : "Juneau"
   }, {
     "country" : "USA",
     "name" : "Arizona",
     "abbr" : "AZ",
     "area" : "294207SKM",
     "capital" : "Phoenix"
   },...

 

It responds with JSON and we can get to the list of states via the RestRespone.result property.

 

The end-user will see the following dropdown in a form that uses this field:

And whatever state is selected will be available in a process variable when the form is completed.

 

When designing a form the state field type will look like this:

 

Prerequisites

As this involves making a call to another domain it will not work straight away because of the same origin policy applied by the web browsers. If we just went on with this implementation and called the other domain (i.e. not the local domain where we loaded the Activiti Application), then we would see the following type of error messages during debugging:

 

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://services.groupkt.com/state/get/usa/all. (Reason: missing token 'cache-control' in CORS header 'Access-Control-Allow-Headers' from CORS preflight channel).

 

We can easily get around this by installing a Proxy in front of the Activiti Server and the External Web Service. We would access everything through the proxy so to the browser it looks like everything is coming from the same origin.

 

Note. if the server supports JSONP, such as the Alfresco ONE server, you could get around installing an extra proxy. Angular $http supports JSONP calls.

Installing NGINX proxy

The NGINX server is a useful piece of software that makes it easy to set up a Reverse Proxy locally. It has a similar function as Apache HTTP Server. The installation is different for different operating systems, here is how I installed it on Ubuntu (see the NGINX site for more info on how to install on other platforms):

 

$ sudo netstat -nlp |grep :80

tcp6       0      0 :::80                   :::*                    LISTEN      2922/apache2

$ /etc/init.d/apache2 stop

[ ok ] Stopping apache2 (via systemctl): apache2.service.

 

$ sudo apt-get install nginx

Setting up nginx-core (1.9.3-1ubuntu1.2) ...

Setting up nginx (1.9.3-1ubuntu1.2) ...

Configure NGINX proxy

So we want to configure the proxy with location of both the Activiti server and the external web service. Open up the default site configuration:

 

martin@gravitonian:/etc/nginx/sites-available$ sudo gedit default

 

And change it so it looks like this:

 

server {

    listen                          8888;

    server_name                     dev-proxy;

 

    access_log  /var/log/nginx/dev-proxy.log;

    error_log  /var/log/nginx/dev-proxy.error.log;

 

    location /state {

          proxy_pass  http://services.groupkt.com;

    }

 

    location /activiti-app/ {

   proxy_pass  http://localhost:8080;

   proxy_pass_header  Set-Cookie;

   proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;

 

   proxy_redirect          off;

   proxy_set_header        Host            $host;

   proxy_set_header        X-Real-IP       $remote_addr;

   proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;

   client_max_body_size    10m;

   client_body_buffer_size 128k;

   proxy_connect_timeout   90;

   proxy_send_timeout      90;

   proxy_read_timeout      90;

   proxy_buffers           32 4k;

    }

}

 

Then restart the proxy:

 

$ sudo service nginx restart

 

You should be able to access the proxy via browser now at http://localhost:8888/ and see something like this:

 

Now, try and access the external web service at http://services.groupkt.com/state/get/usa/all via the proxy on http://localhost:8888/state/get/usa/all:

 

 

And Activiti at http://localhost:8080/activiti-app via the proxy at http://localhost:8888/activiti-app:

 

 

Ok, that’s it, we are all set now to start implementing our US state dropdown.

Creating the Form Field in the Custom Stencil

Open up the “My Company Form Editor” stencil in the Stencil Editor:

 

 

To add a new field type, click on the +Add new item in the upper right corner. Fill in the form as follows (to the right we can see where the properties are used in the form designer or in the finished form as shown to the end user):

 

In this case we have given the new field the name US State, and it will show up as that in the Form Editor palette just after the icon. Upload an icon that fits the field.

 

The Form runtime template has been filled in with HTML markup that will be used to display the field in the form as seen by the end-user. It will bind this UI code to an Angular controller called usSateController, which we have to define later on.

 

The ng-model Angular directive is used to do a data binding so when you select something in the dropdown, the value is stored in the data.selectedState JavaScript variable. The data binding is bidirectional so we also use it to fill the dropdown with options, via the ng-options Angular directive, from the data.states property, which we will set up in the controller implementation.

 

We then define how the new field should be displayed in the form editor when dragged onto the form canvas. This is done with the Form editor template, keep this display static and simple as it does not support parameters.

 

Next step is to implement the Angular controller for the field, this is done by clicking on the Edit button in the field creator screen:

 

 

This brings up the following dialog where we can implement the controller:

 

The full JavaScript code looks like this:

 

angular
.module('activitiApp')
.controller('usSateController',
['$rootScope', '$scope', '$http',
  function ($rootScope, $scope, $http) {
       $scope.data = {
           selectedState: null,
           states: null
       };
    
       // Fetch all the states from an external REST service
       // that responds with JSON
       $http.get('http://localhost:8888/state/get/usa/all').
           success(function(data, status, headers, config) {
               var tempResponseArray = data.RestResponse.result;
               $scope.data.states = [];   
               for (var i = 0; i < tempResponseArray.length; i++) {
                   var state = { name: tempResponseArray[i].name,
                                 code : tempResponseArray[i].abbr };
                   $scope.data.states.push(state);   
               }   
           }).
           error(function(data, status, headers, config) {
                   alert('Error: '+ status);
                   tempResponseArray = [];
           });         

       // Setting the value before completing the task so it's properly stored
       ALFRESCO.formExtensions.formBeforeComplete =
           function(form, outcome, scope) {
               $scope.field.value = $scope.data.selectedState;
           };    
  }]
);

 

The following code attaches this Controller to the HTML markup we just created (i.e. the View):

 

angular.module('activitiApp').controller('usSateController' 

 

The following function declaration starts the controller implementation:

 

function ($rootScope, $scope, $http) {

 

Note here that we also bring in the $http Angular module so we can execute our REST call to the Web Service.

 

Inside the controller we first need to set up the JavaScript object that is used for the data binding to the dropdown control:

 

$scope.data = {
    selectedState: null,
    states: null
};

 

For more information about scopes, see Angular scope guide.

 

We then make the Web Service call via the $http service:

 

$http.get('http://localhost:8888/state/get/usa/all').
    success(function(data, status, headers, config) {
        var tempResponseArray = data.RestResponse.result;
        $scope.data.states = [];   
        for (var i = 0; i < tempResponseArray.length; i++) {
            var state = { name: tempResponseArray[i].name,
                          code : tempResponseArray[i].abbr };
            $scope.data.states.push(state);   
        }   
    }).
    error(function(data, status, headers, config) {
        alert('Error: '+ status);
        tempResponseArray = [];
    });  

 

Note here that we make the Web Service call via the proxy at localhost:8888. The get() call is asynchronous and will return promises, so we need to make sure we do all processing inside the success() method. Otherwise we will try and do the processing before the REST call returns. The $http service will automatically handle JSON responses and we can grab the JSON Array straight from the data.RestResponse.result property. We then get the State Name and Code from the name and abbr properties respectively.

 

After this we hook into one of the extension points in the UI called formBeforeComplete, which will be called just before the end-user completes the user-task. Our implementation looks like this:

 

ALFRESCO.formExtensions.formBeforeComplete =
     function(form, outcome, scope) {
         $scope.field.value = $scope.data.selectedState;
     };           

 

What this code does is making sure that what was selected in the state drop down box, such as the value FL, for state Florida, is set in the field value as specified by the form designer. Basically, when the form designer uses our control, they will give the field an ID, such as this:

 

 

The following code make sure that the chooseusstate field will have the selected state value set:

 

$scope.field.value = $scope.data.selectedState;

 

This means also that there will be a process variable with the name chooseusstate available with the value that the end-user selected.

Trying out the US State Field

Continuing with the process and user-task form we created earlier on for the Month field. Important, be sure to access Activiti via the proxy at http://localhost:8888/activiti-app. Open up the form and add a US State field to it:

 

Drag-and-drop the new US State field into the form canvas and fill in the field properties as follows:

 

 

Now, re-publish the application to get the changes to take effect.

 

Start a new process instance and you should see a User Task form as follows:

 

 

Select a month, fill in some text, and choose a US State:

 

 

Then click the COMPLETE button.

 

If we were to install an execution listener on the sequence flow after the user task, and it printed process variables, then we would see something like this:

 

10:44:44,216 [http-nio-8080-exec-10] INFO  com.activiti.extension.bean.HelloWorldExecutionListener  - --- Process variables:

10:44:44,216 [http-nio-8080-exec-10] INFO  com.activiti.extension.bean.HelloWorldExecutionListener  -    [greeting1Proc = Hello World!]

10:44:44,216 [http-nio-8080-exec-10] INFO  com.activiti.extension.bean.HelloWorldExecutionListener  -    [chooseusstate = HI]

10:44:44,216 [http-nio-8080-exec-10] INFO  com.activiti.extension.bean.HelloWorldExecutionListener  -    [specifysometext = Activiti is cool!]

10:44:44,216 [http-nio-8080-exec-10] INFO  com.activiti.extension.bean.HelloWorldExecutionListener  -    [greeting1ProcLocal = Hello World Local!]

10:44:44,216 [http-nio-8080-exec-10] INFO  com.activiti.extension.bean.HelloWorldExecutionListener  -    [initiator = 1]

10:44:44,216 [http-nio-8080-exec-10] INFO  com.activiti.extension.bean.HelloWorldExecutionListener  -    [selectamonth = null]

Implementing a Hyperlinked Image Form Field

So far all our example custom fields have not needed any form designer input. Meaning they have needed no configuration at the time they were included in the user-task form. However, there might be certain custom form fields that need extra parameters for configuring the field when it is included in the form.

 

The next example will show how to use custom parameters for a form field. We will implement a hyperlink image field, which will need the form designer to supply both the image link and the hyperlink at the time when the form is designed.

Creating the Form Field in the Custom Stencil

Open up the “My Company Form Editor” stencil in the Stencil Editor:

 

 

To add a new field type, click on the +Add new item in the upper right corner. Fill in the form as follows (to the right we can see where the properties are used in the form designer or in the finished form as shown to the end user):

 

In this case we have given the new field the name Image Hyperlink, and it will show up as that in the Form Editor palette just after the icon. Upload an icon that fits the field.

 

The Form runtime template has been filled in with HTML markup that will be used to display the field in the form as seen by the end-user. In the above example the configuration parameters (i.e. hyperlinkURL and imageURL) have been set to point to the Activiti Org site.

 

We then define how the new field should be displayed in the form editor when dragged onto the form canvas. This is done with the Form editor template, keep this display static and simple as it does not support parameters.

 

Next step is to define the two configuration parameters. Click on the Edit link at the bottom of the field creator screen:

 

 

This brings up the following dialog where we can define custom tabs and properties:

 

 

Start by adding a new Tab called Image and position it between the two existing tabs, then add the first property with id hyperlinkURL:

 

 

Click Save property. Then add the other property with id imageURL:

 

 

Click Save property. And then save the stencil.

 

When you specify the IDs for the properties it is important that they match what has been coded in the Form runtime template.

Continuing with the process and user-task form we created earlier on for the other custom fields. Important, be sure to access Activiti via the proxy at http://localhost:8888/activiti-app as we still have the dynamic form field. Open up the form and add an Hyperlink Image field to it:

 

 

Then hover over the field and click the pen icon to the right to edit properties:

 

Specify a hyperlink URL and an image URL.

 

Now, re-publish the application to get the changes to take effect.

 

Start a new process instance and you should see a User Task form as follows:

 

 

If you click on the Activiti Logo it should take you to the http://www.activiti.org site.

Table of Contents                   

 

Introduction

The Activiti Enterprise application comes with an extensive REST API. However, sometimes it might be useful to add your own REST API, to for example support a domain specific workflow implementation. However, before starting any major REST API development, make sure that you are familiar with the REST API that is available out of the box, so you don’t implement stuff that is already supported. It might also at this point be a good idea to read up a bit on the Spring Web MVC framework, which is what the Activiti REST API is based on.

 

Here is a quick summary to get you going. Spring’s annotation based MVC framework simplifies the process of creating RESTful web services. The key difference between a traditional Spring MVC controller and the RESTful web service controller is the way the HTTP response body is created. While the traditional MVC controller relies on the View technology, the RESTful web service controller simply returns the object and the object data is written directly to the HTTP response as JSON or XML.

 

In the traditional workflow the ModelAndView object is forwarded from the controller to the client. Spring lets you return data directly from the @Controller, without looking for a view, using the @ResponseBody annotation on a method. When you use the @ResponseBody annotation on a method, Spring converts the return value and writes it to the HTTP response automatically. Each method in the Controller class must be annotated with @ResponseBody.

 

Beginning with Spring version 4.0, this process is simplified even further with the introduction of the @RestController annotation. It is a specialized version of the controller which is a convenience annotation that does nothing more than adding the @Controller and @ResponseBody annotations. By annotating the controller class with @RestController annotation, you no longer need to add @ResponseBody to all the methods that should map to requests. The @ResponseBody annotation is active by default. You can read more about it here.

Source Code

Source code for the Activiti Developer Series can be found here.

Prerequisites

Before starting with your REST API implementation make sure to set up a proper Activiti Extension project.

Implementing a Custom REST API

This section walks you through the process of creating a "Hello World" REST API with Spring.

 

We will build a REST endpoint that accepts the following HTTP GET requests:

 

http://localhost:8080/activiti-app/app/hello:

 

http://localhost:8080/activiti-app/app/hello?name=Martin:  

 

 

http://localhost:8080/activiti-app/app/hello/martin:

 

 

As you can see, we can make a GET request to the endpoint in three different ways, without any parameters with /hello, with a query parameter like /hello?name={name}, and with a path parameter like /hello/martin. The response to all these requests will be XML. We will see later on how to change it to also support JSON.

Implementing the REST Controller

In Spring’s approach to building web sites, HTTP requests are handled by a controller. You can easily identify these request classes by the @RestController annotation. In the following example, the HelloWorldController handles GET requests for /hello by returning XML.

 

package com.activiti.extension.rest;

import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/hello")
public class HelloWorldRestController {

  @RequestMapping(value = "/", method= RequestMethod.GET)
  public GreetingMessage sayHello(@RequestParam(value="name", required=false,
                                                defaultValue="World") String name) {
      GreetingMessage msg = new GreetingMessage(name, "Hello " + name + "!");
      return msg;
  }

  @RequestMapping(value = "/{name}", method= RequestMethod.GET)
  public GreetingMessage sayHelloAgain(@PathVariable String name) {
      GreetingMessage msg = new GreetingMessage(name, "Hello " + name + "!");
      return msg;
  }
}

 

Note here in which Java package this class is located in: com.activiti.extension.rest. This will register this endpoint as available under /activiti-app/app URL path. It is then meant to be used in the application UI. And it will use cookie based authentication. So if we are logged into the Activiti Application when trying this REST endpoint, then there will be no need to authenticate.

 

The use of the @RestController annotation marks this class as a controller where every method returns a domain object/POJO instead of a View. All URLs will start with /hello for this controller. We are responding with a domain object converted into format understood by the consumers. In our case, due to the Jackson library being included in the class path, the GreetingMessage object will be converted into XML format (or into JSON as we will see later on).

 

The @RequestMapping annotation specifies the exact request that will map to the class and method, for example /app/hello/martin will map to the class and the sayHelloAgain method. The request method (i.e. GET, POST etc) has no default value so we need to set it to something, HTTP GET in this case.

 

For the response to be converted into XML we also provide the following data transfer object class:

 

package com.activiti.extension.rest;

import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import java.io.Serializable;

@XmlRootElement(name = "greeting")
public class GreetingMessage implements Serializable {
  String name;
  String text;

  public GreetingMessage() {}

  public GreetingMessage(String name, String text) {
      this.name = name;
      this.text = text;
  }

  @XmlElement(name = "name")
  public String getName() {
      return name;
  }

  @XmlElement(name = "text")
  public String getText() {
      return text;
  }
}

 

This class uses the JAXB annotations to tell Spring MVC what XML elements to use in the responses.

Making the REST endpoint part of Public API

So far we created a REST endpoint that is available in the Activiti Application context with cookie based authentication. This is not ideal for the public REST API. We would like to instead have it available under /activiti-app/api and supporting basic authentication.

 

To make this happen we need to do just a few changes, here is the updated controller:

 

package com.activiti.extension.api;

import com.activiti.extension.rest.GreetingMessage;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/enterprise")
public class HelloWorldRestController {

  @RequestMapping(value = "/hello", method= RequestMethod.GET)
  public GreetingMessage sayHello(@RequestParam(value="name", required=false,
                                                defaultValue="World") String name) {
      GreetingMessage msg = new GreetingMessage(name, "Hello " + name + "!");
      return msg;
  }

  @RequestMapping(value = "/hello/{name}", method= RequestMethod.GET)
  public GreetingMessage sayHelloAgain(@PathVariable String name) {
      GreetingMessage msg = new GreetingMessage(name, "Hello " + name + "!");
      return msg;
  }
}

 

The class has been moved into the com.activiti.extension.api package. This package is scanned automatically by Activiti and any components are instantiated as custom REST endpoints in the public REST API, protected by basic authentication. Note that the endpoint needs to have /enterprise as first element in the URL, as this is configured in the SecurityConfiguration to be protected with basic authentication (more specifically, the api/enterprise/* path).

 

Now, with basic auth available, it is easy to use other clients than the browser, such as Postman, curl etc. We can also in an easy way request JSON responses by setting the Accept header.

 

Let’s look at a couple of calls using Postman. First we need to set up the Authorization:

 

 

Then configure the Accept header to application/json to see that we can get responses in JSON format:

 

 

Now, click Send button with the http://localhost:8080/activiti-app/api/enterprise/hello?name=Martin GET URL specified as above:

 

 

We can see the JSON response. To have responses in XML, just remove the Accept header:

 

 

Using curl is as easy:

 

$ curl -u admin@app.activiti.com:admin http://localhost:8080/activiti-app/api/enterprise/hello?name=Martin |xmlstarlet fo

 % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current

                                Dload  Upload   Total   Spent    Left  Speed

100   121    0   121    0     0   7330      0 --:--:-- --:--:-- --:--:--  7562

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>

<greeting>

 <name>Martin</name>

 <text>Hello Martin!</text>

</greeting>

 

Here we are piping the XML response into the xmlstarlet tool for formatting. To get JSON responses, do something like this:

 

$ curl -u admin@app.activiti.com:admin -H "Accept: application/json" http://localhost:8080/activiti-app/api/enterprise/hello?name=Martin |jq '.'

 % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current

                                Dload  Upload   Total   Spent    Left  Speed

100    40    0    40    0     0   3587      0 --:--:-- --:--:-- --:--:--  3636

{

 "name": "Martin",

 "text": "Hello Martin!"

}

 

In this case we are using the jq command line tool for formatting of the JSON response.

Injecting Spring Beans into the REST Controller

So far we have used a REST controller class that was not dependent on any other component. In a real scenario we are likely to have to wire in custom beans and out-of-the-box beans.

 

Take the following custom Spring Bean:

 

package com.activiti.extension.bean.service;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

@Service
public class HelloWorldService {
  private static Logger logger = LoggerFactory.getLogger(HelloWorldService.class);

  public String greeting() {
      logger.info("Custom Service method greeting() was called");
    
      return "Hello World from Service!";
  }
}

 

It’s defined in a sub-package to the com.activiti.extension.bean package so it will be picked up by Activiti during scanning and registered as a Spring bean with the helloWorldService name.

 

We can now wire this bean into our REST Controller as follows:

 

package com.activiti.extension.api;

import com.activiti.domain.idm.User;
import com.activiti.extension.bean.service.HelloWorldService;
import com.activiti.extension.rest.GreetingMessage;
import com.activiti.service.api.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/enterprise")
public class HelloWorldRestController {

  @Autowired
  HelloWorldService helloWorldService;

  @Autowired
  UserService userService;

  @RequestMapping(value = "/hello", method= RequestMethod.GET)
  public GreetingMessage sayHello(@RequestParam(value="name", required=false,
                                                defaultValue="World") String name) {
      long userId = 1; // Admin
      User user = userService.findUser(userId);
      String username = user.getFirstName() + " " + user.getLastName();

      GreetingMessage msg = new GreetingMessage(name, "Hello " + name +
                ", service message: " + helloWorldService.greeting() +
                ", userId = 1 = " + username);

      return msg;
  }

  @RequestMapping(value = "/hello/{name}", method= RequestMethod.GET)
  public GreetingMessage sayHelloAgain(@PathVariable String name) {
      GreetingMessage msg = new GreetingMessage(name, "Hello " + name + "!");

      return msg;
  }
}

 

As you can see, we also wire in one of the out-of-the-box services called UserService, and use it to get the first name and last name of the Administrator user.

 

Making a call via curl now shows the following response, that have involved to external beans:

 

$ curl -u admin@app.activiti.com:admin -H "Accept: application/json" http://localhost:8080/activiti-app/api/enterprise/hello?name=Martin |jq '.'

 % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current

                                Dload  Upload   Total   Spent    Left  Speed

100   116    0   116    0     0   1509      0 --:--:-- --:--:-- --:--:--  1526

{

 "name": "Martin",

 "text": "Hello Martin, service message: Hello World from Service!, userId = 1 = null Administrator"

}

 

The administrator user has no first name, hence it is null.

Calling a REST endpoint from a Process

Closely related to implementing a custom REST API is calling out to REST endpoints from a process. This can be done with the out-of-the-box REST call task:

 

 

Read more about it here and watch this video for a detailed walkthrough. The custom REST call task does not have to be configured to call an external web service, such as a weather service. It can also call back into the Activiti public REST API. This means it could actually call the custom REST API we created in this article.

Table of Contents                   

 

Introduction

The Activiti Enterprise workflow solution supports integration with external databases. It is possible to both read from the database and write to the database during the execution of a process instance.

 

There are two ways in which you can talk to the external database, via automatically generated CRUD operations or via manually coded CRUD operations (note. DELETE is not currently supported):

 

In the above picture data is manipulated in table A via custom coded SQL code. This is needed in situations such as when you have a database table that uses a compound primary key, which is not supported in the auto-generated SQL. It is also needed when you for example have a scenario where you make an INSERT that requires you to update another row to be consistent. Table B on the other hand is managed entirely via auto-generated CRUD operations. And there is no need to write any read or write code.

 

As can be seen in the above picture, there is a specific service task type that should be used when writing to an external database, it is called Store Entity task. Each of these store entity tasks execute in their own transaction, which is important to understand, because if the process instance crashes when executing the “Store Entity B Automatically” task in the above sample, then the database might be in an inconsistent state.

 

As well as writing data to the database it is also possible to set up a user task form to map form fields to database column values. If the form field is mapped to a primary key then it will automatically trigger a read in the database.

 

Activiti uses the concept of Entities internally to map between a logical representation of the data and the physical structure of the database table. An entity does not have to map all columns in a table, only those that are used. And if using automatically generated CRUD, then the statements will be generated accordingly. These internal entities are contained in what’s called a Custom Data Model.

Source Code

Source code for the Activiti Developer Series can be found here.

Prerequisites

Before starting with your Custom Data Model implementation make sure to set up a proper Activiti Extension project.

Implementing a process that integrates with an external database

This section walks you through the steps needed to create a process that uses an external MySQL database to fetch and store data in multiple tables.

 

The tables that we want to work with looks like this:

 

mysql> describe employees;

+------------+---------------+------+-----+---------+-------+

| Field      | Type          | Null | Key | Default | Extra |

+------------+---------------+------+-----+---------+-------+

| emp_no     | int(11)       | NO   | PRI | NULL    |       |

| birth_date | date          | NO   |     | NULL    |       |

| first_name | varchar(14)   | NO   |     | NULL    |       |

| last_name  | varchar(16)   | NO   |     | NULL    |       |

| gender     | enum('M','F') | NO   |     | NULL    |       |

| hire_date  | date          | NO   |     | NULL    |       |

+------------+---------------+------+-----+---------+-------+

6 rows in set (0.14 sec)

 

mysql> describe salaries;

+-----------+---------+------+-----+---------+-------+

| Field     | Type    | Null | Key | Default | Extra |

+-----------+---------+------+-----+---------+-------+

| emp_no    | int(11) | NO   | PRI | NULL    |       |

| salary    | int(11) | NO   |     | NULL    |       |

| from_date | date    | NO   | PRI | NULL    |       |

| to_date   | date    | NO   |     | NULL    |       |

+-----------+---------+------+-----+---------+-------+

4 rows in set (0.00 sec)

 

These tables are part of the MySQL sample employees database that can be downloaded from this site. If you are going to implement this process at the same time as you read this article, then now is the time to download and set up the employees database.

 

Note. there is currently a bug that prevents tables with PKs that are INTs to be stored correctly. So for example, if you have a PostgreSQL employees table with PK defined as:

emp_no int primary key not null,

Then that will not work at the moment, If you have control over the table, alter it so the PK is long.

Overview

The process that we will implement will enable a user to update an employee's first name and last name. It will also be possible to set a new salary as active from a specific date. The process definition looks like this:

The start form will have a field where we can specify the Employee No (the PK into the employees table):

 

 

When the start form is completed Activiti will automatically read the employee’s current first name and last name from the employees table and make these available as process variables. If the Employee No is not found, then the Entity/process variable will be null.

 

The current salary will also be fetched but via code we have to write manually. The Update Employee form will then be displayed:

 

 

The First name and Last name fields are automatically populated. We can then update those if needed. Here we can also specify a new salary and from what date it should be active. When completing this form an Approve Employee Update form is displayed:

 

 

This form shows suggested new data and the old/current data. Clicking Approved in this form starts the update of the two tables via two separate Store Entity Task nodes. Each one executing in its own transaction. Note that a Store Entity Task node can manage only one entity.

 

We will see logging as follows from the custom data model service implementation:

 

08:30:59,898 [http-nio-8080-exec-5] INFO  com.activiti.extension.bean.CustomDataModelServiceImpl  - getMappedValue() EntityDefinition [Name=Salary][TableName=salaries][Id=null][Attributes=4] [fieldName=Employee No] [variableValue=10001]

08:30:59,899 [http-nio-8080-exec-5] INFO  com.activiti.extension.bean.CustomDataModelServiceImpl  - getMappedValue() Response: {"Employee No":10001,"From Date":"2002-06-22T00:00:00Z","To Date":"2016-12-01T00:00:00Z","Salary":88958}

08:30:59,938 [http-nio-8080-exec-5] INFO  com.activiti.extension.bean.CustomDataModelServiceImpl  - getMappedValue() EntityDefinition [Name=Salary][TableName=salaries][Id=null][Attributes=4] [fieldName=Employee No] [variableValue=10001]

08:30:59,940 [http-nio-8080-exec-5] INFO  com.activiti.extension.bean.CustomDataModelServiceImpl  - getMappedValue() Response: {"Employee No":10001,"From Date":"2002-06-22T00:00:00Z","To Date":"2016-12-01T00:00:00Z","Salary":88958}

08:31:00,023 [http-nio-8080-exec-5] INFO  com.activiti.extension.bean.CustomDataModelServiceImpl  - getMappedValue() EntityDefinition [Name=Salary][TableName=salaries][Id=null][Attributes=4] [fieldName=Employee No] [variableValue=10001]

08:31:00,024 [http-nio-8080-exec-5] INFO  com.activiti.extension.bean.CustomDataModelServiceImpl  - getMappedValue() Response: {"Employee No":10001,"From Date":"2002-06-22T00:00:00Z","To Date":"2016-12-01T00:00:00Z","Salary":88958}

08:31:00,024 [http-nio-8080-exec-5] INFO  com.activiti.extension.bean.CustomDataModelServiceImpl  - storeEntity() EntityDefinition [Name=Salary][TableName=salaries][Id=null][Attributes=4] [dataModel=Employee Custom] [attributeDefinitionsAndValues=4]

Tell Activiti about the external database

Before we can do anything with external database tables we need to first configure Activiti with connection parameters for it. So it can do a basic JDBC connection to it. This is done via the Identity Management application (note. you need to be logged in as admin):

 

 

Click on this application so you see this screen:

 

 

Now click on the Tenants menu item, this takes you to this screen:

 

 

Here click on Data sources followed by a click on the + sign to the right to add a new data source:

 

Now, set up the connection parameters for your MySQL employees database. Then click Save. This database configuration can now be used by Activiti when doing the automatic CRUD operations. And it is also used when mapping the entities that will be automatically managed (i.e. the employees table entity).

 

Note. if you are only going to use manually managed entities, such as we plan to do with the salaries table, then you don’t need to set up this data source.

Download and Install the JDBC driver

Whether we use automatically managed entities, like we plan on doing for the employees table, or manually managed entities, like for the salaries table. We always need to download and install the JDBC driver that will be used to talk to the database. This driver will be used both by Activiti auto generated SQL statements and our custom coded SQL statements.

 

Download for example the mysql-connector-java-5.1.40-bin.jar file and put it in the <activiti-install-dir>/tomcat/lib directory.

Creating Custom Data Models and Logical Entities

Before we start creating any process model we need to first sort out the logical entities that we will be using in the process model. They will live in a custom data model. As we are going to use both an automatically managed entity (mapping to the employees table) and a manually managed entity (mapping to the salaries table), it will be required to have one data model per entity. The reason for this is that a data model can only contain either automatically managed entities, referred to as Database Entities, or manually managed entities, referred to as Custom Entities.

Creating the Data Model and Database Entity for the employees table

Let start with the Database entity for the employees table. Click on the Kickstart App application:

 

 

This displays the following screen:

 

 

Then click on the Data Models menu item:

 

 

Now, click on the Create Data Model button in the upper right corner:

 

 

Enter the name and description for the data model and then click the Create new data model button:

 

 

First select the Data source (the one we added earlier on). Then click Add entity to start adding the logical entity that should map to the employees table. Give the entity a logical name, such as Employee and then specify what database table it maps to, in this case employees. Then start mapping the columns in the table.

 

In the above picture we can just see the primary key column mapping. The Required property needs to be checked as the PK does not accept null values. Note also that the Database generated value property doesn't need to be checked since the employees table does not have the emp_no PK set as AUTO_INCREMENT.

 

Add also the following column mappings:

 

For first name:

 

And for last name:

 

And finally the gender column:

 

Note that we have not mapped all columns in the employees table. This is because Activiti currently have a problem with date columns. We will only read and update the emp_no, first_name, last_name, and gender columns in this example. If you need to read and write all columns in a table that has a compound PK, or date columns, then use a Custom Entity instead as we will with the salaries table.

 

Note. if you will be using these entity attribute names when configuring visibility conditions in a form, or in sequence flow conditions, then using spaces in the Attribute name might cause problems, see this jira, and this one.

Creating the Data Model and Custom Entity for the salaries table

Click on the Create Data Model button:

 

Enter the name and description for the data model and then click Create new data model button:

 

 

Now, we are going to manage the CRUD operations for this entity manually, with code we write. So to enable this click the Custom button in the upper left side. This disables the automatic CRUD operations, and the link to the Data source, and sets this entity up to be managed via an implementation of the com.activiti.api.datamodel.AlfrescoCustomDataModelService interface. The implementation class will be called every time that an Activity needs to read or write a Salary entity.

 

We add the attributes that should make up the logical entity in the same way we did for the Employee entity, only difference is that we don’t map them to a table column as was needed when CRUD operations should be created automatically. But we need to match to what we want to store in the salaries table. Continue with the other attributes, first the from and to dates:

 

Then the Salary attribute:

 

This finishes the custom data model definitions. We are now ready to start implementing the process model.

Configuring form and data model mappings for the Start Task

The start form for the start task will be used to collect the unique ID for the employee that should be updated:

 

 

This form is very straightforward with a single number field.

 

The start task also need to have a mapping of this form field to some entity fields:

 

 

Click on the Form field to data model mapping property:

 

Start by mapping the Employee No form field to the Employee entity and its Employee No attribute, which is mapped to the PK in the employees table. Call the process variable that will contain the automatically fetched Employee entity selectedEmployee. We will then be able to refer to this process variable object in future tasks/nodes.

 

Then map the Employee No form field again to the Salary entity and its Employee No attribute, which will be part of the PK to lookup the current salary for the employee in the code that we will write (we will also use current date to compare against from and to date to find current salary):

 

 

That completes the Start Task.

Configuring the form for the Update Employee Task

The form for the update employee task will be used to update the employee's name and salary:

 

 

The form starts with a Display value field at the top that will show currently selected employee no. Then we got two Text fields for updating first and last name. And at the bottom we got a Number field for updating the salary and a Date field to specify from when this new salary should be active. There are also two headers that are not necessary but makes it look a bit nicer.

 

To have the top field show the Employee No for the selected employee we have to configure it a bit:

 

 

Click on the Variable button to the right, this makes all the process variables available, including the auto fetched employee entity. Select to display the selectedEmployee.Employee No attribute of the entity. We can see that the currentSalary entity is also available, but it will not work until we have the backing AlfrescoCustomDataModelService implemented.

 

What we would like also to happen when this form is displayed is that the first and last name fields should be pre-populated with current values. Just before you open a form that you are working on, you should see a screen looking something like this:

 

 

Instead of clicking on the Open button click on the Map Variables > button:

 

Here we can configure what value should be used to pre-populate a form field, and what process variable should contain the form field value when the form is completed. For example, for the First Name field we want it to be pre-populated with the selectedEmployee.First Name source process variable. And when the form is completed we want a new target process variable called newFirstName to contain the value from the form field. We can configure the last name field in the same way.

 

This completes the Update Employee task.

Configuring the form for the Approve Employee Update Task

The Approve Employee task should just show the approver what new names and salary that have been suggested. The approver then clicks Approve to trigger the update to the database, or reject to cancel the update. The form looks like this:

 

This form basically contains a bunch of Display value fields that shows all the updated employee data for the approver. The newFirstName, newLastName, New Salary, and New From Date are all form field values. The others are displayed from variables.

 

For example, newFirstName is configured as follows:

 

 

And an example of a Variable display looks like this:

 

When we have finished configuring all the fields we need to also configure the Approved and Rejected outcomes as follows:

 

Now, while we are at it, Save the form and configure the Approve sequence flow condition as follows:

 

 

 

And then the Rejected flow condition as follows:

 

 

This completes the Approve Employee Update task and the following sequence flows.

Configuring the Update Employee Table Store Entity Task

Now we are ready to store the updated employee information, let’s start with the Employee entity, which will be automatically saved to the employees table via our Date Model database entity mapping.

 

We only need to do a little bit of configuration for this to work, click on the Update Employee Table store entity task:

 

 

Then click on the Attribute mapping property for the store entity task:

 

 

Basically what we need to do here is configure what values we want to use when updating (or creating a new row). First we need to however select the custom data model, which is Employee DB for the automatically managed Employee entity. Then select the Employee entity (there is only one entity defined in the data model so far). Then we click on the Existing variable button as we are doing an UPDATE. And then select what process variable contains existing data such as PK, select selectedEmployee.

 

Now configure/map each Entity Attribute to the value that should be used in the update. We use the existing PK and Gender values, but pick the new first name and last name, which would be form fields when we pick them to the right.

 

That’s it for this Store Entity Task.

Configuring the Update Salary Table Store Entity Task

The last Store Entity task will be used to store the updated salary for the employee. Storing this entity is going to be managed by us manually, we need to do some coding for it to work.

 

But let’s first configure the Attribute mappings so we are clear on what we want to store:

 

 

First select the custom data model, which is Employee Custom for the manually managed Salary entity. Then select the Salary entity (there is only one entity defined in this data model too). Then we click on Existing variable button as we are using values from an existing salary record, basically just the Employee No. And then select what process variable contains existing data such as PK, select currentSalary.

 

Now configure/map each Entity Attribute to the value that should be used in the insert (we are going to create a new entry in the salaries table). We use the existing PK, but pick the new salary and from date, which would be form fields when we pick them to the right. For the to date we want it to be indefinitely until a new salary is added, so we choose Static value to the right and enter ‘9999-01-01’.

 

That’s it for this Store Entity Task. It will not work though until we have implemented next section.

Add code to handle read and write of the custom Salary entity

Now, we have opted to handle the read and write of Salary entities to the database manually. That’s why this entity is configured as a Custom Entity and not mapped to a specific database table in the custom data model.

 

When the Activiti engine needs to read or write a Custom Entity it will call an implementation of the com.activiti.api.datamodel.AlfrescoCustomDataModelService interface. So we need to supply one.

 

Here is one implementation that uses the Spring JDBC template, which is good as it is thread safe and takes care of closing resources etc:

 

package com.activiti.extension.bean;

import com.activiti.api.datamodel.AlfrescoCustomDataModelService;
import com.activiti.model.editor.datamodel.DataModelDefinitionRepresentation;
import com.activiti.model.editor.datamodel.DataModelEntityRepresentation;
import com.activiti.runtime.activiti.bean.datamodel.AttributeMappingWrapper;
import com.activiti.variable.VariableEntityWrapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.apache.commons.dbcp.BasicDataSource;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.simple.SimpleJdbcTemplate;
import org.springframework.stereotype.Service;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Service
public class CustomDataModelServiceImpl implements AlfrescoCustomDataModelService {
  private static Logger logger = LoggerFactory.getLogger(CustomDataModelServiceImpl.class);

  /**
   * Database table names
   */

  private static final String SALARIES_TABLE_NAME = "salaries";

  /**
   * Mapping entity into JSON
   */

  @Autowired
  protected ObjectMapper objectMapper;

  /**
   * Use Spring JDBC Template for database access
   */

  private SimpleJdbcTemplate jdbcTemplate;

  /**
   * Salary Data Transfer Object (DTO)
   */

  private class Salary {
      private long empNo;
      private Date fromDate;
      private Date toDate;
      private long salary;

      public long getEmpNo() { return empNo; }
      public void setEmpNo(long empNo) { this.empNo = empNo; }
      public Date getFromDate() { return fromDate; }
      public void setFromDate(Date fromDate) { this.fromDate = fromDate; }
      public Date getToDate() { return toDate; }
      public void setToDate(Date toDate) { this.toDate = toDate; }
      public long getSalary() { return salary; }
      public void setSalary(long salary) { this.salary = salary; }
  }

  /**
   * Salary Spring JDBC Row Mapper
   */

  class SalaryRowMapper implements RowMapper {
      public Object mapRow(ResultSet rs, int rowNum) throws SQLException {
          Salary salary = new Salary();
          salary.setEmpNo(rs.getLong("emp_no"));
          salary.setFromDate(rs.getDate("from_date"));
          salary.setToDate(rs.getDate("to_date"));
          salary.setSalary(rs.getLong("salary"));

          return salary;
      }
  }

  /**
   * CREATE USER employees@localhost IDENTIFIED BY '1234';
   * GRANT ALL PRIVILEGES ON employees.* TO employees@localhost IDENTIFIED BY '1234';
   * FLUSH PRIVILEGES;
   */

  public CustomDataModelServiceImpl() {
      BasicDataSource ds = new BasicDataSource();
      ds.setDriverClassName("com.mysql.jdbc.Driver");
      ds.setUrl("jdbc:mysql://localhost:3306/employees");
      ds.setUsername("employees");
      ds.setPassword("1234");

      jdbcTemplate = new SimpleJdbcTemplate(ds);
  }

  /**
   * This method is called when Activiti wants to fetch a row from the database table
   * that has been mapped as a "Custom" entity.
   *
   * @param entityDefinition the definition of the "custom" entity that was mapped in the Custom Data Model (e.g. Salary)
   * @param fieldName the entity field that represents the PK (e.g. Employee No)
   * @param fieldValue the entity field value (e.g. 10001)
   * @return an object representing the fetched data
   */

  @Override
  public ObjectNode getMappedValue(DataModelEntityRepresentation entityDefinition,
                                   String fieldName, Object fieldValue) {
      logger.info("getMappedValue() EntityDefinition [Name=" + entityDefinition.getName() +
              "][TableName=" + SALARIES_TABLE_NAME +
              "][Id=" + entityDefinition.getId() +
              "][Attributes=" + entityDefinition.getAttributes().size() +
              "] [fieldName=" + fieldName +
              "] [variableValue=" + fieldValue + "]");

      // Check if are to get something from the salaries table
      if (StringUtils.equals(entityDefinition.getTableName(), SALARIES_TABLE_NAME)) {
          // Fetch the Salary row we are looking for
          Long employeeNo = (Long) fieldValue;
          Date currentDate = new Date();

          String sql = "SELECT * FROM " + SALARIES_TABLE_NAME +
                  " WHERE emp_no = ? and from_date <= ? and to_date > ?";

          Salary salary = (Salary) jdbcTemplate.queryForObject(
                  sql, new SalaryRowMapper(), new Object[]{employeeNo, currentDate, currentDate});

          // The following fields have to match the attributes set up in the Custom Data Model
          // and what is used in the form that will display them
          SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); // The expected entity date format is ISO-8601
          ObjectNode fetchedSalaryRowAsJSON = objectMapper.createObjectNode();
          fetchedSalaryRowAsJSON.put("Employee No", salary.getEmpNo());
          fetchedSalaryRowAsJSON.put("From Date", sdf.format(salary.getFromDate()) + "Z"); // see https://github.com/FasterXML/jackson-databind/issues/338
          fetchedSalaryRowAsJSON.put("To Date", sdf.format(salary.getToDate()) + "Z");
          fetchedSalaryRowAsJSON.put("Salary", salary.getSalary());

          logger.info("getMappedValue() Response: " + fetchedSalaryRowAsJSON.toString());

          return fetchedSalaryRowAsJSON;
      }

      return null;
  }

  @Override
  public VariableEntityWrapper getVariableEntity(String keyValue, String variableName,
                                                 String processDefinitionId,
                                                 DataModelEntityRepresentation entityValue) {
      logger.info("getVariableEntity() Entity [Name=" + entityValue.getName() +
              "][TableName=" + SALARIES_TABLE_NAME + "][Id=" + entityValue.getId() +
              "][Attributes=" + entityValue.getAttributes().size() +
              "] [keyValue=" + keyValue +
              "] [variableName=" + variableName + "]");

      return null;
  }

  /**
   * This method is called when Activiti wants to store a "Custom" entity in the
   * database.
   *
   * @param attributeDefinitionsAndValues attributes that will become the column values
   * @param entityDefinition the definition of the "custom" entity that was mapped in the Custom Data Model (e.g. Salary)
   * @param dataModel the custom data model that contains the "custom" entity definition
   * @return
   */

  @Override
  public String storeEntity(List<AttributeMappingWrapper> attributeDefinitionsAndValues,
                            DataModelEntityRepresentation entityDefinition,
                            DataModelDefinitionRepresentation dataModel) {
      logger.info("storeEntity() EntityDefinition [Name=" + entityDefinition.getName() +
              "][TableName=" + SALARIES_TABLE_NAME + "][Id=" + entityDefinition.getId() +
              "][Attributes=" + entityDefinition.getAttributes().size() +
              "] [dataModel=" + dataModel.getName() +
              "] [attributeDefinitionsAndValues=" + attributeDefinitionsAndValues.size() + "]");

      // Check if we are to store something in the salaries table
      if (StringUtils.equals(entityDefinition.getTableName(), SALARIES_TABLE_NAME)) {
          // Set up a map of all the column names and values
          Map<String, Object> parameters = new HashMap<String, Object>();
          for (AttributeMappingWrapper attributeMappingWrapper : attributeDefinitionsAndValues) {
              // Get the column name = mapped name
              // And the column value = attr value
              parameters.put(attributeMappingWrapper.getAttribute().getMappedName(),
                      attributeMappingWrapper.getValue());
          }

          // Update current salary entry to previous
          SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
          String sql = "UPDATE " + SALARIES_TABLE_NAME +
                  " SET to_date = '" +  sdf.format(parameters.get("from_date")) + "'" +
                  " WHERE emp_no = " + parameters.get("emp_no") +
                  " AND to_date = '9999-01-01'";
          jdbcTemplate.update(sql);

          // Insert new salary entry
          sql = "INSERT INTO " + SALARIES_TABLE_NAME +
                  " (emp_no, from_date, to_date, salary) VALUES (:emp_no, :from_date, :to_date, :salary)";
          jdbcTemplate.update(sql, parameters);
      }

      return null;
  }
}

 

Note that this class is in the automatically scanned package com.activiti.extension.bean so it will be picked up as a bean as it is annotated with @Service.

 

The getMappedValue() method is called by Activiti when it wants to read and populate a Salary entity, or any other custom entity, so we need to check that it wants to fetch a Salary entity. Passed in is primary key information in the fieldValue. Because the salaries table have a compound PK we use current date to compare with from_to and to_date to get one row in the response. There is also, as mentioned, some date problems that we work around.

 

When Activiti wants to store a Salary entity (i.e. when we get to the Update Salary Table task) it will call the storeEntity() method. It will call this method for all custom entities that should be stored, so we need to check that it is the Salary entity that should be stored. Here we do a bit more and also update current latest salaries entry before inserting a new row. So this is quite useful, we can do more than one operation when a store is initiated, we have full control.

 

For this to build you need to also add the following dependency in the extension project pom.xml:

 

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-jdbc</artifactId>
  <version>${spring.version}</version>
  <scope>provided</scope>
</dependency>

Table of Contents                   

 

Introduction

Listeners are an Activiti extension to the BPMN 2.0 standard that implement hook points inside a process definition. These are triggered by events during workflow execution. There are two type of listeners, task and execution.

 

Execution listeners can be configured on the process itself, as well as on activities and on transitions. Task listeners can only be configured on user tasks.

 

Listeners enable you to run code during the execution of the process instance. This can be script code or a call to a Java class. The following diagram shows the events in a process definition where you can configure a listener:

 

This picture also illustrates the order in which the event listeners will be invoked. Note for example that an assign task listener is invoked before a create task listener, which might be a bit surprising.

 

Listeners are described in more detail in the Activiti user guide, see the execution listeners docs and the task listeners docs.

Source Code

Source code for the Activiti Developer Series can be found here.

Prerequisites

Before starting with your listener implementation make sure to set up a proper Activiti Extension project.

Implementing Java backed Listeners

Let’s implement one execution listener and one task listener in Java and configure them for all the events shown for the process definition in the introduction section. The listeners will not do much more than print a log message.

Coding the Java classes

This section goes through how to implement both the execution and the task listener.

Implementing the Execution Listener

Here is the implementation of the Execution Listener class:

 

package com.activiti.extension.bean;

import org.activiti.engine.delegate.DelegateExecution;
import org.activiti.engine.delegate.ExecutionListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class HelloWorldExecutionListener implements ExecutionListener {
  private static Logger logger = LoggerFactory.getLogger(HelloWorldExecutionListener.class);

  @Override
  public void notify(DelegateExecution execution) throws Exception {
      logger.info("[Process=" + execution.getProcessInstanceId() +
                  "][event=" + execution.getEventName() +
                  "][ExecutionListener=" + this +
                  "][ActivityId=" + execution.getCurrentActivityId() + "]");
  }
}

 

The class has been created in the com.activiti.extension.bean package, but it does not have to be located in this package. It is just good practice to put the class in this package if it ever needs to be converted to a Spring Bean backed listener in the future. Then this package is scanned automatically by Activiti and any beans are instantiated.

 

An Execution Listener needs to implement the org.activiti.engine.delegate.ExecutionListener interface, which contains the notify method. This methods takes one parameter of type org.activiti.engine.delegate.DelegateExecution, which is how we get access to the process instance that is invoking this Listener. An Execution Listener object instance is shared between process instances, so the DelegateExecution parameter is our way of finding out information about the active process instance.

 

Notice how we use an external class here for logging and that the logger.info method could be called concurrently by multiple threads. So it needs to be thread safe, which it is. Same thing applies for other class members that we define and use in the notify method. We can do whatever we want inside the notify method as all threads have independent call stacks.

Implementing the Task Listener

Here is the implementation of the Task Listener class:

 

package com.activiti.extension.bean;

import org.activiti.engine.delegate.DelegateExecution;
import org.activiti.engine.delegate.DelegateTask;
import org.activiti.engine.delegate.TaskListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class HelloWorldTaskListener implements TaskListener {
  private static Logger logger = LoggerFactory.getLogger(HelloWorldTaskListener.class);

  @Override
  public void notify(DelegateTask task) {
      DelegateExecution execution = task.getExecution();

      logger.info("[Process=" + execution.getProcessInstanceId() +
                  "][event=" + task.getEventName() +
                  "][TaskListener=" + this +
                  "][ActivityId=" + execution.getCurrentActivityId() +
                  "][TaskAssignee=" + task.getAssignee() +
                  "][TaskForm=" + task.getFormKey() + "]");
  }
}

 

The class has been created in the com.activiti.extension.bean package, but it does not have to be located in this package. It is just good practice to put the class in this package if it ever needs to be converted to a Spring Bean backed listener in the future. Then this package is scanned automatically by Activiti and any beans are instantiated.

 

A Task Listener needs to implement the org.activiti.engine.delegate.TaskListener interface, which contains the notify method. This methods takes one parameter of type org.activiti.engine.delegate.DelegateTask, which is how we get access to the user task instance that is invoking this Listener. We can also get to the process execution via the task.getExecution method.

 

Via the passed in DelegateTask object you can also get to more details around the user task, such as who it is assigned to, its description, event, when it was created, what form it is using etc.

 

A Task Listener object instance is shared between process instances, so the DelegateTask parameter is our way of finding out information about the active process instance and user task instance.

 

Note. Task Listeners work only on User tasks, they will have no effect on for example a Service Task.

Testing the Listeners

Now to test the Listener implementations create a process model looking like this:

 

 

Then set up the listeners as follows, start by clicking on the canvas (not on any activity or event) so you can set the process level execution listener:

 

 

Clicking on the No execution listeners configured property brings up the following dialog:

 

 

Add the com.activiti.extension.bean.HelloWorldExecutionListener implementation for the start and end events. Then click on the User Task 1 activity:

 

 

Clicking on the No execution listeners configured property brings up the following dialog where we can set up listener for start and end event for activity User Task 1:

 

 

Save the execution listener configuration and then click on the task listeners property:

 

This bring up a dialog where we can configure task listeners for the different events:

 

Here we are using the com.activiti.extension.bean.HelloWorldTaskListener implementation. We can also see a new task event called delete that we can attach a task listener to, it will be invoked when the task instance is completed and deleted.

 

Now, for the sequence flow between the User Task 1 and the Simple Script Task set up an execution listener on the take event. First, click on the transition:

 

 

Then click on the No execution listeners configured property, which brings up the Execution Listener configuration dialog:

 

 

Use the com.activiti.extension.bean.HelloWorldExecutionListener implementation for the take event.

 

For the Simple Script Task set up execution listeners for the start and end event:

 

 

The configuration will look like this:

 

 

We also need to configure a script and script format for the Script task as it currently is failing validation:

 

 

Add a simple Groovy script that just prints a log message:

 

 

In BPMN 2.0 XML this will look like:

 

<process id="TestingListeners" name="Testing Listeners" isExecutable="true">
  <extensionElements>
      <activiti:executionListener event="start"
          class="com.activiti.extension.bean.HelloWorldExecutionListener"/>

      <activiti:executionListener event="end"
          class="com.activiti.extension.bean.HelloWorldExecutionListener"/>

  </extensionElements>

  <startEvent id="startEvent" name="Start Event"></startEvent>

  <sequenceFlow id="transitionStart2UserTask"
                sourceRef="startEvent"
                targetRef="userTask1">
</sequenceFlow>

  <userTask id="userTask1" name="User Task 1" activiti:assignee="$INITIATOR">
      <extensionElements>
          <activiti:executionListener event="start"
              class="com.activiti.extension.bean.HelloWorldExecutionListener"/>

          <activiti:executionListener event="end"
              class="com.activiti.extension.bean.HelloWorldExecutionListener"/>

          <activiti:taskListener event="create"
              class="com.activiti.extension.bean.HelloWorldTaskListener"/>

          <activiti:taskListener event="assignment"
              class="com.activiti.extension.bean.HelloWorldTaskListener"/>

          <activiti:taskListener event="complete"
              class="com.activiti.extension.bean.HelloWorldTaskListener"/>

          <activiti:taskListener event="delete"
              class="com.activiti.extension.bean.HelloWorldTaskListener"/>

      </extensionElements>
  </userTask>

  <sequenceFlow id="transitionUserTask2ScriptTask"
                sourceRef="userTask1"
                targetRef="scriptTask">

      <extensionElements>
          <activiti:executionListener event="take"
              class="com.activiti.extension.bean.HelloWorldExecutionListener"/>

      </extensionElements>
  </sequenceFlow>

  <scriptTask id="scriptTask" name="Simple Script Task"
              scriptFormat="groovy"
              activiti:autoStoreVariables="false">

      <extensionElements>
          <activiti:executionListener event="start"
              class="com.activiti.extension.bean.HelloWorldExecutionListener"/>

          <activiti:executionListener event="end"
              class="com.activiti.extension.bean.HelloWorldExecutionListener"/>

      </extensionElements>
      <script><![CDATA[print "Hello from Script Task"]]></script>
  </scriptTask>

  <sequenceFlow id="transitionScriptTask2EndEvent"
                sourceRef="scriptTask"
                targetRef="endEvent">
</sequenceFlow>

  <endEvent id="endEvent"></endEvent>
</process>

 

Note that the process definition has been updated so each element has a more descriptive ID. This will make it easier to see what is going on when we do logging. It is also easier to read the process definition.

 

When we start this process it will print similar logs to the following:

 

08:01:52,361 [http-nio-8080-exec-5] INFO  com.activiti.extension.bean.HelloWorldExecutionListener  - [Process=132501][event=start][ExecutionListener=com.activiti.extension.bean.HelloWorldExecutionListener@47c496c6][ActivityId=startEvent]

08:01:52,368 [http-nio-8080-exec-5] INFO  com.activiti.extension.bean.HelloWorldExecutionListener  - [Process=132501][event=start][ExecutionListener=com.activiti.extension.bean.HelloWorldExecutionListener@4dcb7e48][ActivityId=userTask1]

08:01:52,378 [http-nio-8080-exec-5] INFO  com.activiti.extension.bean.HelloWorldTaskListener  - [Process=132501][event=assignment][TaskListener=com.activiti.extension.bean.HelloWorldTaskListener@3df0a8c4][ActivityId=userTask1][TaskAssignee=1][TaskForm=null]

08:01:52,379 [http-nio-8080-exec-5] INFO  com.activiti.extension.bean.HelloWorldTaskListener  - [Process=132501][event=create][TaskListener=com.activiti.extension.bean.HelloWorldTaskListener@5cac7332][ActivityId=userTask1][TaskAssignee=1][TaskForm=null]

 

Here we can see that the execution listener set on the process level is invoked first when we get the process start event. Then we get the execution listener invoked again on the task start event. Then it moves on with task assignment and create event, invoking the task listener. All as expected. What might be unexpected is that there is actually one listener instance created for each event it is registered on. So here we have four unique listener objects (i.e. 47c496c6, 4dcb7e48, 3df0a8c4, and 5cac7332).

 

When we complete the User Task 1 the following logs are printed:

 

08:08:01,566 [http-nio-8080-exec-7] INFO  com.activiti.extension.bean.HelloWorldTaskListener  - [Process=132501][event=complete][TaskListener=com.activiti.extension.bean.HelloWorldTaskListener@1d06918c][ActivityId=userTask1][TaskAssignee=1][TaskForm=null]

08:08:01,567 [http-nio-8080-exec-7] INFO  com.activiti.extension.bean.HelloWorldTaskListener  - [Process=132501][event=delete][TaskListener=com.activiti.extension.bean.HelloWorldTaskListener@59681d0f][ActivityId=userTask1][TaskAssignee=1][TaskForm=null]

08:08:01,573 [http-nio-8080-exec-7] INFO  com.activiti.extension.bean.HelloWorldExecutionListener  - [Process=132501][event=end][ExecutionListener=com.activiti.extension.bean.HelloWorldExecutionListener@1687fdbb][ActivityId=userTask1]

08:08:01,577 [http-nio-8080-exec-7] INFO  com.activiti.extension.bean.HelloWorldExecutionListener  - [Process=132501][event=take][ExecutionListener=com.activiti.extension.bean.HelloWorldExecutionListener@4e282041][ActivityId=userTask1]

08:08:01,578 [http-nio-8080-exec-7] INFO  com.activiti.extension.bean.HelloWorldExecutionListener  - [Process=132501][event=start][ExecutionListener=com.activiti.extension.bean.HelloWorldExecutionListener@7224678f][ActivityId=scriptTask]

08:08:02,281 [http-nio-8080-exec-7] INFO  com.activiti.extension.bean.HelloWorldExecutionListener  - [Process=132501][event=end][ExecutionListener=com.activiti.extension.bean.HelloWorldExecutionListener@65230b79][ActivityId=scriptTask]

08:08:02,282 [http-nio-8080-exec-7] INFO  com.activiti.extension.bean.HelloWorldExecutionListener  - [Process=132501][event=end][ExecutionListener=com.activiti.extension.bean.HelloWorldExecutionListener@24183909][ActivityId=endEvent]

 

As expected, we can see the task complete and delete events, followed by the end of user task. Then we get the invocation of the execution listener for the take event on the transition. Then we get the execution listener invoked for the start and end event of the script task activity. And finally the end event for the whole process.

 

Note again that each registration of a listener will get its own listener instance during runtime. Let’s see what happens if we start two processes in parallel, without completing the User task. Here is the logs from process one:

 

08:13:43,809 [http-nio-8080-exec-7] INFO  com.activiti.extension.bean.HelloWorldExecutionListener  - [Process=132509][event=start][ExecutionListener=com.activiti.extension.bean.HelloWorldExecutionListener@47c496c6][ActivityId=startEvent]

08:13:43,811 [http-nio-8080-exec-7] INFO  com.activiti.extension.bean.HelloWorldExecutionListener  - [Process=132509][event=start][ExecutionListener=com.activiti.extension.bean.HelloWorldExecutionListener@4dcb7e48][ActivityId=userTask1]

08:13:43,812 [http-nio-8080-exec-7] INFO  com.activiti.extension.bean.HelloWorldTaskListener  - [Process=132509][event=assignment][TaskListener=com.activiti.extension.bean.HelloWorldTaskListener@3df0a8c4][ActivityId=userTask1][TaskAssignee=1][TaskForm=null]

08:13:43,812 [http-nio-8080-exec-7] INFO  com.activiti.extension.bean.HelloWorldTaskListener  - [Process=132509][event=create][TaskListener=com.activiti.extension.bean.HelloWorldTaskListener@5cac7332][ActivityId=userTask1][TaskAssignee=1][TaskForm=null]

 

And then we start the second process:

 

08:15:00,808 [http-nio-8080-exec-9] INFO  com.activiti.extension.bean.HelloWorldExecutionListener  - [Process=132515][event=start][ExecutionListener=com.activiti.extension.bean.HelloWorldExecutionListener@47c496c6][ActivityId=startEvent]

08:15:00,809 [http-nio-8080-exec-9] INFO  com.activiti.extension.bean.HelloWorldExecutionListener  - [Process=132515][event=start][ExecutionListener=com.activiti.extension.bean.HelloWorldExecutionListener@4dcb7e48][ActivityId=userTask1]

08:15:00,810 [http-nio-8080-exec-9] INFO  com.activiti.extension.bean.HelloWorldTaskListener  - [Process=132515][event=assignment][TaskListener=com.activiti.extension.bean.HelloWorldTaskListener@3df0a8c4][ActivityId=userTask1][TaskAssignee=1][TaskForm=null]

08:15:00,811 [http-nio-8080-exec-9] INFO  com.activiti.extension.bean.HelloWorldTaskListener  - [Process=132515][event=create][TaskListener=com.activiti.extension.bean.HelloWorldTaskListener@5cac7332][ActivityId=userTask1][TaskAssignee=1][TaskForm=null]

 

Important thing to be aware of here is that listener instances will be re-used between processes.

Listeners and Thread Safety

When using execution and task listeners, things are not thread safe even if each event the listener is registered for results in a unique listener instance. This is because listener instances are shared between process instances. This could potentially lead to library code used in the listener implementation being called concurrently, and as a result it needs to be thread-safe, as the log library we used in our example. Same thing for any class member variables, access needs to be synchronized.

 

So use thread-safe libraries and avoid class members.

Using Process Variables in a Listener

Most of the time when you implement a listener you would want to use some process variable values. Process variables are typically set when the process instance is started, through the API, or by different activities in the process. They are stored in the database for each process instance.

 

So how do you pass these process variable values into the listener? It is not necessary to pass any value, you can access all process instance variables via the DelegateExecution object, or the DelegateTask object, and the getVariable method.

 

What variables that are available in a listener depends on if it is an execution listener or a task listener. So let’s talk about them separately.

Using Process Variables in an Execution Listener

This is very similar to how you read and write process variables in a Java Delegate, read the docs here.

 

Let’s say we got the following execution listener implementation:

 

public void notify(DelegateExecution execution) throws Exception {
  logger.info("[Process=" + execution.getProcessInstanceId() +
              "][event=" + execution.getEventName() +
              "][ExecutionListener=" + this +
              "][ActivityId=" + execution.getCurrentActivityId() + "]");

  String initiator = (String)execution.getVariable("initiator");
  logger.info("Initiator of the process has user ID = " + initiator);

  execution.setVariable("greeting1Proc", "Hello World!");
  execution.setVariableLocal("greeting1ProcLocal", "Hello World Local!");

  logger.info("--- Process variables:");
  Map<String, Object> procVars = execution.getVariables();
  for (Map.Entry<String, Object> procVar : procVars.entrySet()) {
      logger.info("   [" + procVar.getKey() + " = " + procVar.getValue() + "]");
  }
}

 

It will result in the following log messages if we look at for example the output from the execution listener attached to the process start event:

 

09:10:02,994 [http-nio-8080-exec-5] INFO  com.activiti.extension.bean.HelloWorldExecutionListener  - [Process=140005][event=start][ExecutionListener=com.activiti.extension.bean.HelloWorldExecutionListener@25c1060a][ActivityId=startEvent]

09:10:02,994 [http-nio-8080-exec-5] INFO  com.activiti.extension.bean.HelloWorldExecutionListener  - Initiator of the process has user ID = 1

09:10:02,994 [http-nio-8080-exec-5] INFO  com.activiti.extension.bean.HelloWorldExecutionListener  - --- Process variables:

09:10:02,994 [http-nio-8080-exec-5] INFO  com.activiti.extension.bean.HelloWorldExecutionListener  -    [greeting1Proc = Hello World!]

09:10:02,994 [http-nio-8080-exec-5] INFO  com.activiti.extension.bean.HelloWorldExecutionListener  -    [greeting1ProcLocal = Hello World Local!]

09:10:02,994 [http-nio-8080-exec-5] INFO  com.activiti.extension.bean.HelloWorldExecutionListener  -    [initiator = 1]

Using Process Variables in a Task Listener

When it comes to using process variables in a task listener we have to take into account a new scope. The user task is created in a sub-scope to the process instance. So if we got process variables, will they get propagated to the task scope, yes they will, automatically.

 

Take the following task listener implementation:

 

public void notify(DelegateTask task) {
  DelegateExecution execution = task.getExecution();

  logger.info("[Process=" + execution.getProcessInstanceId() +
              "][event=" + task.getEventName() +
              "][TaskListener=" + this +
              "][ActivityId=" + execution.getCurrentActivityId() +
              "][TaskAssignee=" + task.getAssignee() +
              "][TaskForm=" + task.getFormKey() + "]");

  String initiator = (String)execution.getVariable("initiator");
  logger.info("Initiator of the process has user ID = " + initiator);

  execution.setVariable("greeting2Proc", "Hello World!");
  execution.setVariableLocal("greeting2ProcLocal", "Hello World Local!");
  task.setVariable("greetingTask", "Hello World!");
  task.setVariableLocal("greetingTaskLocal", "Hello World Local!");

  logger.info("--- Process variables:");
  Map<String, Object> procVars = execution.getVariables();
  for (Map.Entry<String, Object> procVar : procVars.entrySet()) {
      logger.info("   [" + procVar.getKey() + " = " + procVar.getValue() + "]");
  }

  logger.info("--- Task variables:");
  Map<String, Object> taskVars = task.getVariables();
  for (Map.Entry<String, Object> taskVar : taskVars.entrySet()) {
      logger.info("   [" + taskVar.getKey() + " = " + taskVar.getValue() + "]");
  }
}

 

This will print a log looking something like this for the user task assignment event:

 

09:10:03,006 [http-nio-8080-exec-5] INFO  com.activiti.extension.bean.HelloWorldTaskListener  - [Process=140005][event=assignment][TaskListener=com.activiti.extension.bean.HelloWorldTaskListener@62afbe29][ActivityId=userTask1][TaskAssignee=1][TaskForm=null]

09:10:03,006 [http-nio-8080-exec-5] INFO  com.activiti.extension.bean.HelloWorldTaskListener  - Initiator of the process has user ID = 1

09:10:03,008 [http-nio-8080-exec-5] INFO  com.activiti.extension.bean.HelloWorldTaskListener  - --- Process variables:

09:10:03,008 [http-nio-8080-exec-5] INFO  com.activiti.extension.bean.HelloWorldTaskListener  -    [greeting1Proc = Hello World!]

09:10:03,008 [http-nio-8080-exec-5] INFO  com.activiti.extension.bean.HelloWorldTaskListener  -    [greeting1ProcLocal = Hello World Local!]

09:10:03,008 [http-nio-8080-exec-5] INFO  com.activiti.extension.bean.HelloWorldTaskListener  -    [greeting2Proc = Hello World!]

09:10:03,008 [http-nio-8080-exec-5] INFO  com.activiti.extension.bean.HelloWorldTaskListener  -    [initiator = 1]

09:10:03,008 [http-nio-8080-exec-5] INFO  com.activiti.extension.bean.HelloWorldTaskListener  -    [greetingTask = Hello World!]

09:10:03,008 [http-nio-8080-exec-5] INFO  com.activiti.extension.bean.HelloWorldTaskListener  -    [greeting2ProcLocal = Hello World Local!]

09:10:03,008 [http-nio-8080-exec-5] INFO  com.activiti.extension.bean.HelloWorldTaskListener  - --- Task variables:

09:10:03,008 [http-nio-8080-exec-5] INFO  com.activiti.extension.bean.HelloWorldTaskListener  -    [greeting1Proc = Hello World!]

09:10:03,008 [http-nio-8080-exec-5] INFO  com.activiti.extension.bean.HelloWorldTaskListener  -    [greeting1ProcLocal = Hello World Local!]

09:10:03,008 [http-nio-8080-exec-5] INFO  com.activiti.extension.bean.HelloWorldTaskListener  -    [greeting2Proc = Hello World!]

09:10:03,008 [http-nio-8080-exec-5] INFO  com.activiti.extension.bean.HelloWorldTaskListener  -    [greetingTaskLocal = Hello World Local!]

09:10:03,008 [http-nio-8080-exec-5] INFO  com.activiti.extension.bean.HelloWorldTaskListener  -    [initiator = 1]

09:10:03,008 [http-nio-8080-exec-5] INFO  com.activiti.extension.bean.HelloWorldTaskListener  -    [greetingTask = Hello World!]

09:10:03,008 [http-nio-8080-exec-5] INFO  com.activiti.extension.bean.HelloWorldTaskListener  -    [greeting2ProcLocal = Hello World Local!]

 

Let’s start with process scoped variables, we can see that we got all the variables we set available in the process scope, including variables such as greeting1Proc that has been set by previous execution listeners and variables such as greetingTask that has been set in task scope. However, there is one variable missing, the greetingTaskLocal. This is because the task instance is created in a sub-scope to the process scope, and when you use task.setVariableLocal("greetingTaskLocal", "Hello World Local!"), it will set this variable only in the task scope. A local task variable will not be propagated to the process scope and it will be gone when the task instance is deleted.

 

Now let’s see what variables we got available in the task scope, the “task variables”. We can see that we got all the process scoped variables (i.e. greeting1Proc, greeting2Proc, greeting1Local, and greeting2Local) as they are automatically propagated through to the task scope when the task instance is created. Besides the greetingTask variable we can also see the local task variable greetingTaskLocal.

 

Now let’s see what process variables will be available when we get to the script task. Let’s complete the user task and we should then see the following variables when we enter the start event for the script task:

 

09:30:21,630 [http-nio-8080-exec-1] INFO  com.activiti.extension.bean.HelloWorldExecutionListener  - [Process=140005][event=start][ExecutionListener=com.activiti.extension.bean.HelloWorldExecutionListener@5b12a6e][ActivityId=scriptTask]

09:30:21,630 [http-nio-8080-exec-1] INFO  com.activiti.extension.bean.HelloWorldExecutionListener  - Initiator of the process has user ID = 1

09:30:21,633 [http-nio-8080-exec-1] INFO  com.activiti.extension.bean.HelloWorldExecutionListener  - - Process variables:

09:30:21,633 [http-nio-8080-exec-1] INFO  com.activiti.extension.bean.HelloWorldExecutionListener  -    [greeting1Proc = Hello World!]

09:30:21,633 [http-nio-8080-exec-1] INFO  com.activiti.extension.bean.HelloWorldExecutionListener  -    [greeting1Local = Hello World Local!]

09:30:21,633 [http-nio-8080-exec-1] INFO  com.activiti.extension.bean.HelloWorldExecutionListener  -    [greeting2Proc = Hello World!]

09:30:21,633 [http-nio-8080-exec-1] INFO  com.activiti.extension.bean.HelloWorldExecutionListener  -    [initiator = 1]

09:30:21,633 [http-nio-8080-exec-1] INFO  com.activiti.extension.bean.HelloWorldExecutionListener  -    [greetingTask = Hello World!]

09:30:21,633 [http-nio-8080-exec-1] INFO  com.activiti.extension.bean.HelloWorldExecutionListener  -    [greeting2ProcLocal = Hello World Local!]

 

We can see that we got pretty much all variables we have set available, the only one that is not available any more is the greetingTaskLocal as it was set locally in the user task scope. The reason we can see the greetingTask variable here is that when it was set in the task scope with task.setVariable, this method will actually navigate up the scope hierarchy until it reaches a root scope and set it there, or overwrite any local variable with the same name on the way.

Using Field Injection in a Listener

This works in the same way as for service task POJO Java Delegates. See docs.

Making the Listener a Spring Bean

This works in the same way as for service task Spring Bean Java Delegates. See docs.

 

A “springified” execution listener looks like this:

 

package com.activiti.extension.bean;

import org.activiti.engine.delegate.DelegateExecution;
import org.activiti.engine.delegate.ExecutionListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.util.Map;

@Component("helloWorldExecutionListener")
public class HelloWorldSpringExecutionListener implements ExecutionListener {
  private static Logger logger = LoggerFactory.getLogger(HelloWorldSpringExecutionListener.class);

  @Override
  public void notify(DelegateExecution execution) throws Exception {
      logger.info("[Process=" + execution.getProcessInstanceId() +
                  "][event=" + execution.getEventName() +
                  "][ExecutionListener=" + this +
                  "][ActivityId=" + execution.getCurrentActivityId() + "]");
  }
}

 

And it is used as in the following example:

 

 

And the BPMN 2.0 XML for this will look like:

 

<sequenceFlow id="transitionStart2UserTask" sourceRef="startEvent" targetRef="userTask1">
  <extensionElements>
      <activiti:executionListener
         event="take"
         delegateExpression="${helloWorldExecutionListener}"/>

  </extensionElements>
</sequenceFlow>

 

And in a similar way we can easily turn a task listener into a Spring bean:

 

package com.activiti.extension.bean;

import org.activiti.engine.delegate.DelegateExecution;
import org.activiti.engine.delegate.DelegateTask;
import org.activiti.engine.delegate.TaskListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.util.Map;

@Component("helloWorldTaskListener")
public class HelloWorldSpringTaskListener implements TaskListener {
  private static Logger logger = LoggerFactory.getLogger(HelloWorldSpringTaskListener.class);

  @Override
  public void notify(DelegateTask task) {
      DelegateExecution execution = task.getExecution();

      logger.info("[Process=" + execution.getProcessInstanceId() +
                  "][event=" + task.getEventName() +
                  "][TaskListener=" + this +
                  "][ActivityId=" + execution.getCurrentActivityId() +
                  "][TaskAssignee=" + task.getAssignee() +
                  "][TaskForm=" + task.getFormKey() + "]");
  }
}

 

Note. as mentioned before, these classes need to be located in the com.activiti.extension.bean package to be picked up by Activiti bean scanning.

 

And it is used as in the following example, click on the User task and then on the task listeners property:

 

 

And the BPMN 2.0 XML for this will look like:

 

<userTask id="userTask1" name="User Task 1" activiti:assignee="$INITIATOR">
  <extensionElements>
      <activiti:executionListener event="start"
          class="com.activiti.extension.bean.HelloWorldExecutionListener"/>

      <activiti:executionListener event="end"
          class="com.activiti.extension.bean.HelloWorldExecutionListener"/>


      <activiti:taskListener event="create"
          delegateExpression="${helloWorldTaskListener}"/>


      <activiti:taskListener event="assignment"
          class="com.activiti.extension.bean.HelloWorldTaskListener"/>

      <activiti:taskListener event="complete"
          class="com.activiti.extension.bean.HelloWorldTaskListener"/>

      <activiti:taskListener event="delete"
          class="com.activiti.extension.bean.HelloWorldTaskListener"/>

  </extensionElements>
</userTask>

 

If we were to change all our listeners that we have configured for the process into Spring bean based ones. Then we would have logs looking like this. Starting process one, not completing user task 1:

 

10:37:09,980 [http-nio-8080-exec-8] INFO  com.activiti.extension.bean.HelloWorldSpringExecutionListener  - [Process=142551][event=start][ExecutionListener=com.activiti.extension.bean.HelloWorldSpringExecutionListener@2a72c0a6][ActivityId=startEvent]

10:37:09,981 [http-nio-8080-exec-8] INFO  com.activiti.extension.bean.HelloWorldSpringExecutionListener  - [Process=142551][event=take][ExecutionListener=com.activiti.extension.bean.HelloWorldSpringExecutionListener@2a72c0a6][ActivityId=startEvent]

10:37:09,981 [http-nio-8080-exec-8] INFO  com.activiti.extension.bean.HelloWorldSpringExecutionListener  - [Process=142551][event=start][ExecutionListener=com.activiti.extension.bean.HelloWorldSpringExecutionListener@2a72c0a6][ActivityId=userTask1]

10:37:09,982 [http-nio-8080-exec-8] INFO  com.activiti.extension.bean.HelloWorldSpringTaskListener  - [Process=142551][event=assignment][TaskListener=com.activiti.extension.bean.HelloWorldSpringTaskListener@6da401df][ActivityId=userTask1][TaskAssignee=1][TaskForm=null]

10:37:09,988 [http-nio-8080-exec-8] INFO  com.activiti.extension.bean.HelloWorldSpringTaskListener  - [Process=142551][event=create][TaskListener=com.activiti.extension.bean.HelloWorldSpringTaskListener@6da401df][ActivityId=userTask1][TaskAssignee=1][TaskForm=null]

 

If we start a second process instance and stop before completing User task 1:

 

10:40:14,691 [http-nio-8080-exec-5] INFO  com.activiti.extension.bean.HelloWorldSpringExecutionListener  - [Process=142557][event=start][ExecutionListener=com.activiti.extension.bean.HelloWorldSpringExecutionListener@2a72c0a6][ActivityId=startEvent]

10:40:14,692 [http-nio-8080-exec-5] INFO  com.activiti.extension.bean.HelloWorldSpringExecutionListener  - [Process=142557][event=take][ExecutionListener=com.activiti.extension.bean.HelloWorldSpringExecutionListener@2a72c0a6][ActivityId=startEvent]

10:40:14,692 [http-nio-8080-exec-5] INFO  com.activiti.extension.bean.HelloWorldSpringExecutionListener  - [Process=142557][event=start][ExecutionListener=com.activiti.extension.bean.HelloWorldSpringExecutionListener@2a72c0a6][ActivityId=userTask1]

10:40:14,693 [http-nio-8080-exec-5] INFO  com.activiti.extension.bean.HelloWorldSpringTaskListener  - [Process=142557][event=assignment][TaskListener=com.activiti.extension.bean.HelloWorldSpringTaskListener@6da401df][ActivityId=userTask1][TaskAssignee=1][TaskForm=null]

10:40:14,693 [http-nio-8080-exec-5] INFO  com.activiti.extension.bean.HelloWorldSpringTaskListener  - [Process=142557][event=create][TaskListener=com.activiti.extension.bean.HelloWorldSpringTaskListener@6da401df][ActivityId=userTask1][TaskAssignee=1][TaskForm=null]

 

So there is a significant difference here to if we use POJO listeners, with Spring bean listeners we will have just one bean instance per listener implementation shared between all process instances.

Making the Listener implementation a Spring Bean method

This works in the same way as for service task Spring Bean Methods. See docs.

 

Having a listener implemented as a Spring bean method looks like this (there is no difference between execution and task listener method implementations, unless you want to pass in an execution or task context):

 

package com.activiti.extension.bean.service;

import org.activiti.engine.delegate.DelegateExecution;
import org.activiti.engine.delegate.DelegateTask;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

@Service
public class HelloWorldService {
  private static Logger logger = LoggerFactory.getLogger(HelloWorldService.class);

  public String greeting() {
      return "Hello World from Service!";
  }

  public String customGreetingFromExecListener(
          DelegateExecution execution,
          String text) {
      logger.info("[Process=" + execution.getProcessInstanceId() +
                  "][Java Delegate=" + this + "]");

      logger.info("Hello World: " + text);

      return "Something back from service!";
  }

  public String customGreetingFromTaskListener(
          DelegateTask task,
          String text) {
      logger.info("[Process=" + task.getExecution().getProcessInstanceId() +
                  "][Java Delegate=" + this + "]");

      logger.info("Hello World: " + text);

      return "Something back from service!";
  }
}

 

And it is used as in the following BPMN 2.0 XML sample:

 

<process id="TestingListeners" name="Testing Listeners" isExecutable="true">
  <extensionElements>

    <activiti:executionListener
      event="start"
      expression="${helloWorldService.customGreetingFromExecListener(
                  execution, &quot;Hello-start&quot;)}"
/>


    <activiti:executionListener
      event="end"
      expression="${helloWorldService.customGreetingFromExecListener(
                 execution, &quot;Hello-end&quot;)}"
/>


  </extensionElements>

  <startEvent id="startEvent" name="Start Event"></startEvent>

  <userTask id="userTask1" name="User Task 1" activiti:assignee="$INITIATOR">
    <extensionElements>
      <activiti:executionListener event="start"
         delegateExpression="${helloWorldExecutionListener}"/>

      <activiti:executionListener event="end"
         delegateExpression="${helloWorldExecutionListener}"/>


      <activiti:taskListener
        event="create"
        expression="${helloWorldService.customGreetingFromTaskListener(
                   task, &quot;Hello-task-create&quot;)}"
/>

      <activiti:taskListener
        event="assignment"
        expression="${helloWorldService.customGreetingFromTaskListener(
                   task, &quot;Hello-task-assignment&quot;)}"
/>


      <activiti:taskListener event="complete"
         delegateExpression="${helloWorldTaskListener}"/>

      <activiti:taskListener event="delete"
         delegateExpression="${helloWorldTaskListener}"/>

  </extensionElements>
</userTask>

 

We can see here that some of the execution listeners and some of the task listeners have been changed to use the new Spring Bean method implementations. Let’s see what that looks like when we run the process.

 

Starting process one, not completing user task 1:

 

11:12:47,495 [http-nio-8080-exec-9] INFO  com.activiti.extension.bean.service.HelloWorldService  - [Process=145020][Java Delegate=com.activiti.extension.bean.service.HelloWorldService@41c60c23]

11:12:47,495 [http-nio-8080-exec-9] INFO  com.activiti.extension.bean.service.HelloWorldService  - Hello World: Hello-start

11:12:47,498 [http-nio-8080-exec-9] INFO  com.activiti.extension.bean.HelloWorldSpringExecutionListener  - [Process=145020][event=take][ExecutionListener=com.activiti.extension.bean.HelloWorldSpringExecutionListener@7076b9d][ActivityId=startEvent]

11:12:47,500 [http-nio-8080-exec-9] INFO  com.activiti.extension.bean.HelloWorldSpringExecutionListener  - [Process=145020][event=start][ExecutionListener=com.activiti.extension.bean.HelloWorldSpringExecutionListener@7076b9d][ActivityId=userTask1]

11:12:47,502 [http-nio-8080-exec-9] INFO  com.activiti.extension.bean.service.HelloWorldService  - [Process=145020][Java Delegate=com.activiti.extension.bean.service.HelloWorldService@41c60c23]

11:12:47,502 [http-nio-8080-exec-9] INFO  com.activiti.extension.bean.service.HelloWorldService  - Hello World: Hello-task-assignment

11:12:47,504 [http-nio-8080-exec-9] INFO  com.activiti.extension.bean.service.HelloWorldService  - [Process=145020][Java Delegate=com.activiti.extension.bean.service.HelloWorldService@41c60c23]

11:12:47,504 [http-nio-8080-exec-9] INFO  com.activiti.extension.bean.service.HelloWorldService  - Hello World: Hello-task-create

 

As we can see, same Spring Bean instance are used for all calls. If we start a second process instance and stop before completing User task 1:

 

11:14:52,185 [http-nio-8080-exec-1] INFO  com.activiti.extension.bean.service.HelloWorldService  - [Process=145026][Java Delegate=com.activiti.extension.bean.service.HelloWorldService@41c60c23]

11:14:52,186 [http-nio-8080-exec-1] INFO  com.activiti.extension.bean.service.HelloWorldService  - Hello World: Hello-start

11:14:52,186 [http-nio-8080-exec-1] INFO  com.activiti.extension.bean.HelloWorldSpringExecutionListener  - [Process=145026][event=take][ExecutionListener=com.activiti.extension.bean.HelloWorldSpringExecutionListener@7076b9d][ActivityId=startEvent]

11:14:52,187 [http-nio-8080-exec-1] INFO  com.activiti.extension.bean.HelloWorldSpringExecutionListener  - [Process=145026][event=start][ExecutionListener=com.activiti.extension.bean.HelloWorldSpringExecutionListener@7076b9d][ActivityId=userTask1]

11:14:52,189 [http-nio-8080-exec-1] INFO  com.activiti.extension.bean.service.HelloWorldService  - [Process=145026][Java Delegate=com.activiti.extension.bean.service.HelloWorldService@41c60c23]

11:14:52,189 [http-nio-8080-exec-1] INFO  com.activiti.extension.bean.service.HelloWorldService  - Hello World: Hello-task-assignment

11:14:52,189 [http-nio-8080-exec-1] INFO  com.activiti.extension.bean.service.HelloWorldService  - [Process=145026][Java Delegate=com.activiti.extension.bean.service.HelloWorldService@41c60c23]

11:14:52,189 [http-nio-8080-exec-1] INFO  com.activiti.extension.bean.service.HelloWorldService  - Hello World: Hello-task-create

 

So here is also a difference from Spring Bean implemented listeners, when we use expressions to call Spring Bean methods we will always just have one Spring Bean instance shared both inside and between process instances. This is okay when using the method approach (or expression approach as it is also referred to) as all context is passed in via parameters, which makes it thread safe.

Listening to login and logout events

There are special interfaces you can implement to listen to login and logout operations. For login events implement the LoginListener interface as follows:

 

package com.activiti.extension.bean;

import com.activiti.api.security.LoginListener;
import com.activiti.domain.idm.User;
import org.springframework.stereotype.Component;

@Component
public class LoginListenerImpl implements LoginListener {
  @Override
  public void onLogin(User user) {
      // User <user> logged in, do something

  }
}

 

And for logout events implement the LogoutListener as follows:

 

package com.activiti.extension.bean;

import com.activiti.api.security.LogoutListener;
import com.activiti.domain.idm.User;
import org.springframework.stereotype.Component;

@Component
public class LogoutListenerImpl implements LogoutListener {

  @Override
  public void onLogout(User user) {
    // User <user> logged out, do something

  }
}

 

Note that these classes need to be Spring beans and they need to be located in the com.activiti.extension.bean package to be picked up as such.

Listening to any event

In some cases we might just want to listen to all the events generated by the Activiti workflow engine. And maybe offload them to an external system for further processing. This can be done by implementing the RuntimeEventListener interface as follows:

 

package com.activiti.extension.bean;

import com.activiti.service.runtime.events.RuntimeEventListener;
import org.activiti.engine.delegate.event.ActivitiEvent;
import org.springframework.stereotype.Component;

@Component
public class RuntimeEventListenerImpl implements RuntimeEventListener {

  @Override
  public boolean isEnabled() {
      return true;
  }

  @Override
  public void onEvent(ActivitiEvent activitiEvent) {
      // Handle event here, offload to external system maybe
  }

  @Override
  public boolean isFailOnException() {
      return false;
  }
}

 

Note that this class need to be a Spring bean and it need to be located in the com.activiti.extension.bean package to be picked up as such.

Implementing Script backed Listeners

In some cases it could be useful to be able to implement a listener quickly without having to restart the server. This can be achieved with Script Listeners.

Coding the Scripts

This section goes through how to implement both an execution listener as a script and a task listener as a script.

Implementing an Execution Listener as a Script

To set up a script execution listener we use the out-of-the-box execution listener class called org.activiti.engine.impl.bpmn.listener.ScriptExecutionListener. We set two fields for it where the first field is called language, which specifies in what scripting language the script is written in. And the second field is called script and contains the actual code.

 

The following is an example of how to configure a script based execution listener for the process start event:

 

 

The resulting BPMN 2.0 XML looks like this:

 

<extensionElements>
  <activiti:executionListener
    event="start" class="org.activiti.engine.impl.bpmn.listener.ScriptExecutionListener">

      <activiti:field name="script">
          <activiti:string><![CDATA[execution.setVariable('varSetInScript', 'val1');]]></activiti:string>
      </activiti:field>
      <activiti:field name="language">
          <activiti:string><![CDATA[javascript]]></activiti:string>
      </activiti:field>
  </activiti:executionListener>

  <activiti:executionListener
    event="end"
    expression="${helloWorldService.customGreetingFromExecListener(execution, &quot;Hello-end&quot;)}"/>


</extensionElements>

 

This script execution listener just sets a process variable called varSetInScript to val1.

Implementing a Task Listener as a Script

To set up a script task listener we use the out-of-the-box task listener class called org.activiti.engine.impl.bpmn.listener.ScriptTaskListener. We set two fields for it where the first field is called language, which specifies in what scripting language the script is written in. And the second field is called script and contains the actual code.

 

The following is an example of how to configure a script based task listener for the task create event:

 

 

The resulting BPMN 2.0 XML looks like this:

 

<userTask id="userTask1" name="User Task 1" activiti:assignee="$INITIATOR">
<extensionElements>
  <activiti:executionListener event="start" delegateExpression="${helloWorldExecutionListener}"/>
  <activiti:executionListener event="end" delegateExpression="${helloWorldExecutionListener}"/>

  <activiti:taskListener event="create" class="org.activiti.engine.impl.bpmn.listener.ScriptTaskListener">
      <activiti:field name="script">
          <activiti:string><![CDATA[task.name = 'User Task ' + task.getExecution().getVariable('varSetInScript');]]></activiti:string>
      </activiti:field>
      <activiti:field name="language">
          <activiti:string><![CDATA[javascript]]></activiti:string>
      </activiti:field>
  </activiti:taskListener>

  <activiti:taskListener event="assignment" expression="${helloWorldService.customGreetingFromTaskListener(task, &quot;Hello-task-assignment&quot;)}"/>
  <activiti:taskListener event="complete" delegateExpression="${helloWorldTaskListener}"/>
  <activiti:taskListener event="delete" delegateExpression="${helloWorldTaskListener}"/>
</extensionElements>

 

The script task listener just takes the varSetInScript variable value and then uses it as part of the task name.

 

Running it should give a User Task 1 with a name as follows:

 

Should we use Script or Java based Listeners?

The following list some of the pros and cons with Java backed and Script backed listeners.

 

Java

Pros: Easy to debug, easy to test, most likely faster, easier to reuse code, code completion, secure, you can change them for an active process instance

Cons: More involved to get going, you need an extension project. Need to stop the server to install a new version, unless using something like JRebel

 

Scripting

Pros: Very easy to get going with it and you don’t have to leave the BPMN Editor, no other tools required, no server restarts required

Cons: Changes to script cannot be used by active process instances, can be a security risk (however, just recently there have been the release of so called secure script listeners that can be used to handle certain security risks), harder to debug problems, more error prone as you see error first during runtime, not easy to reuse code, not easy to create tests

Table of Contents                   

 

Introduction

Service tasks are one of the fundamental building blocks of any process. They allow you to implement complex business logic, make calculations, talk to external systems and services, and more. In Activiti there are a number of ways in which a service task can be implemented:

 

  1. As a POJO, which is called a Java Delegate
  2. As a Spring Bean Java Delegate
  3. As a Spring Bean method (what this article covers)

 

What implementation approach you choose depend on the use-case. If you don’t need to use any Spring beans in your implementation then use a POJO Java Delegate. If your service task implementation needs to use, for example out-of-the-box Spring beans, then use the Spring Bean Java Delegate. These two approaches uses a “one operation per service task” implementation. If you need your implementation to support multiple operations, then go with the Spring bean method implementation.

 

There is also a runtime twist to this, most likely there will be multiple process instances calling the same service task implementation. And the same service task implementation might be called from multiple service tasks in a process. The implementation behaves differently depending on approach:

 

  1. POJO Java Delegate - inside a process instance you have one class instance per service task, between process instances you share class instances per service task
  2. Spring Bean Java Delegate - same Spring bean (i.e. class instance) is used within a process instance and between process instances
  3. Spring Bean method - same Spring bean (i.e. class instance) is used within a process instance and between process instances but there is no field injection and all data is passed in via method params, so thread-safe

 

What this basically means is that if you use a third party class inside a Java Delegate, then it needs to be thread safe as it can be called by multiple concurrent threads. If you use a Spring bean Java Delegate approach then the same thing applies, if you inject beans they need to all be thread safe. With the Spring bean approach you can also change the bean instantiation scope to be PROTOTYPE, which means an instance will be created per service task.

 

Or you can use the Spring Bean Method approach, which this article covers, and which solves a lot of concurrency issues.

Source Code

Source code for the Activiti Developer Series can be found here.

Prerequisites

Before starting with your service task implementation make sure to set up a proper Activiti Extension project.

Implementing a Hello World Spring Bean Method

Let’s start the usual way with a Hello World Spring Bean Method. This differs from the other approaches as we are not implementing one specific execute method for a Java Delegate. Here we are implementing as many methods as we want, serving as many service tasks as we need from the same Spring Bean implementation.

Coding the Spring Bean class

Here is the implementation of the class:

 

package com.activiti.extension.bean.service;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;


@Service
public class HelloWorldService {
  private static Logger logger = LoggerFactory.getLogger(HelloWorldService.class);

  public String greeting() {
      return "Hello World from Service!";
  }

  public void customGreeting(String text) {
      logger.info("[Java object=" + this + "]");
      logger.info("Hello World: " + text);
  }
}

 

So every method we provide inside the Spring Bean class implementation is a potential Service Task implementation. In this case we are going to use the customGreeting method as the implementation and pass in different texts depending on what service task that is calling the method.

 

This is completely thread safe as we are not using any class members, just passing in data via a method parameter. However, the logging library needs to be thread safe as this method can be called from multiple concurrent threads, serving different service tasks.

Testing the method

Now to test the Spring Bean method as a service task implementation create a process model looking like this:

 

 

And the Service Task is connected to the Spring Bean method implementation via the Expression property:

 

 

The first service task has the following Expression:

 

${helloWorldService.customGreeting('Service Task 1')}

 

And the second task has the following Expression:

 

${helloWorldService.customGreeting('Service Task 2')}

 

In BPMN 2.0 XML it will look like this:

 

<serviceTask id="serviceTask1"
             name="Service Task 1 (Spring Bean method customGreeting(x))"
             activiti:expression="${helloWorldService.customGreeting('Service Task 1')}">

 

When we run this process it will print similar logs to the following:

 

03:11:10,919 [http-nio-8080-exec-7] INFO  com.activiti.extension.bean.service.HelloWorldService  - [Java object=com.activiti.extension.bean.service.HelloWorldService@4ed3dbd3]

03:11:10,919 [http-nio-8080-exec-7] INFO  com.activiti.extension.bean.service.HelloWorldService  - Hello World: Service Task 1

03:11:10,920 [http-nio-8080-exec-7] INFO  com.activiti.extension.bean.service.HelloWorldService  - [Java object=com.activiti.extension.bean.service.HelloWorldService@4ed3dbd3]

03:11:10,920 [http-nio-8080-exec-7] INFO  com.activiti.extension.bean.service.HelloWorldService  - Hello World: Service Task 2

 

We can see here that all the context for the method is passed in as a parameter. In this case it’s only a String of text, but could be any parameters you like. So using the same Spring bean works fine as the logging library is thread safe.

 

Now, what about process information and process variables? How do I get to them? That’s easy, just add an extra parameter of type DelegateExecution:

 

package com.activiti.extension.bean.service;

import org.activiti.engine.delegate.DelegateExecution;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

@Service
public class HelloWorldService {
  private static Logger logger = LoggerFactory.getLogger(HelloWorldService.class);

  public String greeting() {
      return "Hello World from Service!";
  }

  public void customGreeting(DelegateExecution execution, String text) {
      logger.info("[Process=" + execution.getProcessInstanceId() +
                  "][Java Delegate=" + this + "]");

      logger.info("Hello World: " + text);
  }
}

 

And update the Expression as follows:

 

${helloWorldService.customGreeting(execution, 'Service Task 1')}

Using Process Variables in the Spring Bean Method

In a Spring Bean Method we access process variables in the same way as in a POJO Java Delegate, so read its docs.

Returning data from the Spring Bean Method

There are probably many situations where we would want to return some data from the Spring Bean method and use it in the process. We can achieve this by defining return values for the service task. First update the customGreeting method to return a String:

 

public String customGreeting(DelegateExecution execution, String text) {
  logger.info("[Process=" + execution.getProcessInstanceId() + "][Java Delegate=" + this + "]");
  logger.info("Hello World: " + text);

  return "Something back from service!";
}

 

Now, to return this back to the process instance as a process variable we need to do the following property configuration for each service task:

 

 

Basically we need to specify the process variable name as the Result variable name property.

 

In BPMN 2.0 XML it will look like this:

 

<serviceTask id="serviceTask1"
             name="Service Task 1 (Spring Bean method customGreeting(x))"
             activiti:expression="${helloWorldService.customGreeting(execution, 'Service Task 1')}"
             activiti:resultVariableName="greetingFromService">

 

You could now use the REST API to list the variables available for the process instance after the Service Tasks have been executed, we should see the greetingFromService variable:

 

The URL path parameter 122501 in the above example is the process instance ID.

Using Field Injection in the Spring Bean Method

Not available when using Spring Bean Methods.

Injecting Beans into the backing Spring Bean

When we use Spring we most likely want to use other Spring Beans in our Java Delegate implementation. This can be Spring beans that we create and out-of-the-box Spring beans.

 

This works the same way as for Spring Bean Java Delegates, see docs.

Calling Spring Bean Methods Asynchronously

See the POJO Java Delegate docs.

Table of Contents                   

 

Introduction

Service tasks are one of the fundamental building blocks of any process. They allow you to implement complex business logic, make calculations, talk to external systems and services, and more. In Activiti there are a number of ways in which a service task can be implemented:

 

  1. As a POJO, which is called a Java Delegate
  2. As a Spring Bean Java Delegate (what this article covers)
  3. As a Spring Bean method

 

What implementation approach you choose depend on the use-case. If you don’t need to use any Spring beans in your implementation then use a POJO Java Delegate. If your service task implementation needs to use, for example out-of-the-box Spring beans, then use the Spring Bean Java Delegate. These two approaches uses a “one operation per service task” implementation. If you need your implementation to support multiple operations, then go with the Spring bean method implementation.

 

There is also a runtime twist to this, most likely there will be multiple process instances calling the same service task implementation. And the same service task implementation might be called from multiple service tasks in a process. The implementation behaves differently depending on approach:

 

  1. POJO Java Delegate - inside a process instance you have one class instance per service task, between process instances you share class instances per service task
  2. Spring Bean Java Delegate - same Spring bean (i.e. class instance) is used within a process instance and between process instances
  3. Spring Bean method - same Spring bean (i.e. class instance) is used within a process instance and between process instances but there is no field injection and all data is passed in via method params, so thread-safe

 

What this basically means is that if you use a third party class inside a Java Delegate, then it needs to be thread safe as it can be called by multiple concurrent threads. If you use a Spring bean approach then the same thing applies, if you inject beans they need to all be thread safe. With the Spring bean approach you can also change the bean instantiation scope to be PROTOTYPE, which means an instance will be created per service task.

 

This article cover the second approach - Spring Bean Java Delegate.

Source Code

Source code for the Activiti Developer Series can be found here.

Prerequisites

Before starting with your service task implementation make sure to set up a proper Activiti Extension project.

Implementing a Hello World Spring Bean Java Delegate

This is pretty much the same thing as implementing a POJO Java Delegate, you just “Springify” it a bit so it can act as a Spring Bean and wire in other Spring Beans. Let’s start the usual way with a Hello World Spring Bean Java Delegate. However, we are going to take the opportunity to check process IDs and object instance IDs while we are at it, so the log message from the Spring Bean Java Delegate will be a little bit different than the usual “Hello World”.

Coding the Java class

Here is the implementation of the Spring Bean Java Delegate class:

 

package com.activiti.extension.bean;

import org.activiti.engine.delegate.DelegateExecution;
import org.activiti.engine.delegate.JavaDelegate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Component("helloWorld")
public class HelloWorldSpringJavaDelegate implements JavaDelegate {
  private static Logger logger = LoggerFactory.getLogger(HelloWorldSpringJavaDelegate.class);

  @Override
  public void execute(DelegateExecution execution) throws Exception {
      logger.info("[Process=" + execution.getProcessInstanceId() +
                  "][Spring Java Delegate=" + this + "]");
  }
}

 

There are two things here that differentiate a Spring Bean Java Delegate implementation from a POJO Java Delegate implementation. The first is that it is annotated with org.springframework.stereotype.Component, setting it up to be discovered and  registered as a Bean in the Spring application context. The second thing, which is very important, is that the class needs to be defined in the com.activiti.extension.bean package, or any sub-package, for the Spring component scanning to find it.

 

Note here that the Spring Bean will have the name helloWorld, as specified via the @Component annotation. If we don’t specify a name then the classname will be used with first letter in lower-case.

 

The rest of the implementation is the same as for the POJO Java Delegate, so read through the implementation section for it.

Testing the Spring Bean Java Delegate

Now to test the Java Delegate implementation create a process model looking like this:

 

 

And the Service Task is connected to the Spring Bean Java Delegate implementation via the Delegate expression property:

 

 

In BPMN 2.0 XML it will look like this:

 

<serviceTask id="sid-7C83EB30-8E02-400B-BEEF-CAE34BFB6FFD"
             name="Service Task 1 (Spring Bean Java Delegate)"
             activiti:delegateExpression="${helloWorld}">

 

Note here that instead of using activiti:class we use activiti:delegateExpression to tell Activiti about the Spring Bean name. An activiti:delegateExpression can be used when the expression resolves to a Java object instance that implements the JavaDelegate interface.

 

When we run this process it will print similar logs to the following:

 

02:43:54,236 [http-nio-8080-exec-9] INFO  com.activiti.extension.bean.HelloWorldJavaDelegate  - [Process=102534][Spring Java Delegate=com.activiti.extension.bean.HelloWorldSpringJavaDelegate@3e39e