DEV Community

Cover image for Embed Bonita Engine in a Spring Boot application
Emmanuel Duchastenier for Bonitasoft

Posted on • Updated on

Embed Bonita Engine in a Spring Boot application

This tutorial is an example of how to embed the Bonita Engine (BPM workflow engine) in a Spring Boot application.

The proposed use-case is an application based on a process that allows someone to request a loan from their bank. This request will be reviewed, approved or rejected by the bank which will give explanations for this decision.

Scope

In this tutorial, you will learn how to write an application, using the Spring Boot framework, that integrates the Bonita Execution Engine to run processes.
You will learn how to configure the Bonita Engine to point to the database of your choice and tune the connection pool.
You will learn how to build processes programmatically, deploy and execute them.
You will also learn how to deploy processes generated with Bonita Studio.

TLDR;

The full source code of this tutorial can be accessed directly, in 2 different flavours:

Prerequisites

Database

If you just want to run an embedded H2 database, nothing is required.

To have your application point to a MySQL, PostgreSQL, Microsoft SQL Server, or Oracle database make sure you have a Database server up and running, and that it contains a schema dedicated to the Bonita Engine (default name is bonita).

For deeper details on database preparation for Bonita, see the specific documentation page.

Then read below how to configure the database access.

Processes

This tutorial assumes you have basic knowledge of BPMN / process design.

Use case

For this example, we will develop and interact with the following process.

Loan Request process diagram

Let's write the application step by step

In this example, we propose to use Gradle as the build tool, and Kotlin as the programming language.
If you are not familiar with Gradle, there is a Maven version of this example.
If you are not familiar with Kotlin, don't worry, it can be read easily if you know Java or a similar language.

Bootstrap of the application, using Spring Boot

Let's write a Gradle build (file build.gradle.kts) with the minimum Spring Boot + Kotlin requirements:

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    id("org.springframework.boot") version "2.1.6.RELEASE"
    id("io.spring.dependency-management") version "1.0.7.RELEASE"
    kotlin("jvm") version "1.3.41"
    kotlin("plugin.spring") version "1.3.41"
}

repositories {
    mavenCentral()
    jcenter()
}

dependencies {
    // Embed an application server:
    implementation("org.springframework.boot:spring-boot-starter")

    // Libs to code in Kotlin:
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
}

java.sourceCompatibility = JavaVersion.VERSION_1_8

// configure Kotlin compiler:
tasks.withType<KotlinCompile> {
    kotlinOptions {
        freeCompilerArgs = listOf("-Xjsr305=strict")
        jvmTarget = "1.8"
    }
}
Enter fullscreen mode Exit fullscreen mode

Write the main Spring Boot class to launch our application:

package org.bonitasoft.loanrequest

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class LoanRequestApplication

fun main(args: Array<String>) {
    runApplication<LoanRequestApplication>(*args)
}
Enter fullscreen mode Exit fullscreen mode

In the current state, our application can already be run (but does not do anything) by typing in the command line:

./gradlew bootRun
Enter fullscreen mode Exit fullscreen mode

and can then be accessed at http://localhost:8080/

Adding the Bonita Engine

As a next step, let's add the Bonita Engine in the equation in our build.gradle.kts:

...
dependencies {
    ...
    val bonitaEngineVersion = "7.9.0"

    // adding dependency on bonita-engine-spring-boot-starter automatically provides
    // and starts a Bonita Engine when used in a Spring Boot application:
    implementation("org.bonitasoft.engine:bonita-engine-spring-boot-starter:$bonitaEngineVersion")

    // use bonita-client to be able to interact with the running Engine
    // to deploy and run instances of processes:
    implementation("org.bonitasoft.engine:bonita-client:$bonitaEngineVersion")

    // Add the database driver we want Bonita to use:
    runtime("com.h2database:h2:1.4.199")
    ...
}
Enter fullscreen mode Exit fullscreen mode

The magic of Spring Boot happens, and a Bonita Engine is automatically started when our application starts.
We can see Engine startup logs in the console:

