Creating a Data Object Model With an Expression-based Service Model
In this guide, we’ll look at creating a data object model, where the data for the data object comes from a Java API which are referenced through expressions.
The steps we’ll go through are
-
(Optional) Create a Spring bean and expose it for writing expressions
-
Create the data object model
-
Create the service model based on the data object model
-
Create an example case model and forms to show the usage of the data object
Creating a Spring Bean (optional)
This part can be skipped if you’re not interested in executing the example models in the next parts. This part assumes familiarity with Spring, exposing a custom Spring configuration in Flowable and using an IDE.
We’ll create a simple Spring bean that can be used in expressions later on to build the service model for the data object model. This Spring bean is by no means production-ready and only serves as purpose to demonstrate how an expression in a service model delegates to the actual Java class (e.g. no persistency, but keeping everythin in a HashMap).
The customer POJO looks quite simple:
public class Customer {
private String id;
private String name;
private boolean isActive;
private String country;
private long yearlyFee;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public boolean getIsActive() {
return isActive;
}
public void setActive(boolean active) {
this.isActive = active;
}
public String getCountry() {
return country;
}
public void setCountry(String country) {
this.country = country;
}
public long getYearlyFee() {
return yearlyFee;
}
public void setYearlyFee(long yearlyFee) {
this.yearlyFee = yearlyFee;
}
@Override
public String toString() {
return "Customer{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", isActive=" + isActive +
", country='" + country + '\'' +
", yearlyFee=" + yearlyFee +
'}';
}
}
The example class implementing the CRUD methods looks as follows:
import java.util.*;
import org.slf4j.*;
import com.fasterxml.jackson.databind.*;
public class CustomerService {
private static Logger LOGGER = LoggerFactory.getLogger(CustomerService.class);
private ObjectMapper objectMapper;
private Map<String, Customer> customers = new HashMap<>();
public CustomerService(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
public JsonNode findCustomerById(String id) {
Customer customer = customers.get(id);
if (customer != null) {
LOGGER.info("Retrieved customer " + customer);
return objectMapper.valueToTree(customer);
}
return null;
}
public JsonNode createCustomer(String name, boolean isActive, String country, Long yearlyFee) {
Customer customer = new Customer();
customer.setId(UUID.randomUUID().toString());
customer.setName(name);
customer.setActive(isActive);
customer.setCountry(country);
customer.setYearlyFee(yearlyFee);
customers.put(customer.getId(), customer);
LOGGER.info("Created customer " + customer);
return objectMapper.valueToTree(customer);
}
public JsonNode updateCustomer(String id, String name, boolean isActive, String country, Long yearlyFee) {
Customer customer = customers.get(id);
customer.setName(name);
customer.setActive(isActive);
customer.setCountry(country);
customer.setYearlyFee(yearlyFee);
LOGGER.info("Updated customer " + customer);
return objectMapper.valueToTree(customer);
}
public void deleteCustomer(String id) {
Customer customer = customers.remove(id);
LOGGER.info("Deleted customer " + customer);
}
}
One perhaps unexpected observation here is that all of the return types of the methods are JsonNode. Flowable mandates that any Spring bean used in an expression for a service model returns JsonNode. The reason for this is
-
to have a uniform way of handling service invocations
-
to avoid storing serializable Java objects and force storing the data as a JSON variable
This class needs to be exposed as a Spring bean so that it can be used in expressions. To do so, add the following lines to your custom Configuration (class annotated with @Configuration).
@Bean
public CustomerService customerService(ObjectMapper objectMapper) {
return new CustomerService(objectMapper);
}
Creating the Data Object Model
In Flowable Design, go to the Others
section, select Data object
from the list on the left-hand side, and then click on the the Create button in the top-right corner.
In the dialog, give the data object model a name, set the key, and optionally add a meaningful description for it. Once all the information is entered then click on Create new model.
Once created, the Data Object Model editor opens, and we can start defining it.
Defining the Data Object Fields
A data object consists of one or more fields. New fields can be added by clicking the Create new field button.
The following form is shown:
Following properties can be set:
-
Label (optional): a human-readable version of the (technical) name of the field.
-
Name (required): the (technical) name of the field. This is the name that is used for example in expressions in process or case models (e.g.
${customer.id === '123}
). -
Type (required): the type of the field. Currently supported are String/Date/Boolean/Integer/Double/Long fields.
-
Default value (optional): a value for the field that is set in case when a value would not have been set.
-
Description (optional): a human-readable explanation of the purpose of this field.
Let’s add following fields to the data object:
-
ID: a unique identifier for the customer.
-
Name: the name of the customer
-
Active: a flag indicating whether this is an active customer or not
-
Country: the country where the customer resides.
-
Yearly fee: a monetary value indicating some yearly fee the customer pays.
This should result in something that looks like this:
If a description is set, an information icon is shown next to the label of that field (e.g. for the ID field here.)
The actions column of the table shows the following actions (from left to right):
-
Edit: this will allows changing the field settings.
-
Delete: this deletes the field. A confirmation will be asked before deleting.
-
Move up: moves the field one row up.
-
Move down: moves the field one row down.
Advanced Settings
At the bottom of the page there is a check box available that, when selected, displays optional advanced settings:
All these fields are (technical) metadata settings:
-
Type: A custom type for the data object.
-
Sub type: A custom sub type for the data object.
-
External Id: An external id to identify the data object.
The main use case for these is to categorise or easily query and retrieve data object definitions via the API’s.
Include in App
Data object models can be added and are deployed as part of a regular app.
To do that we need to go to the Apps
section of Flowable Design and
create an app.
After the app is created in the App Editor, we need to
-
select the + button
-
select now
Include an existing model into the app
-
go to the
Others
models tab and then select the data object model
Creating the Service Model
In this part, we’ll create the service model needed to retrieve and manipulate the data for a data object model. The point about doing this, is that all of this is hidden to the business modeler. The actual details how this done are hidden in the service models and don’t leak into the models where the data object gets used.
In Flowable Design, go to the Others
section, select Service
from the list on the left-hand side,
and then click on the the Create button in the top-right corner.
In the dialog, give the model a name, set the key, and optionally add a meaningful description for it. Once all the information is entered then click on Create new model.
Two dropdowns are available: one to select the type and one to select the service type. he type is either standard or based on a data object model.
-
A standard service model allows defining operations and parameters in a free-form way. This is for service models that e.g. get used by the service registry task (See The Service Registry Developer Guide) in process or case models.
-
A service model based on a data object model is used to connect a data object model to an implementation of how to retrieve and manipulate the data at runtime. When this type is selected, the structure of the data model will define a set of required operations and define which parameters are available (i.e. the fields of the data object model).
Select now based on a data object model. A second dropdown appears that allows selecting the
data object model. Choose from that list the Customer
data object model which we’ve created above.
With the third dropdown, the type of service is chosen. In this case, we’ll use the expression option.
When switching to this type a few things happen, as shown on the screenshot below.
-
The
Data Object Configuration
section appears -
Four operations are automatically generated
For expression-based service models, only the Lookup ID
needs to be configured.
This Lookup ID
is used to uniquely identify and retrieve data object instances.
As such, it is mandatory to mark one of the data object fields as the 'lookup identifier'.
In the example here, this is the ID
field.
Operations
After selecting a data object model from the dropdown, four operations have automatically been generated. These operations are needed by any service definitions that handles the data of a data object (this is why these operations, unlike any other operations, cannot be deleted). Of course, other operations for the same service model are still permitted.
The default generated operations are
-
Lookup : This operation will be used whenever the data object instance is needed. For example, when a form is displaying information about the data object, the data will be retrieved using this operation behind the scene.
-
Create : This operation is be used when a data object is created for the first time.
-
Update : This operation is used when the data of an existing data object instance is changed.
-
Delete : This operations allow deleting the data object instance.
Let’s have a look at the concrete configuration of the four operations for our customer data object.
Lookup Operation
The responsability of the Lookup
operation is to retrieve the data of a data object instance at
runtime, based on a data object model.
Note that the name can be changed, but the key is not editable. This is because this opertation is a 'fixed' operation which every data object based service model needs to have.
In the example Spring bean, this data is retrieved by calling public JsonNode findCustomerById(String id)
,
where id
is the actual unique identifier of the customer.
The Lookup operation does not have input parameters, as the only value available is the lookupId
.
Any (backend) expression in Flowable is put between ${}
,
the Expression
field thus needs to be configured to ${customerService.findCustomerById(lookupId)}
.
The Lookup REST API produces a JSON response and this JSON needs to be mapped to the structure
of the data object. Unlike a standard
service model, the fact we’ve got the data object structure
available, helps us in defining this mapping.
As our example Spring bean returns all the properties on the root level, the mapping is a simple one-to-one mapping. The left-hand side shows the JsonNode response side, which needs to be mapped to the data object model structure on the right-hand side:
Since the JSON response contains the data on the root level, the Path
column is empty.
However, if this wouldn’t have been the case the Path
can be used to define where the value can
be found (using JSON-Pointer syntax).
Create and Update Operation
The Create
and Update
operations are quite similar: they both start from a representation
of the data object, typically through forms, and need to either create or change the corresponding data.
Configuring the create operation is easy: the Expression
needs to be set to
${customerService.createCustomer(name, isActive, country, yearlyFee)}
The Update
operation looks similar, except using ${customerService.updateCustomer(id, name, isActive, country, yearlyFee)}
for the Expression
The Create
and Update
method calls typically return a JSON response. This response needs to be mapped
to the data object instance in a way similar to the Lookup
(one-to-one).
Delete Operation
The delete operation is the simplest of all operations. Beyond changing the Expression
to ${customerService.deleteCustomer(lookupId)}
, nothing else is needed.
A delete operation has neither input nor output parameters, as only the lookupId
can be used and
nothing needs to be mapped back as the data object instance gets deleted when invoked.
Example: How To Use the Data Object Model in a Case Model
Let’s use the models from above to create a simple case model. The goal is to create a CMMN case model that can be used to create and manage customer data.
The end result will be a CMMN model that looks like:
Of course, this is but a minimal model highlighting specifically using a data object model (backed by a service model). A realistic customer case model would have way more steps and details than this example.
Creating the CMMN Model
In Flowable Design, click on Apps
in the header bar. Create a new App and name it Customer app
.
Include the data object and service model we’ve created above by clicking the large +
button and selecting
Include an existing model into the app
.
Now, click the +
button again and select Create a new model for the app
and Case
in the next step.
Give the model a name (e.g. Customer Case
) and a unique key (e.g. customerCase
).
Click Create new model
. The view will now change to the CMMN editor.
The Start Form
When a case instance is started for this model, we want to show a form that asks for customer data.
Click on the plan model (the only thing shown on the screen right now) and click in the property panel on
the Start form
property. The New
tab is open by default, so fill in a name for the start form and click on Create
:
An empty form model is now shown.
Before we model the form, we have to configure it to be bound to a data object model.
To do this, click on the Data model
property in the property panel:
A popup is now shown that allows us to fill in the data object model we want to reference. Here, we fill in the following:
-
variable name: customer
-
This is how we’ll reference the data object in the form value bindings.
-
-
Data object definition key: customer
-
This is the key of the data object model.
-
-
Mode: Create
-
When handling the form, we need to know whether this form will create the data object or only view or modify it.
-
This effectively creates a sort of 'scope', similar to the root or parent scope where variables can be stored on. It is also possible to bind the data object instance to a root or parent variable (e.g. using root.customer). |
Let’s add some fields to the model:
The fields here are
-
Name, bound to customer.name and of type text.
-
Active?, bound to customer.active and of type checkbox.
-
Country, bound to customer.country and of type text.
-
Fee, bound to customer.yearlyFee and of type number.
Note that each fields is bound to customer., which is the variable configured for the data object in the popup above.
Publishing the App
Save the CMMN model and go to our app model (it is the model right at the top when going to the Apps
tab).
The three models (data object, service and CMMN model) are part of the app:
Now click the Publish button
(this will only work when you have a running Flowable Work or Engage where you
can publish against).
Go to your Flowable Work or Engage installation, log in, Click the New
button and select Work
.
Type customer
in the search field. The customer case is now be selectable:
Select it. The start form is now shown:
Filling in the form and clicking Submit
will start a case instance and immediately end it,
as the case has no tasks to do.
The Work Form
Simular to the start form, we can add a Work Form to the case model. This form can be seen from the case instance view and the goal is to display the customer information, but in a read-only mode. The actual editing will be implemented with a user task later.
Close to the Start form
property, there is the Work form
property. Create a new form:
Similar to the start form, click the Data model
property of the form.
Give it the same customer
variable and key, but change the mode to View
this time:
The fields look exactly the same as for the start form,
except that all fields have the Enabled
checkbox set to false
.
The Customer Update Task
Let’s add a few things to the case model:
-
Add a stage with name
Active
-
Add a user event listener
Inactivate
in this stage -
Add an exit sentry to this stage and link it to the event listener
-
Add user task named
Change customer details
to this stage -
Make this task manual activated (select the
manual activation
checkbox in the property panel) -
Make this task (infinite) repeatable (select the
repetition
checkbox in the property panel) -
Add a stage with name
Inactive
below the other stage -
Add an entry sentry to the second stage. Link it to the first stage and select the
Exit
transition. We want to move to this stage when the first one is terminated
Select the user task again and create a new form:
Similar to above, configure the data object model for this form. Set the mode to Modify
.
Note: Modify actually also allows for viewing data, as will become clear in the next step.
Let’s create a simple form, only adding a name and fee field.
Set the Enabled
checkbox of the Name
field to false.
Let’s deploy the app: either go to the Apps
tab, select the app model and
click Publish
or click the Quick Deploy
button in the editor toolbar (the cloud icon with the triangle).
Go to Flowable Work or Engage and, like before, start a new case instance and fill in the start form.
After starting the case instance, the view is now more interesting. The two stages are shown in the header
and when clicking the Work form
tab, the read-only form from above is displayed:
To start the (manual) user task, we need to click the Change customer details
button in the header.
This will start the user task, which can be found by clicking the Open tasks
tab.
The form to update the customer data is shown:
Change the fee
to another value. If you’re running the example applications,
you’ll see in those applications the output
Update customer Customer{id='8db19819-62df-4baa-b746-12fae1376941', name='John Doe', active=false, country='Belgium', yearlyFee=123456}
Note that the Change customer details
tasks can be started an infinite number of times.
In reality, you’d probably want to limit this to one. The Max instance count
property
on the user can be used to make sure there is only one user task open at any given time.
Using the Service Registry Task
This feature is only available from Flowable 3.5.1 |
Let’s finish up the CMMN model:
-
Add an event listener to the
Inactive
stage with nameRemove customer data
. The idea is that when theInactivate
event listener is selected, the first stage gets terminated, which brings the customer in an inactive but not yet deleted state. To actually delete the data, theRemove customer data
event listener needs to be triggered. -
Add a service registry task
Delete customer data
to the same stage and link it to the event listener with an entry sentry. -
Add an exit sentry to the whole case (this is fully optional, as the behavior would be the same without here, but it’s visually clearer).
Above, we’ve configured the service model to be based on a data object model. However, this service model is still available as a regular service model and thus can be referenced from the service registry task.
Select the Delete customer data
task and select the Delete
operation from the model and operation.
In the Input parameters
configuration, bind the value to ${customer.id}
.
Now
-
Deploy the app again and start a new case instance.
-
Click the
Inactivate
button in the header. This brings the case instance to theInactive
stage. -
Click the
Remove customer data
button in the header.
The customer data is now deleted. When running with the example appliaction, in the logging you can now see something like
Deleted customer Customer{id='3465f3ae-e8e9-43e2-a10e-27887270e764', name='John Doe', active=false, country='Belgium', yearlyFee=123456}