Workflow-lite is a simple workflow engine using the Spring framework. As of now, it can be used to define a simple sequential workflow.
There are few blogs on how to use Spring to have a simple sequential workflow. But they mostly deal with sequential action execution without support for conditional branching. Also, in most cases, the interface for performing action takes some context which is used to pass the inputs from one action to another which makes the actions interdependent.
Workflow-lite on the other hand allows to define the actions as normal Java classes defining their dependencies to be injected using constructor or properties. Even the output of one action can be passed to another using dependency injection and not using the context object.
Add the maven dependency to your pom.xml as follows.
<dependency>
<groupId>org.expedientframework.workflowlite</groupId>
<artifactId>workflow-lite-core</artifactId>
<version>1.0.0</version>
</dependency>
We will define a workflow to calculate the score for a given student. The workflow will take a student object as input and have following actions:
Using the Papyrus plugin create the activity diagram as follow:
All the workflow actions needs to implement the Action interface. Also, instead of directly implementing the interface consider extending the AbstractAction or AbstractAsyncAction as follows:
public class PublishStudentScoreAction extends AbstractAction<ExecutionContext, String>
{
public PublishStudentScoreAction(final String studentName, final int score)
{
this.studentName = studentName;
this.score = score;
}
@Override
public String execute(final ExecutionContext context)
{
return String.format("Student '%s' scored %d marks.", this.studentName, this.score);
}
// Private
private final String studentName;
private final int score;
}
As seen above, the PublishStudentScoreAction simply takes the student name and score as constructor parameters and then in execute() returns a simple formatted string. Note that we are not using ExecutionContext object to pass parameters to actions but using constructor injection. Similarly, implement other actions.
So far we have created UML activity diagram describing the workflow we need to execute and implemented the actions. But how do we link them? This is also a very simple steps. We will use String dependency injection here and define the workflow actions as beans.
<bean class="org.expedientframework.workflowlite.core.samples.CalculateTotalScoreAction">
<constructor-arg value="%{student.scores}" />
</bean>
In above bean definition we have used Spring Expression Language to pass the input. Our expression definition starts with %{ and ends with }. For this example, we are passing the value of property stores on the student object. The student object is our input to the workflow. In our StudentWorkflowExecutionContext we have mentioned that the input to the workflow should be referred as student in the expressions. Also, there are context and output variables available. context refers to the ExecutionContext instance while output refers to the output from previous action. The PublishStudentScoreAction takes two inputs: one from original input referred to as student.name in the expression below and other from previous activity referred to as output below. The student variable refers to Student instance while output is a simple numeric value.
<bean class="org.expedientframework.workflowlite.core.samples.PublishStudentScoreAction">
<constructor-arg name="studentName" value="%{student.name}" />
<constructor-arg name="score" value="%{output}" />
</bean>
Defining the condition
To define the conditional flow we will again use the Spring expression.
Now that we have implemented the actions and also linked them with the given classes, it's time to register the workflows by implementing the WorkflowDefinitionsProvider interface. When the application starts, it checks all the beans implementing this interface and invoke them one by one. The interface definition is simple as follows:
public interface WorkflowDefinitionsProvider
{
public List<InputStream> getDefinitions();
}
It just expects the list of streams of UML files having the workflow definitions. The UmlActivityDefinitionsProvider implements the above interface. It simply takes the file names as input and then will resolve them and return the list of streams. In your application bean, add the following bean definition:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<import resource="spring-beans/workflow-lite-core.xml"/>
<bean id="workflowDefinitions" class="org.expedientframework.workflowlite.core.UmlActivityDefinitionsProvider">
<constructor-arg>
<list>
<value>classpath:workflows/workflow_definitions.uml</value>
</list>
</constructor-arg>
</bean>
</beans>
In above sample, we are including the workflow-lite-core.xml which has the required framework beans defined. Then we have the UmlActivityDefinitionsProvider taking the list of files having the UML definitions. For this example, our UML definitions are in workflow_definitions.uml. You can use classpath or filepath.
The StudentScoreCardWorkflowTest shows how we are going to execute the workflow. We are using TestNG to write the tests as follows:
@ContextConfiguration(locations="classpath:wf_definitions.xml")
public class StudentScoreCardWorkflowTest extends AbstractTestNGSpringContextTests
{
@Test
public void resultForNormalStudent_resultWithoutBonusMarks()
{
final Student student = new Student("John Doe");
student.addScore("History", 60);
student.addScore("Science", 70);
final String result = this.workflowManager.execute(new StudentWorkflowExecutionContext(), student);
assertThat(result).as("Result").isEqualTo("Student 'John Doe' scored 130 marks.");
}
@Test
public void resultForNormalStudent_resultWithBonusMarks()
{
final Student student = new Student("Octavia Wilford");
student.addScore("History", 60);
student.addScore("Science", 70);
final String result = this.workflowManager.execute(new StudentWorkflowExecutionContext(), student);
assertThat(result).as("Result").isEqualTo("Student 'Octavia Wilford' scored 140 marks.");
}
// Private
@Inject
private WorkflowManager workflowManager;
}
The @ContextConfiguration points to the bean XML wf_definitions.xml to be used which imports the workflow-lite beans and specifies the path to the UML definition file as mentioned in previous section. Next we inject the instance of WorkflowManager. The two tests are simple. They create the student instance, adds the marks and then execute the workflow passing the student instance. The logic to detemine whether student has participated in any extracurricular activity or not is simple. If the student name starts with a vowel (aeiou) then we return true else false. So the first test with student name as John Doe will not have any bonus marks added while the second test with student name as Octavia Wilford will have the bonus marks added.
Also, notice that we are passing the instance of StudentWorkflowExecutionContext. The constructor of this class takes the workflow id as input which should be the name of the UML activity and the alias to be used for refering the input which in this case is student since we are passing student instance. The alias is used in the Spring expressions for passing the inputs to actions or evaluating the condition.
In most of the cases an action will perform some asynchronous operation or will wait on some other asynchronous operation to complete. Hence, the overall workflow execution itself needs to be asynchronous. Handling this is very easy. The action needs to simply return a CompletableFuture and that's all. The output from the workflow itself will be a CompletableFuture and consumer should use appropriate methods on it to listen for result or error.