|09:44:15.601|main|INFO |o.b.p.s.ScriptExecutor| configuration for Database vendor: h2
|09:44:15.989|main|INFO |o.b.p.s.PlatformSetup| Connected to 'h2' database with url: 'jdbc:h2:file:./build/h2_database/bonita' with user: 'BONITA'
|09:44:16.341|main|INFO |o.b.p.s.ScriptExecutor| Executed SQL script file:/home/manu/.gradle/caches/modules-2/files-2.1/org.bonitasoft.platform/platform-resources/7.9.0/c183cb/platform-resources-7.9.0.jar!/sql/h2/createTables.sql
...
|09:44:26.437|main|INFO |o.b.e.a.i.PlatformAPIImpl| THREAD_ID=1 | HOSTNAME=manu-laptop | Start service of platform : org.bonitasoft.engine.classloader.ClassLoaderServiceImpl
|09:44:26.438|main|INFO |o.b.e.a.i.PlatformAPIImpl| THREAD_ID=1 | HOSTNAME=manu-laptop | Start service of platform : org.bonitasoft.engine.cache.ehcache.PlatformEhCacheCacheService
|09:44:26.490|main|INFO |o.b.e.a.i.PlatformAPIImpl| THREAD_ID=1 | HOSTNAME=manu-laptop | Start service of platform : org.bonitasoft.engine.service.BonitaTaskExecutor
...
|09:44:26.708|main|INFO |o.b.e.a.i.t.SetServiceState| THREAD_ID=1 | HOSTNAME=manu-laptop | TENANT_ID=1 | start tenant-level service org.bonitasoft.engine.cache.ehcache.EhCacheCacheService on tenant with ID 1
|09:44:26.718|main|INFO |o.b.e.a.i.t.SetServiceState| THREAD_ID=1 | HOSTNAME=manu-laptop | TENANT_ID=1 | start tenant-level service org.bonitasoft.engine.business.data.impl.JPABusinessDataRepositoryImpl on tenant with ID 1
Enter fullscreen mode Exit fullscreen mode

Let's code our first process

Our platform is running, we can now create a LoanRequestProcessBuilder class that builds a Bonita process and returns a DesignProcessDefinition.
Let's build the process designed on the diagram above.

package org.bonitasoft.loanrequest.process

// imports removed for readability

const val ACTOR_REQUESTER = "Requester"
const val ACTOR_VALIDATOR = "Validator"

const val START_EVENT = "Start Request"
const val REVIEW_REQUEST_TASK = "Review Request"
const val DECISION_GATEWAY = "isAccepted"
const val SIGN_CONTRACT_TASK = "Sign contract"
const val NOTIFY_REJECTION_TASK = "Notify rejection"
const val ACCEPTED_END_EVENT = "Accepted"
const val REJECTED_END_EVENT = "Rejected"

const val CONTRACT_AMOUNT = "amount"

class LoanRequestProcessBuilder {

