Flowable provides a set of Spring Boot starters to help you embed the different engines (i.e., BPMN, DMN and CMMN) and to expose their RESTful APIs.

In this post, I'll walk you through the steps I followed to create a RESTful API (Resource Server) that embeds Flowable's BPMN engine, exposes the BPMN engine's RESTful API and leverages Spring Security’s support for OAuth 2.0 and Jason Web Tokens (JWTs).

Spring Boot

Getting Started

According to the Spring Boot Getting Started guide you should use the Spring Initializr to bootstrap your application:

I added the following dependencies: Spring Boot Starter Security, Spring Boot Starter Web, Spring Security OAuth2 Resource Server, Spring Security OAuth2 JOSE (Javascript Object Signing and Encryption), Spring Security Config, Spring Boot Starter HAETOAS, Evo Inflector, Spring Boot Starter Data Rest, Spring Boot Starter Data JPA, H2 Database and Lombok.

Flowable

In order to embed Flowable's BPMN engine and expose the BPMN engine's RESTful API we need to update the project's pom.xml:

  <dependencies>
  
    ...
    
    <dependency>
      <groupId>org.flowable</groupId>
      <artifactId>flowable-spring-boot-starter</artifactId>
      <version>${flowable.version}</version>
    </dependency>

    <dependency>
      <groupId>org.flowable</groupId>
      <artifactId>flowable-spring-boot-starter-process-rest</artifactId>
      <version>${flowable.version}</version>
    </dependency>
    
    <dependency>
      <groupId>org.flowable</groupId>
      <artifactId>flowable-ldap</artifactId>
      <version>${flowable.version}</version>
    </dependency>    
   
  </dependencies>

By default, certain folders on the classpath are automatically scanned:

  • /apps: Looks for all files ending with .zip or .bar and deploys them
  • /cases: Looks for all files ending with .cmmn, .cmmn11, .cmmn.xml or .cmmn11.xml and deploys them
  • /dmn: Looks for all files ending with .dmn, .dmn11, .dmn.xml or .dmn12.xml and deploys them
  • /forms: Looks for all files ending with .form and deploys them
  • /processes: Looks for all files ending with .bpmn20.xml or .bpmn and deploys them

For example:

├── /src
     └── /main
           └── /java
           └── /resources
                 └── /apps
                       ├── hr-app.zip
                 └── /processes
           ├── application.properties
     └── /test

Spring Security

I extended Spring Security's WebSecurityConfigurerAdapter:

package org.serendipity.restapi.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;

import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

  @Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}") String jwkSetUri;

  @Override
  protected void configure(HttpSecurity http) throws Exception {

    // Enable CORS, disable CSRF, every request must be authenticated
        
    http.cors().and().csrf().disable().authorizeRequests().anyRequest().authenticated();

    // OAuth2 Resource Server configuration

    http.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);

  }

  @Bean
  JwtDecoder jwtDecoder() {
    return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri).build();
  }

  @Bean
  CorsConfigurationSource corsConfigurationSource() {

    final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();

    source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());

    return source;
  }

}

And updated the application.properties:

spring.main.banner-mode=off
spring.jpa.open-in-view=false

server.port=3001

# Logging
logging.level.root=INFO
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
logging.level.org.springframework.security=DEBUG

# Spring Security OAuth2 Resource Server
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost:10001/auth/realms/development/protocol/openid-connect/certs

# Spring Data REST
spring.data.rest.base-path: /api

# Spring JPA
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:~/serendipity-db/db;AUTO_SERVER=TRUE;AUTO_SERVER_PORT=9091;DB_CLOSE_DELAY=-1
spring.datasource.username=admin
spring.datasource.password=secret
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=create-drop

# H2 Console
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
spring.h2.console.settings.trace=false
spring.h2.console.settings.web-allow-others=false

To point to my Authorization Server's JWK Set Uri.

For example:

curl http://localhost:10001/auth/realms/development/protocol/openid-connect/certs

Sample output:

