Groovy script language in Bonita
In Bonita, it is a prerequisite to be comfortable with Java when implementing advanced behaviors in your processes, the below article aims at helping with Groovy, the scripting language used by Bonita.
Groovy has been chosen as the scripting language because it is :
-
Concise, readable and have an expressive syntax, easy to learn for Java developers
-
Seamlessly and transparently integrates and interoperates with Java and any third-party libraries
From Java to Groovy
For a complete description, please have a look at the reference documentation here.
Strings
Text literals are represented in the form of chain of characters called strings. Groovy lets you instantiate java.lang.String objects, as well as GStrings (groovy.lang.GString) which are also called interpolated strings in other programming languages.
-
Single quoted strings are a series of characters surrounded by single quotes:
'a single quoted string'
-
Triple single quoted strings are a series of characters surrounded by triplets of single quotes and are multiline:
def aMultilineString = '''line one
line two
line three'''
-
Double quoted strings are a series of characters surrounded by double quotes:
"a double quoted string"
Double quoted strings are plain java.lang.String if there’s no interpolated expression, but are groovy.lang.GString instances if interpolation is present. |
-
String interpolation
Any Groovy expression can be interpolated in all string literals, apart from single and triple single quoted strings. Interpolation is the act of replacing a placeholder in the string with its value upon evaluation of the string. The placeholder expressions are surrounded by ${}
or prefixed with $
for dotted expressions. The expression value inside the placeholder is evaluated to its string representation when the GString is passed to a method taking a String as argument by calling toString()
on that expression.
Here, we have a string with a placeholder referencing a local variable:
def name = 'Romain' // a plain string
def greeting = "Hello ${name}"
assert greeting.toString() == 'Hello Romain'
In addition to ${}
placeholders, we can also use a lone $
sign prefixing a dotted expression:
def person = [name: 'Romain', age: 34]
assert "$person.name is $person.age years old" == 'Romain is 34 years old'
But only dotted expressions of the form a.b
, a.b.c
, etc, are valid, but expressions that would contain parentheses like method calls, curly braces for closures, or arithmetic operators would be invalid.
More about Strings in Groovy here.
Lambdas & Closures
Java 8+ supports lambdas and method references:
Runnable run = () -> System.out.println("Run");
list.forEach(System.out::println);
Java lambdas can be more or less considered as anonymous inner classes. Groovy doesn’t support that syntax, but has closures instead:
Runnable run = { println 'run' }
list.each { println it } // or list.each(this.&println)
Closures are heavily used when processing collections:
def list = ['Daffy', 'Bugs', 'Elmer', 'Tweety', 'Silvester', 'Yosemite']
assert 'Bugs' == list.find { it == 'Bugs' }
assert ['Daffy', 'Bugs', 'Elmer'] == list.findAll { it.size() < 6 }
assert list.any { it =~ /a/ }
assert list.every { it.size() > 3 }
def map = [name: 'Messages from mrhaki', url: 'http://mrhaki.blogspot.com', blog: true]
def found = map.find { key, value -> key == 'name' }
assert found.key == 'name' && found.value == 'Messages from mrhaki'
found = map.find { it.value =~ /mrhaki/ }
assert found.key == 'name' && found.value == 'Messages from mrhaki'
assert [name: 'Messages from mrhaki', url: 'http://mrhaki.blogspot.com'] == map.findAll { key, value -> value =~ /mrhaki/ }
assert map.any { entry -> entry.value }
assert map.every { key, value -> key.size() >= 3 }
More about Closures in Groovy here.
Operators
-
Identity operator
In Groovy, using ==
to test equality is different from using the same operator in Java. In Groovy, it is calling equals. If you want to compare reference equality, you should use is like in the following example:
def list1 = ['Groovy 1.8','Groovy 2.0','Groovy 2.3'] // Create a list of strings
def list2 = ['Groovy 1.8','Groovy 2.0','Groovy 2.3'] // Create another list of strings containing the same elements
assert list1 == list2 // using ==, we test object equality
assert !list1.is(list2) // but using 'is', we can check that references are distinct
-
Safe navigation operator
?.
to Avoid NullPointerException:
Suppose we have a simple model like this:
class Company {
Address address
String name
}
class Address {
Street street
String postalCode
String city
}
class Street {
String name
String number
String additionalInfo
}
We want to display the street name, but we don’t know if all object instances are available. To avoid a NullPointerException we write the following code:
// company can be null.
if (company != null && company.getAddress() != null && company.getAddress().getStreet() != null) {
println company.address.street.name
}
Groovy adds the safe navigation operator to shorten all this to:
// company can be null.
println company?.address?.street?.name
-
The Elvis operator
:?
to shorten ternary expression
def sampleText
// Normal ternary operator.
def ternaryOutput = (sampleText != null) ? sampleText : 'Hello Groovy!'
// The Elvis operator in action. We must read: 'If sampleText is not null assign
// sampleText to elvisOuput, otherwise assign 'Viva Las Vegas!' to elvisOutput.
def elvisOutput = sampleText ?: 'Viva Las Vegas!'
More about Operators in Groovy here.
Groovy truth
Groovy decides whether an expression is true or false by applying the rules given below.
-
Boolean expressions : True if the corresponding Boolean value is
true
. -
Collections and Arrays: Non-empty Collections and arrays are true.
-
Matchers: True if the Matcher has at least one match.
-
Maps: Non-empty Maps are evaluated to true.
-
Strings: Non-empty Strings, GStrings and CharSequences are coerced to true.
-
Numbers: Non-zero numbers are true.
-
Object References: Non-null object references are coerced to true.
More about Groovy Truth here.
Bonita use cases with Groovy
In the below examples, the following BDM will be used
*
for mandatory fields
Comment {
String content*
AppUser createdBy* //Aggregation
DateTime creationDate*
AppUser lastEditedBy* //Aggregation
}
AppUser {
String firstName*
String lastName*
Address address //Composition
}
Address {
Street street* //Composition
String postalCode*
String city*
}
Street {
String name*
String number*
String additionalInfo
}
Instantiate a Business Data
Using the generated DAO
By default a newInstance
factory method is generated in the object DAO.
This method has as many parameters as mandatory fields for this object.
appUserDAO.newInstance('Jane','Doe') // create a UserApp instance with firstName = 'Jane' and lastName = 'Doe'
List Business Objects using DAO
When defining your BDM you can write custom queries in JPQL that can be called using the object DAO.
Some queries are generated by default like: find
, findByFirstName
, findByLastName
…
All object DAO are injected at runtime in Groovy script expression or can be retrieved using APIClient#getDAO
Here is the UserAppDAO interface generated for the UserApp object:
public interface AppUserDAO extends BusinessObjectDAO {
AppUser findByPersistenceId(Long persistenceId);
List<AppUser> findByFirstName(String firstName, int startIndex, int maxResults);
List<AppUser> findByLastName(String lastName, int startIndex, int maxResults);
List<AppUser> find(int startIndex, int maxResults);
Long countForFindByFirstName(String firstName);
Long countForFindByLastName(String lastName);
Long countForFind();
AppUser newInstance(String firstName, String lastName);
}
So, in a Groovy script, you can access the data like this:
def users = appUserDAO.find(0, 10) // returns to first tens users ordered by persistenceId
def johnUsers = appUserDAO.findByFirstName('John', 0, 10) // returns to first tens users with firstName == 'John' ordered by persistenceId
Update a Business Data
The example below is the generated code when editing a UserApp address from a contract
user
is the existing data in the process
userInput
is the contract input of the edition task
if (!userInput?.address) { // As Address is not mandatory it can be null
return null
}
def addressVar = user.address ?: new com.company.model.Address() // Retrieve the existing address or create a new one
addressVar.street = { //Use a Closure to resolve the street value
if (!userInput?.address?.street) { // Street is mandatory so it can't be null here, protected by a contract constraint, null-check statement is generated anyway
return null
}
def streetVar = addressVar.street ?: new com.company.model.Street() // Retrieve the existing street or create a new one
// Assign contract values, note that nullsafe navigators are used even if we know that userInput.address is not null here
streetVar.name = userInput?.address?.street?.name
streetVar.number = userInput?.address?.street?.number
streetVar.additionalInfo = userInput?.address?.street?.additionalInfo
return streetVar
}() //execute the Closure to assign the Street value to address
addressVar.postalCode = userInput?.address?.postalCode
addressVar.city = userInput?.address?.city
return addressVar // Return the edited (or new) address
As it is generated code it has to work in many situation so it is not the most concise code. Here is another example where it updates the lastEditedBy aggregation relation
//Retrieve aggregated AppUser using its DAO and persistenceId
def appUserVar = appUserDAO.findByPersistenceId(commentInput?.lastEditedBy?.persistenceId_string?.trim() ? commentInput.lastEditedBy.persistenceId_string.toLong() : null) //commentInput?.lastEditedBy?.persistenceId_string?.trim() checks that the persistenceId_string is not null and not empty after removing all whitspaces
if (!appUserVar) { // no userApp found for the given persistenceId
if (commentInput?.lastEditedBy?.persistenceId_string?.trim() ? commentInput.lastEditedBy.persistenceId_string.toLong() : null) {
// Throw an exception to explain that the given persistenceId is invalid
throw new IllegalArgumentException("The aggregated reference of type `AppUser` with the persistence id " + commentInput?.lastEditedBy?.persistenceId_string?.trim() ? commentInput.lastEditedBy.persistenceId_string.toLong() : null + " has not been found.")
}
//Just return null when no persistenceId is given, case of a not mandatory relation
return null
}
return appUserVar //Return the user found for the given persistenceId
Search for Tasks instances
In a Groovy script expression you can access Bonita APIs using the apiAccessor
provided variable.
def user = apiAccessor.identityAPI.getUserByUserName('john.doe')
def johnsTasks = apiAccessor.processAPI
.searchHumanTaskInstances(new SearchOptionsBuilder(0, 50).with { // using groovy builder
filter(HumanTaskInstanceSearchDescriptor.ASSIGNEE_ID, user.id)
sort(HumanTaskInstanceSearchDescriptor.DUE_DATE, Order.DESC)
done()
})
.result // returns the 50 first opened tasks assigned to john.doe sorted by due date
Get CustomUserInfo value for a user
In a Groovy script expression you can access Bonita APIs using the apiAccessor
provided variable.
def user = apiAccessor.identityAPI.getUserByUserName('john.doe')
def customInfo1Value = apiAccessor.identityAPI
.getCustomUserInfo(user.id, 0, 1000)
.find { "customInfo1" == it.getDefinition().getName() }
?.getValue()
Create a data model
An advantage of Groovy over Java when implementing a data model is that accessor’s methods are not required. In addition, it is possible to use annotations like @Canonical to generate toString, equals and hashCode methods.
To create a Groovy class, right click on your project, New > Groovy…
package org.company.model
import groovy.transform.Canonical
@Canonical
class Customer implements Serializable{
String firstName
String lastName
LocalDate birthDate
}
This object can then be used as a process variable type for example.