Brainslug

Brainslug

brainslug is a control flow abstraction library. It allows to model business logic flow of an application as a graph of typed nodes, which can be transformed to different representations or be executed within a customisable environment.

Features

  • Builder DSL for flow definitions

  • BPMN 2.0 XML export using the Activiti Model

  • Flow Renderer based on the BPMN symbols

  • Quartz Scheduler for Async Tasks

Example

FlowBuilder simpleFlow = new FlowBuilder() {
  @Override
  public void define() {
    flowId(id("simpleFlow"));

    start(event(id("start")))
      .execute(task(id("task")).display("A Task"))
      .execute(task(id("task2")).display("Another Task"))
        .end(event(id("end")));
  }
};

represents the the following flow:

task_flow

Download

The current version is available in the maven central repository

<dependencies>
...
  <dependency>
    <groupId>com.drobisch</groupId>
    <artifactId>brainslug-core</artifactId>
    <version>...</version>
  </dependency>
...
</dependencies>

Basics

Flow Definition

The flow definition is the central concept of brainslug. A definition defines possible paths / sequence of actions for a desired outcome, e.g. the steps for an ordering process, call to external systems etc. …​

A flow definition is constructed using the FlowBuilder DSL and is internally represented as a directed graph of typed flow nodes.

Example

class HelloWorldFlow extends FlowBuilder {

  public static Identifier helloFlow = id("helloFlow");

  public static Identifier start = id("start");
  public static Identifier helloTask = id("helloTask");

  @Override
  public void define() {
    flowId(helloFlow);

    start(start).execute(task(helloTask, new SimpleTask() {
      @Override
      public void execute(ExecutionContext context) {
        System.out.println(String.format(
            "Hello %s!", context.property("name", String.class))
        );
      }
    }));
  }
}

brainslug provides a set of predefined node types to define the control flow. These types might be extended in additional modules like the BPMN support.

In addition to the examples here in the documentation, the FlowBuilderTest is a good source for examples on how to build flow definitions.

Execution

To execute a flow definition you need to create BrainslugContext which defines all aspects of the execution. The DefaultBrainslugContext will use HashMap and List-based implementations for all aspects related to storing flow instance information. It is possible to add durable persistence by using the JPA-module or by writing custom stores.

Flow Instance

A flow instance is a single execution of a flow definition. A flow instance may have properties which are stored in the PropertyStore to share data between flow nodes while the execution is not completed.

Flow Token

A flow token is a pointer to a flow node in a flow instance. A flow node might have multiple tokens.

Tokens are consumed / deleted when the execution of a single node was successfully, which in turn creates new tokens in the nodes which are considered as the next tokens in the flow or path of execution.

Which succeeding nodes get tokens is decided by the corresponding FlowNodeExecutor of the node. The token includes the information which node execution lead to the creation of token ("where it came from").

Flow Execution Properties

Execution Properties are key-value pairs which can be seen as the input parameters for an flow instance. They are used to influence the execution of an instance by changing the control flow or by acting as input for the execution of tasks.

Example

//  create brainslug context with defaults
BrainslugContext context = new BrainslugContextBuilder().build();
// add the flow definition
FlowDefinition helloFlow = new HelloWorldFlow().getDefinition();
context.addFlowDefinition(helloFlow);
// start the flow with property 'name' = "World"
context.startFlow(helloFlow, newProperties().with("name", "World"));

will execute the helloFlow from the listing above and thus print Hello World! on the console.

Control Flow

Brainslug Flow Node Types

Task Node

A Task Node will be executed for every incoming token and produces one token per outgoing edge. See the [task documentation](task) for details on how to define what a task should do during execution.

single_task

Event Node

An Event Node is triggered by every incoming token and produces one token per outgoing edge. There are different type of event: Start Events, End Events, Wait (Intermediate) Events.

Example

class EventFlow extends FlowBuilder {

  Identifier eventFlowId = id("helloFlow");

  EventDefinition eventFlowStart = event(id("start")).display("Every 5 Seconds");

  EventDefinition fiveSecondsPassed = event(id("wait")).display("After 5 Seconds")
    .timePassed(5, TimeUnit.SECONDS);

  TaskDefinition theTask = task(id("doIt")).display("Do Something");

  @Override
  public void define() {
    flowId(eventFlowId);

    start(eventFlowStart, every(5, TimeUnit.SECONDS))
      .waitFor(fiveSecondsPassed)
      .execute(theTask);
  }
}
event_flow

Start Events mark the start of a flow and are generally the starting point of your flow definition. The waitFor-method takes an event definition which might have duration to wait for and create a intermediate event for it.

End Events mark the end of a flow execution path and are optional.

Choice Node

A Choice Node will be executed for every incoming token. A token is produced for the first outgoing path where the predicate is fullfilled.

Merge Node

A Merge Node merges different execution paths. It will be executed for every incoming token. A token is produced for every outgoing edge.

Example

class ChoiceFlow extends FlowBuilder {

  Identifier choiceFlowId = id("choiceFlow");

  EventDefinition choiceFlowStart = event(id("start"));
  EventDefinition choiceEnd = event(id("end")).display("end");

  Identifier meaningOfLiveChoice = id("meaning_choice");
  Value meaningProperty = property(id("meaning"));

  EqualsExpression equalsFortyTwo = eq(meaningProperty, 42);
  EqualsExpression equalsFortyThree = eq(meaningProperty, 43);

  TaskDefinition meaningfulTask = task(id("meaning_ful")).display("Meaningful Task");
  TaskDefinition meaninglessTask = task(id("meaning_less")).display("Meaningless Task");

  @Override
  public void define() {
    flowId(choiceFlowId);

    start(choiceFlowStart)
      .choice(meaningOfLiveChoice).display("Meaning of live?")
        .when(equalsFortyTwo)
          .execute(meaningfulTask)
        .or()
        .when(equalsFortyThree)
          .execute(meaninglessTask);

    merge(meaningfulTask, meaninglessTask)
      .end(choiceEnd);
  }
}
choice_flow

Parallel Node

A Parallel Node defines flow paths which are meant for parallel execution, if the execution is really concurrent depends on the configuration of the corresponding FlowNodeExecutor. It will be executed for every incoming token. A token is produced for every outgoing edge.

Join Node

A Parallel Node joins or synchronizes multiple concurrent execution paths. It will only be executed if it has tokens from every incoming edge. A token is produced for every outgoing edge.

Example

class ParallelFlow extends FlowBuilder {

  Identifier parallelFlowId = id("parallel_flow");

  EventDefinition flowStart = event(id("start"));
  EventDefinition flowEnd = event(id("end")).display("end");

  TaskDefinition firstTask = task(id("first_task")).display("Do Something");
  TaskDefinition secondTask = task(id("second_task")).display("Do another Thing");

  @Override
  public void define() {
    flowId(parallelFlowId);

    start(flowStart)
      .parallel(id())
        .execute(firstTask)
          .and()
        .execute(secondTask);

    join(firstTask, secondTask)
      .end(flowEnd);
  }
}
parallel_flow

Tasks

Registry

The brainslug context has a Registry where singletons of service classes can be registered:

context.getRegistry().registerService(Delegate.class, new Delegate());

or retrieved:

Delegate delegateService = context.getRegistry().getService(Delegate.class);

Ways to define a task

Inline Task

FlowBuilder inlineTaskFlow = new FlowBuilder() {
  @Override
  public void define() {
    flowId(id("task_flow"));

    start(event(id("start")).display("Start"))
      .execute(task(id("task"), new SimpleTask() {
        @Override
        public void execute(ExecutionContext ctx) {
          ctx.service(ExampleService.class).doSomething();
        }
      }).display("Do Something"))
      .end(event(id("end")).display("End"));
  }
};

In Java 8 the task can be defined using a lambda expression:

ctx -> {
  ctx.service(MyService.class).doSomething();
}

Delegate class

If you do not want to specify the method by name, you can use the Execute-annotation to define which method you want be executed for a task:

class TestDelegate implements Delegate {
  @Execute
  abstract public void execute(TestService testService, ExecutionContext context);
}
FlowDefinition handlerFlow = new FlowBuilder() {
  @Override
  public void define() {
    start(event(id(START)))
      .execute(task(id(TASK)).delegate(TestDelegate.class))
    .end(event(id(END)));
  }
}.getDefinition();

Service Call

You may use service call definition, to directly define the invocation of a method during flow node definition.

FlowDefinition serviceCallFlow = new FlowBuilder() {

  @Override
  public void define() {
    start(event(id(START)))
      .execute(task(id(TASK)).call(method(TestService.class).name("getString")))
    .end(event(id(END)));
  }

}.getDefinition();

Typesafe Service Call

It possible to define service calls using a proxy-based approach similar to Mockito.

public interface TestService {
  String getString();

  String echo(String echo);

  String multiEcho(String echo, String echo2);
}
FlowDefinition serviceCallFlow = new FlowBuilder() {

  @Override
  public void define() {
    Property<String> echoProperty = FlowBuilderSupport.property(id("echo"), String.class);

    TestService testService = service(TestService.class);

    start(event(id(START)))
      .execute(task(id(TASK)).call(method(testService.echo(testService.getString()))))
      .execute(task(id(TASK2)).call(method(testService.echo(value(echoProperty)))))
    .end(event(id(END)));

  }

}.getDefinition();

In this case, the call to the service will be made at execution using the recorded argument values. This will be done using reflection on the instance of the service, which must be available in the Registry.

JPA Support

It is possible to use JPA for the persistence of flow instance information. This support is implemented using querydsl.

Tested Databases:

  • MySQL 5.5

  • PostgreSQL 9.5

  • H2 1.3.170

Setup

Using Spring

Spring configuration classes are provided and just need to be imported. Your application context only needs to provide JPQLTemplates and an EntityManager with the brainslug.jpa.entity package entities registered.

@Configuration
@Import({SpringBrainslugConfiguration.class, SpringDatabaseConfiguration.class})
public class BrainslugConfiguration {

    @Bean
    JPQLTemplates jpqlTemplates() {
      return new HQLTemplates();
    }

    @Bean
    FlowBuilder aFlow() {
        ...
    }
}

Using existing EntityManager

First you need an instance of brainslug.jdbc.Database :

new Database(entityManager, new HQLTemplates()); // adjust templates to your JPA provider

to create a the JPA TokenStore, JPA PropertyStore and JPA AsyncTriggerStore

JpaTokenStore jpaTokenStore = new JpaTokenStore(...)
JpaAsyncTriggerStore jpaAsyncTriggerStore = new JpaAsyncTriggerStore(...)
JpaPropertyStore jpaPropertyStore = new JpaPropertyStore(...)

These can than be provided to the BrainslugContextBuilder:

new BrainslugContextBuilder()
  .withTokenStore(jpaTokenStore)
  .withAsyncTriggerStore(jpaAsyncTriggerStore)
  .withPropertyStore(jpaPropertyStore)
  .build()

Spring Support

It is possible integrate brainslug with the Spring application context, so that the Spring Beans are available in the execution context. To use it, you need to get the brainslug-spring module.

Setup

Just import the spring configuration class into you application context.

Calls to the ExecutionContext or the Registry will then return the beans from the Spring application context.

All beans of type FlowBuilder will be made available in the BrainslugContext as well:

@Configuration
@Import(brainslug.spring.SpringBrainslugConfiguration.class)
public class ConfigurationExample {

  @Component
  public static class SpringExampleTask implements SimpleTask {
    Environment environment;

    @Autowired
    public SpringExampleTask(Environment environment) {
      this.environment = environment;
    }

    @Override
    public void execute(ExecutionContext context) {
      printHello(context.property("name", String.class));

      context.service(SpringExampleTask.class).printHello("again");
    }

    public void printHello(String name) {
      System.out.println(
        format("Hello %s!", name)
      );
    }
  }

  @Bean
  FlowBuilder flowBuilder() {
    return new FlowBuilder() {
      @Override
      public void define() {
        flowId(id("spring-flow"));

        start(task(id("spring-task")).delegate(SpringExampleTask.class));
      }
    };
  }

  public static void main(String[] args) {
    AnnotationConfigApplicationContext applicationContext =
      new AnnotationConfigApplicationContext(ConfigurationExample.class);

    BrainslugContext brainslugContext = applicationContext.getBean(BrainslugContext.class);

    brainslugContext.startFlow(FlowBuilder.id("spring-flow"),
      newProperties().with("name", "World"));
  }

}

Will output the two lines:

Hello World!
Hello again!

Custom BrainslugContext

To configure the BrainslugContext in your ApplicationContext you can use the SpringBrainslugContextBuilder:

@Configuration
@Import(brainslug.spring.SpringBrainslugConfiguration.class)
public class ContextBuilderExample {
  @Bean
  SpringBrainslugContextBuilder contextBuilder() {
    return new SpringBrainslugContextBuilder()
      .withPropertyStore(new HashMapPropertyStore());
      // ...
  }
}

BPMN Support

brainslug is able to export flow definitions to BPMN 2.0 as rendered image or XML definitions file.

The following examples use this flow:

FlowBuilder simpleFlow = new FlowBuilder() {
  @Override
  public void define() {
    flowId(id("simpleFlow"));

    start(event(id("start")))
      .execute(task(id("task")).display("A Task"))
      .execute(task(id("task2")).display("Another Task"))
        .end(event(id("end")));
  }
};

BPMN Renderer

Format format = Format.JPG;
JGraphBpmnRenderer renderer = new JGraphBpmnRenderer(new DefaultSkin());

String fileName = simpleFlow.getName() + "." + format.name();
FileOutputStream outputFile = new FileOutputStream(fileName);

renderer.render(simpleFlow, outputFile, format);
task_flow

BPMN XML Export

BpmnModelExporter bpmnModelExporter = new BpmnModelExporter();
String bpmnXml = bpmnModelExporter.toBpmnXml(simpleFlow);

will produce

<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:activiti="http://activiti.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.activiti.org/test">
    <process id="471ff134-4050-497b-bf80-d1fa5164f311" name="471ff134-4050-497b-bf80-d1fa5164f311" isExecutable="true">
        <startEvent id="start" name="start"></startEvent>
        <serviceTask id="task" name="A Task"></serviceTask>
        <serviceTask id="task2" name="Another Task"></serviceTask>
        <endEvent id="end" name="end"></endEvent>
        <sequenceFlow sourceRef="start" targetRef="task"></sequenceFlow>
        <sequenceFlow sourceRef="task" targetRef="task2"></sequenceFlow>
        <sequenceFlow sourceRef="task2" targetRef="end"></sequenceFlow>
    </process>
    <bpmndi:BPMNDiagram id="BPMNDiagram_471ff134-4050-497b-bf80-d1fa5164f311">
        <bpmndi:BPMNPlane bpmnElement="471ff134-4050-497b-bf80-d1fa5164f311" id="BPMNPlane_471ff134-4050-497b-bf80-d1fa5164f311"></bpmndi:BPMNPlane>
    </bpmndi:BPMNDiagram>
</definitions>

Motivation

"Do we really need another Workflow-BPM-whatever engines on the JVM? Smells like 'Not invented here'!"

Activiti, camunda BPM and jBPM might be the most popular on the list, but they all have some kind of bigger legacy and target environment they have to deal with and its more difficult to just get to their core.

For example, Activiti is very focused on BPMN 2.0 and its jBPM 4 roots, jBPM5 is coming from the business rule side, BPEL engines make most sense in WS-* environments.

To some degree they all share some strong assumptions:

  • Relational persistence (with JDBC), execution semantics are often coupled to the database transaction management

  • High initial learning effort required to learn the workflow description language like BPMN, BPEL

  • Dynamic expression evaluation, at least to control the flow

  • You are living in a container (Tomcat / Spring, Java Enterprise Edition / JBoss)

  • XML based descriptions / configurations

brainslug_big aims to provide a small workflow library for the JVM without these strings attached.

Design Goals

A developer using brainslug should be able to:

  • have no external dependencies in the core parts (model and execution), except for logging

  • understand the execution model by reading the code

  • customize all aspects the library (including persistence and transaction management)

  • not want to buy commercial (sometimes called enterprise) support