{
    "keys": [
        {
            "kid": "5E-jGIlDdpgjnhJzbdS3XXeZWZCmRUB85Snfp5IwyjI",
            "kty": "RSA",
            "alg": "RS256",
            "use": "sig",
            "n": "t3609odH5dIcPZUd4yFTjilR89MigdbgwnzFII82hzeazIsZivoSVWTfWG4620-zYN5PyFPKsaW_0IF7MPCeDXgRZ0odiizRQdhczZQA-zVrlqxy93SqRpjqRd_F3bOmwQZAsdepQefGNrTIArpq62s5ycUQTf-qbzsnCQugb5_SRa7u1VJaBgb0jTM-L304TSiW1vUFg2Th4fyQqVL4xrJuBPrrJCBs9nx9GPJDD8fXZtDMLlXRYhzZjW8zfqHSyA46JCDbK4-Tt6Ra6dNNKz8n2leSbUcf9NgBsiGHn23SnkM5GzbKbI-i_oQTvueP2psZzm8_Oyi3KYLtjNwBWw",
            "e": "AQAB",
            "x5c": [
                "MIICrTCCAZUCBgFvIWzNBjANBgkqhkiG9w0BAQsFADAaMRgwFgYDVQQDDA9TZXJlbmRpcGl0eSBDRVAwHhcNMTkxMjIwMDM0NzU2WhcNMjkxMjIwMDM0OTM2WjAaMRgwFgYDVQQDDA9TZXJlbmRpcGl0eSBDRVAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3frT2h0fl0hw9lR3jIVOOKVHz0yKB1uDCfMUgjzaHN5rMixmK+hJVZN9YbjrbT7Ng3k/IU8qxpb/QgXsw8J4NeBFnSh2KLNFB2FzNlAD7NWuWrHL3dKpGmOpF38Xds6bBBkCx16lB58Y2tMgCumrraznJxRBN/6pvOycJC6Bvn9JFru7VUloGBvSNMz4vfThNKJbW9QWDZOHh/JCpUvjGsm4E+uskIGz2fH0Y8kMPx9dm0MwuVdFiHNmNbzN+odLIDjokINsrj5O3pFrp000rPyfaV5JtRx/02AGyIYefbdKeQzkbNspsj6L+hBO+54/amxnObz87KLcpgu2M3AFbAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAFVnIzwaPDVTvQ3q1QRczCBG348SqpDV1rrh5omMTwiqDI6uVWqgz7ij4I6XkS2w3B+eccxecE5cio0ZkRoxT1Ft4gZt5rzOIVch0nGIJp7NgTd5OKXE3BfJ0hcji+QPMosB7VKBWC82zu/n13DLoNzVn0qPUvh/1cvg/3cjDE5WKqPzaJleoRSdQhATMo5PyyqRkNVaqObuFWJffrErJNRFfeFXUFQvstSxcOmdKkuGZcMlLKbfug5qfAHK+0xCL/nOCMZizEhnAZqA7qzXqaM7NMUg48XN6x0z1Vxz5IDDfpVlN9TbDnNiSGiPJLENY5rm31kDjm9iHp4FrHF8FzI="
            ],
            "x5t": "bHB0yas9UeCu3AzfcE0Uu8omPhY",
            "x5t#S256": "otHl46Z-oZbRfHnTezn5gtr1rjHUhmWTvGYV3g_q8lc"
        }
    ]
}

Now we can build the application:

mvn clean
mvn package

And launch it:

java -jar target/serendipity-rest-api-core-0.0.1-SNAPSHOT.jar

We can use Postman to obtain an access token from Keycloak:

We can also use Postman to test the application's RESTful API:

package org.serendipity.restapi.controller;

import lombok.extern.slf4j.Slf4j;
import org.serendipity.restapi.assembler.IndividualModelAssembler;
import org.serendipity.restapi.entity.Individual;
import org.serendipity.restapi.model.IndividualModel;
import org.serendipity.restapi.repository.IndividualRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.rest.webmvc.BasePathAwareController;
import org.springframework.data.web.PagedResourcesAssembler;
import org.springframework.hateoas.PagedModel;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.server.ResponseStatusException;

@BasePathAwareController
@Slf4j
public class IndividualController {

  private final IndividualRepository repository;
  private final IndividualModelAssembler assembler;
  private final PagedResourcesAssembler<Individual> pagedResourcesAssembler;

  public IndividualController(IndividualRepository repository,
                              IndividualModelAssembler assembler,
                              PagedResourcesAssembler<Individual> pagedResourcesAssembler) {

    this.repository = repository;
    this.assembler = assembler;
    this.pagedResourcesAssembler = pagedResourcesAssembler;
  }