    fun buildExampleProcess(): DesignProcessDefinition {
        val processBuilder = ProcessDefinitionBuilder().createNewInstance("LoanRequest", "1.0")
        // Define the actors of the process:
        processBuilder.addActor(ACTOR_REQUESTER, true) // only requester can initiate a new process
        processBuilder.addActor(ACTOR_VALIDATOR) // only requester can initiate a new process
        // Define the tasks
        processBuilder.addUserTask(REVIEW_REQUEST_TASK, ACTOR_VALIDATOR)
        processBuilder.addUserTask(SIGN_CONTRACT_TASK, ACTOR_REQUESTER) // Imagine this task involve paper signing

        // For completion, this auto-task should have a connector on it,
        // to notify the rejection (through email connector, for example):
        processBuilder.addAutomaticTask(NOTIFY_REJECTION_TASK)

        // Define the events:
        processBuilder.addStartEvent(START_EVENT)
        processBuilder.addEndEvent(ACCEPTED_END_EVENT)
        processBuilder.addEndEvent(REJECTED_END_EVENT)
        // Define the Gateway:
        processBuilder.addGateway(DECISION_GATEWAY, GatewayType.EXCLUSIVE)
        // Define transitions:
        processBuilder.addTransition(START_EVENT, REVIEW_REQUEST_TASK)
        processBuilder.addTransition(REVIEW_REQUEST_TASK, DECISION_GATEWAY)
        processBuilder.addTransition(DECISION_GATEWAY, SIGN_CONTRACT_TASK,
                // let's simulate a human decision with a random accepted / rejected decision:
                ExpressionBuilder().createGroovyScriptExpression("random decision", "new java.util.Random(System.currentTimeMillis()).nextBoolean()", "java.lang.Boolean")
        )
        processBuilder.addDefaultTransition(DECISION_GATEWAY, NOTIFY_REJECTION_TASK) // Default transition, taken is expression above returns false
        processBuilder.addTransition(SIGN_CONTRACT_TASK, ACCEPTED_END_EVENT)
        processBuilder.addTransition(NOTIFY_REJECTION_TASK, REJECTED_END_EVENT)

        // Define a contract on the process initiation:
        processBuilder.addContract().addInput(CONTRACT_AMOUNT, Type.DECIMAL, "Amount of the loan requested")
        // Here we imagine a more complex contract with more inputs...

        return processBuilder.process
    }

}
Enter fullscreen mode Exit fullscreen mode

Then this process must be deployed and enabled. Let's create a dedicated class:

package org.bonitasoft.loanrequest.process

// imports...

class ProcessDeployer {

    @Throws(BonitaException::class)
    fun deployAndEnableProcessWithActor(designProcessDefinition: DesignProcessDefinition,
                                        requesterActor: String,
                                        requesterUser: User,
                                        validatorActor: String,
                                        validatorUser: User): ProcessDefinition {
        // Create the Actor Mapping with our Users:
        val requester = Actor(requesterActor)
        requester.addUser(requesterUser.userName)
        val validator = Actor(validatorActor)
        validator.addUser(validatorUser.userName)
        val actorMapping = ActorMapping()
        actorMapping.addActor(requester)
        actorMapping.addActor(validator)

        // Create the Business Archive to deploy:
        val businessArchive = BusinessArchiveBuilder().createNewBusinessArchive()
                .setProcessDefinition(designProcessDefinition)
                // set the actor mapping so that the process is resolved and can then be enabled:
                .setActorMapping(actorMapping)
                .done()

        with(apiClient.processAPI) {
            val processDefinition = deploy(businessArchive)
            enableProcess(processDefinition.id)
            return processDefinition
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Let's now call our build and deploy methods from our main application:

val apiClient = APIClient()

// Log in on Engine API:
loginAsTenantAdministrator()
// Create business users to interact with the process:
val requester = createNewUser("requester", "bpm", "Requester", "LoanRequester")
val validator = createNewUser("validator", "bpm", "Validator", "LoanValidator")
// Use this newly created users to create and execute the process flow:
loginWithAnotherUser(requester)
val processDefinition = createAndDeployProcess(requester, validator)

// Utility function to log in to Bonita with the provided Administrator User:
private fun loginAsTenantAdministrator() {
    apiClient.logout()
    apiClient.login(TENANT_ADMIN_NAME, TENANT_ADMIN_PASSWORD)
}

// Utility function to create a User in Bonita:
private fun createNewUser(userName: String, password: String, firstName: String, lastName: String): User {
    return apiClient.identityAPI.createUser(userName, password, firstName, lastName)
}

// Utility function to log in to Bonita with a specific User:
private fun loginWithAnotherUser(newUser: User) {
    apiClient.logout()
    apiClient.login(newUser.userName, "bpm")
}

private fun createAndDeployProcess(initiator: User, validator: User): ProcessDefinition {
    // Create the process:
    val designProcessDefinition = LoanRequestProcessBuilder().buildExampleProcess()
    // Deploy the process and enable it:
    return ProcessDeployer().deployAndEnableProcessWithActor(
            designProcessDefinition, ACTOR_REQUESTER, initiator, ACTOR_VALIDATOR, validator)
}
Enter fullscreen mode Exit fullscreen mode

At this point, our process is created and deployed in Bonita.
Let's check this by writing a HTTP endpoint that lists all existing processes.
To do so, we need to add a simple Spring Boot dependency and its JSON library, to return the results in JSON format:
In this file build.gradle.kts, in the dependencies { } section

    // Libs to expose Rest API through an embedded application server:
    implementation("org.springframework.boot:spring-boot-starter-web:2.1.6.RELEASE")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.9.8")
Enter fullscreen mode Exit fullscreen mode

Now we can write a simple Spring MVC controller to expose our processes through an HTTP API:

package org.bonitasoft.loanrequest.api

import org.bonitasoft.engine.api.APIClient
import org.bonitasoft.engine.bpm.process.ProcessDeploymentInfo
import org.bonitasoft.engine.search.SearchOptionsBuilder
import org.bonitasoft.loanrequest.process.CONTRACT_AMOUNT
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RestController

@RestController
// our apiClient is automagically :-) injected by Spring:
class ProcessController(val apiClient: APIClient) {

    // Expose the deployed processes through Rest APIs:
    @GetMapping("/processes")
    fun list(): List<ProcessDeploymentInfo> {
        apiClient.login("install", "install")
        val result = apiClient.processAPI.searchProcessDeploymentInfos(SearchOptionsBuilder(0, 100).done()).result
        apiClient.logout()
        return result
    }
}
Enter fullscreen mode Exit fullscreen mode

Finally, let's restart our application with ./gradlew bootRun.
Our application starts, creates and deploys our process.
Let's access http://localhost:8080/processes to list our processes. The result should be something like:

[
    {
        "id": 1,
        "name": "LoanRequest",
        "version": "1.0",
        "displayDescription": null,
        "deploymentDate": "2019-07-05T09:33:21.490+0000",
        "deployedBy": 1,
        "configurationState": "RESOLVED",
        "activationState": "ENABLED",
        "processId": 7450221031288910000,
        "displayName": "LoanRequest",
        "lastUpdateDate": "2019-07-05T09:33:21.607+0000",
        "iconPath": null,
        "description": null
    }
]
Enter fullscreen mode Exit fullscreen mode

Beware that the field "processId" is incorrect, due to a limitation of the JSON / Javascript Long size, which is smaller that the Long size in Java (or Kotlin). The symptom is that the last digits are 0000 instead of a real value.
This limitation only exists if you use JSON / Javascript to display a Long value.
To bypass this limitation, we could convert this value as a String before returning it through Rest APIs.

Deploy a process designed with Bonita Studio

If you have designed your process using Bonita Studio (graphical tool), you can deploy the generated .bar file through the APIs as well. To do so, place your .bar file somewhere in the classpath of the application and load it like this:

class ProcessLoader {
    fun loadProcessFromBar(barFilePath: String): BusinessArchive {
        this.javaClass.getResourceAsStream(barFilePath).use { resourceAsStream ->
            return BusinessArchiveFactory.readBusinessArchive(resourceAsStream)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

and make the call to the "load" function and then deploy:

val processFromBar = ProcessLoader().loadProcessFromBar("/SimpleProcess--1.0.bar")
ProcessDeployer().deployAndEnableBusinessArchiveWithoutAnyActor(processFromBar)
Enter fullscreen mode Exit fullscreen mode

We can see that our process named SimpleProcess is now deployed:

|15:50:59.107|main|INFO |o.b.e.b.BusinessArchiveServiceImpl| THREAD_ID=1 | HOSTNAME=Ubuntu | TENANT_ID=1 | The user <requester> has installed process <SimpleProcess> in version <1.0> with id <6733385816066783268>
|15:50:59.119|BonitaTaskExecutor|INFO |o.b.e.c.ClassLoaderServiceImpl| THREAD_ID=60 | HOSTNAME=Ubuntu | Refreshing classloader with key: PROCESS:6733385816066783268
|15:50:59.200|main|INFO |o.b.e.a.i.t.p.EnableProcess| THREAD_ID=1 | HOSTNAME=Ubuntu | TENANT_ID=1 | The user <requester> has enabled process <SimpleProcess> in version <1.0> with id <6733385816066783268>
Enter fullscreen mode Exit fullscreen mode

The full example can be found in the source code.

Warning: be aware that a .bar file generated by a specific Studio version is compatible only with the same version of Bonita you imported in your Gradle dependencies. If you update the dependency to a newer version, you may need to regenerate the .bar file with the same newer version of Bonita Studio.

Interact with the process

Our platform and process are now ready to be executed.
We have to set interactions with the process to continue the execution flow.
The interactions depend on the design of our process.
Here is an example of the application flow:

...
executeProcess(requester, validator, processDefinition)

@Throws(BonitaException::class)
fun executeProcess(initiator: User, validator: User, processDefinition: ProcessDefinition) {
    // Start a new Loan request with an amount of 12000.0 (€ Euro):
    val processInstance = apiClient.processAPI.startProcessWithInputs(processDefinition.id, mapOf(Pair(CONTRACT_AMOUNT, 12000.0)))

    // Now the validator needs to review it:
    loginWithAnotherUser(validator)
    // Wait for the user task "Review Request" to be ready to execute, using a user member of "Validator" actor:
    val reviewRequestTask = waitForUserTask(validator, processInstance.id, REVIEW_REQUEST_TASK)

    // Take the task and execute it:
    apiClient.processAPI.assignAndExecuteUserTask(validator.id, reviewRequestTask.id, emptyMap())

    // If the request has been accepted, wait for the "Sign contract" task to be ready and execute it:
    val signContractTask = waitForUserTask(initiator, processInstance.id, SIGN_CONTRACT_TASK)
    apiClient.processAPI.assignAndExecuteUserTask(initiator.id, signContractTask.id, emptyMap())

    // Wait for the whole process instance to finish executing:
    waitForProcessToFinish()

    println("Instance of Process LoanRequest(1.0) with id ${processInstance.id} has finished executing.")
}

// Utility function to wait for a User task to be ready:
fun waitForUserTask(user: User, processInstanceId: Long, userTaskName: String): HumanTaskInstance {
    Awaitility.await("User task should not last so long to be ready :-(")
            .atMost(TEN_SECONDS)
            .pollInterval(FIVE_HUNDRED_MILLISECONDS)
            .until(Callable<Boolean> {
                apiClient.processAPI.getNumberOfPendingHumanTaskInstances(user.id) == 1L
            })
    return apiClient.processAPI.getHumanTaskInstances(processInstanceId, userTaskName, 0, 1)[0]
}

// Utility function to wait for a process to be completed:
fun waitForProcessToFinish() {
    Awaitility.await("Process instance lasts long to complete")
            .atMost(TEN_SECONDS)
            .pollInterval(FIVE_HUNDRED_MILLISECONDS)
            .until(Callable<Boolean> {
                apiClient.processAPI.numberOfArchivedProcessInstances == 1L
            })
}

Enter fullscreen mode Exit fullscreen mode

Bonus: cherry on the top

Spring Boot allows you to easily tune the banner that is displayed when an application starts.
Simply put a banner.txt file in the resources folder with some ASCII art:

 _                        ______                           _
| |                       | ___ \                         | |
| |     ___   __ _ _ __   | |_/ /___  __ _ _   _  ___  ___| |_
| |    / _ \ / _` | '_ \  |    // _ \/ _` | | | |/ _ \/ __| __|
| |___| (_) | (_| | | | | | |\ \  __/ (_| | |_| |  __/\__ \ |_
\_____/\___/ \__,_|_| |_| \_| \_\___|\__, |\__,_|\___||___/\__|
                                        | |
                                        |_|
Enter fullscreen mode Exit fullscreen mode

Expose business monitoring on our application flow

Maybe we want our application to expose APIs to be able to follow the flow of our processes.
Simply add the following Rest Controller class to expose the running and completed cases (process instances):

package org.bonitasoft.loanrequest.api

import org.bonitasoft.engine.api.APIClient
// ...
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController

@RestController
class CaseController(val apiClient: APIClient) {

    // Expose the open process instances (=cases not completed)
    @GetMapping("/cases")
    fun list(): List<ProcessInstance> {
        apiClient.login("install", "install")
        try {
            return apiClient.processAPI
                    .searchOpenProcessInstances(SearchOptionsBuilder(0, 100).done())
                    .result
        } finally {
            apiClient.logout()
        }
    }

    // Expose the finished process instances (=cases completed)
    @GetMapping("/completedcases")
    fun listCompleted(): List<ArchivedProcessInstance> {
        apiClient.login("install", "install")
        try {
            return apiClient.processAPI
                    .searchArchivedProcessInstances(SearchOptionsBuilder(0, 100).done())
                    .result
        } finally {
            apiClient.logout()
        }
    }

}
Enter fullscreen mode Exit fullscreen mode

Similarly, to expose the tasks ready to execute, add the following Rest Controller class:

package org.bonitasoft.loanrequest.api

// ...
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RestController

@RestController
class TaskController(val apiClient: APIClient) {

    @GetMapping("/tasks")
    fun list(): List<HumanTaskInstance>? {
        apiClient.login("install", "install")
        val result = apiClient.processAPI.searchMyAvailableHumanTasks(
                apiClient.session.userId,
                SearchOptionsBuilder(0, 100).done())
                .result
        apiClient.logout()
        return result
    }
}
Enter fullscreen mode Exit fullscreen mode

Full example

This repository contains the full code of this example, ready to build / run.

Build your application

Run ./gradlew build to build the binaries. It will generate a jar file in build/libs/ folder.

Run your application

Simply run the gradle command:

./gradlew bootRun
Enter fullscreen mode Exit fullscreen mode

or run the previously-built jar file:

java -jar build/libs/bonita-loan-request-application.jar
Enter fullscreen mode Exit fullscreen mode

You can then access the list of processes by opening a web browser at http://localhost:8080/processes.
The list of cases (process instances) is available at http://localhost:8080/cases.
The list of finished cases (completed process instances) is available at http://localhost:8080/completedcases.
The list of tasks is available at http://localhost:8080/tasks.

Configure the database

When nothing is specified, an embedded H2 database is created and used.
To use a different database, in folder resources create a standard Spring Boot configuration file named application.properties (or .yaml)
and set the following properties. Here is an example with PostgreSQL:

# specify the database vendor you wish to use (supported values are h2, mysql, postgres, sqlserver, oracle):
org.bonitasoft.engine.database.bonita.db-vendor=postgres
# specify the URL to connect to your database:
org.bonitasoft.engine.database.bonita.url=jdbc:postgresql://localhost:5432/bonita

# specify the connection user to the database:
org.bonitasoft.engine.database.bonita.user=bonita
# specify the connection password to the database:
org.bonitasoft.engine.database.bonita.password=bpm
org.bonitasoft.engine.database.bonita.datasource.maxPoolSize=3

# specify a different AppServer port if required:
server.port=8080
Enter fullscreen mode Exit fullscreen mode

The provided application.properties file contains examples for all database vendors.

Finally, don't forget to replace the default H2 dependency for your PostgreSQL drivers in file build.gradle.kts:

    // runtime("com.h2database:h2:1.4.199")
    // runtime("mysql:mysql-connector-java:8.0.14")
     runtime("org.postgresql:postgresql:42.2.5")
    // runtime("com.microsoft.sqlserver:mssql-jdbc:7.2.1.jre8")
    // Oracle database drivers are not open-source and thus cannot be included here directly
Enter fullscreen mode Exit fullscreen mode

Top comments (0)