  @GetMapping("/individuals")
  @PreAuthorize("hasAuthority('SCOPE_individual:read')")
  public ResponseEntity<PagedModel<IndividualModel>> findAll(Pageable pageable) {

    Page<Individual> individuals = repository.findAll(pageable);
    PagedModel<IndividualModel> individualModels = pagedResourcesAssembler.toModel(individuals, assembler);

    return ResponseEntity.ok(individualModels);
  }

  @GetMapping("/individuals/{id}")
  @PreAuthorize("hasAuthority('SCOPE_individual:read')")
  public ResponseEntity<IndividualModel> findById(
      @PathVariable("id") final Long id) throws ResponseStatusException {

    Individual entity = repository.findById(id).orElseThrow(() ->
        new ResponseStatusException(HttpStatus.NOT_FOUND));

    return ResponseEntity.ok(assembler.toModel(entity));
  }

}

For example:

And to test the BPMN engine's RESTful API:

We can also navigate to the H2 console:

Flowable UI Applications

  • Flowable Identity Management
  • Flowable Modeler
  • Flowable Task
  • Flowable Admin

You can download the Flowable open source distribution from the Flowable web site.

Externalised Configuration

The Flowable Web applications take advantage of Spring Boot's support for externalised configuration:

spring.main.banner-mode=off

# Logging
logging.level.root=INFO
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
logging.level.org.springframework.security=DEBUG

# Spring JPA
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:~/serendipity-db/db;AUTO_SERVER=TRUE;AUTO_SERVER_PORT=9091;DB_CLOSE_DELAY=-1
spring.datasource.username=admin
spring.datasource.password=secret
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=update

# Default Admin Accounts
flowable.idm.app.admin.user-id=flowable
flowable.idm.app.admin.password=secret
flowable.idm.app.admin.first-name=
flowable.idm.app.admin.last-name=Administrator
flowable.idm.app.admin.email=admin@serendipity.org.au

flowable.common.app.idm-admin.user=flowable
flowable.common.app.idm-admin.password=secret

flowable.modeler.app.deployment-api-url=http://localhost:9999/flowable-task/app-api

# LDAP
flowable.idm.ldap.enabled=true
flowable.idm.ldap.server=ldap://localhost
flowable.idm.ldap.port=10389
flowable.idm.ldap.user=cn=admin,dc=flowable,dc=org
flowable.idm.ldap.password=secret
flowable.idm.ldap.base-dn=dc=flowable,dc=org
flowable.idm.ldap.user-base-dn=ou=users,dc=flowable,dc=org
flowable.idm.ldap.group-base-dn=ou=groups,dc=flowable,dc=org
flowable.idm.ldap.query.user-by-id=(&(objectClass=inetOrgPerson)(uid={0}))
flowable.idm.ldap.query.user-by-full-name-like=(&(objectClass=inetOrgPerson)(|({0}=*{1}*)({2}=*{3}*)))
flowable.idm.ldap.query.all-users=(objectClass=inetOrgPerson)
flowable.idm.ldap.query.groups-for-user=(&(objectClass=groupOfUniqueNames)(uniqueMember={0}))
flowable.idm.ldap.query.all-groups=(objectClass=groupOfUniqueNames)
flowable.idm.ldap.query.group-by-id=(&(objectClass=groupOfUniqueNames)(uniqueId={0}))
flowable.idm.ldap.attribute.user-id=uid
flowable.idm.ldap.attribute.first-name=cn
flowable.idm.ldap.attribute.last-name=sn
flowable.idm.ldap.attribute.email=mail
flowable.idm.ldap.attribute.group-id=cn
flowable.idm.ldap.attribute.group-name=cn
flowable.idm.ldap.cache.group-size=10000
flowable.idm.ldap.cache.group-expiration=180000

Flowable Identity Management

To launch Flowable's Identity Management application:

java -jar flowable-idm.war

Then navigate to: http://localhost:8080/flowable-idm

Flowable Modeler

To launch Flowable's Modeler application:

java -jar flowable-modeler.war

Then navigate to: http://localhost:8888/flowable-modeler

Flowable Task

To launch Flowable's Task application:

java -jar flowable-task.war

Then navigate to: http://localhost:9999/flowable-task

Flowable Admin

To launch Flowable's Admin application:

java -jar flowable-admin.war

Then navigate to: http://localhost:9988/flowable-admin

Source Code:
References: