This repository contains one of the jBPM starter applications from jBPM - Build your business application expanded to demonstrate more complete examples.
The original-business-* contains the original starter business application.
- original-business-application-kjar: A kjar project. The kjar project holds the process flows, business rules, optimization and other information needed to implement and run in the jBPM runtime engine. The default starter kjar only has a default configuration files. Adding process flows will be covered later. The will build as is out of the box and installs with GAV of
com.company:business-application-kjar:1.0-SNAPSHOT
. Thepom.xml
that comes out of the box will cause issues with eclipse m2e and so an entry to fix the warnings from m2e has been added to the project'spom.xml
. - original-business-application-model: A simple java project to be used as an external data model for business processes. The default contains an empty POJO at
com.company.model.Model
. - original-business-application-service: The default springboot jBPM service. The default server state is defined in
business-application-service.xml
in the project's root directory. This configuration file defines a deployed and running container with the same GAV value as the business-application-kjar. If you run the server it will initially fail with a java runtime exceptionjava.lang.RuntimeException: Cannot find KieModule: com.company:business-application-kjar:1.0-SNAPSHOT
. To fix this you must install the kjar into the local maven repository withmvn install
from the kjar project. Alternatively the container startup information could be removed from the configuration file. Once the springboot jBPM service is started it is running on localhost port 8090. The jBPM REST endpoint can be found at http://localhost:8090/rest/server and is access restricted. The Authorization is configured in the DefaultWebSecurityConfig.java class of the project.
In order to create and test a simple script I created two projects by copying the respective original-business-application-*
projects.
- first-business-application-kjar:
- first-business-application-service:
The first step building a business process service will be to make a process flow. There are two obvious ways to do this. The first is to use the Business Central Process Designer and the second is to use the Eclipse BPMN 2.0 Modeler.
Using the eclipse plugin would be very simple at this point but it has to be installed in Spring Tool Suite 4. The modeler can be found in the Eclipse Market place under Eclipse BPMN2 Modeler and so is easy to install.
Once installed a simple process flow is created by selecting src/main/resources in the first-business-application-kjar project and then file->new->other->BPMN->BPMN2 Model. In the dialog I selected the name of simple-process.bpmn2 and for Model Object I selected process
. Unfortunately this created the process in the business-application main directory and I had to move it to the first-business-process-kjar/src/main/resources directory. I think right clicking on the resources directory would have created the simple process in the correct location.
The BPMN2 modeler should give a visual interface for process creation. Add a start event, a script task, and an end event, and connected them with arrows. When I first tried to open the process properties by opening the properties
view and selecting the simple-process background I was not able to see all of the properties of a process that should be there. After closing and reopening the simple-process.bpmn2 file in Eclipse with the Bpmn2 Diagram Editor I was able to see all of the properties. Under the Process
tab in the properties view I changed the process name to SimpleProcess
. I then selected the script task and under the Script Task
tab of the properties view I entered the script System.out.println("Hello, World\n");
.
As of version 1.5.0 of the Eclipse BPMN2 Modeler there was no way to change the process ID under the Process Tab of properties. Open the simple-script.bpmn2 file in a text editor and change the processId by hand. The default was <bpmn2:process id="Process_1"
and I changed it to <bpmn2:process id="SimpleProcess"
. Process_1 was also found in another location in the file that needed to be changed as well.
Compile the kjar in maven by selecting the first-business-application-kjar project and then run->run as->maven test. That workd with a few parser errors that I was not concerned about. I then updated the kjar in the local maven repository with run->run as->maven install. Be sure to remember that any change you make to the kjar needs to be followed with maven install
because the kjar is loaded from the local repository.
Next is to invoke the process-flow from within the spring-boot service project. The service project is already configured to load the kjar and since the GAV value of com.company:business-application-kjar:1.0-SNAPSHOT
has not changed the new business process with the SimpleProcess flow should still be loaded. The com.company.service.Application.java
file is modified as follows:
@SpringBootApplication
public class Application implements CommandLineRunner {
@Autowired
private ProcessService processService;
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Override
public void run(String... args) throws Exception {
processService.startProcess("business-application-kjar-1_0-SNAPSHOT", "SimpleProcess");
}
}
Notice that the org.jbpm.services.api.ProcessService
is injected into the application and used by the run
method to start the process. Note also that the deploymentId argument is "business-application-kjar-1_0-SNAPSHOT"
. This is set in the business-application-service.xml configuration file in the root of the service project.
http://localhost:8090/rest/server/containers
- second-business-application-kjar: Holds a simple process flow that takes a "name" parameter and prepends a "Hello " on to the front of it in a script task.
- second-business-application-service: Adds a simple
RestController
to start the process flow, pass a name parameter to it, read and return the result.
In order to manipulate a parameter in a business flow a process variable is added to the simple-process.bpmn2
business process created in the first-business-application-kjar
project.
The simple-process.bpmn2
process is opened in the Eclipse BPMN2 modeler. The background of the process is clicked on (just to be sure) and the properties view is opened. The Data Items
tab is opened selected and the plus +
sign is clicked in the Properties List for Process Simple Process
is clicked. A new process var is created with with the name processVar1
and of Data Type
xs:string
.
The Script Task 1
is clicked and the Script Task
tab is opened. The script in the Script
field is changed to
kcontext.setVariable("processVar1", "Hello " + kcontext.getVariable("processVar1"));
In short, kcontext is available to all business process scripts and allows access to the jBPM API from within a business flow. This new script reads the process variable processVar1
and prepends the string Hello
and stores the result back into the process variable.
Build and install the new kjar into the local maven repository.
Building the service rest interface is just a matter of changing the Application.java
class. Remove the CommandlineRunner
support and add RestController
support. In order to read the result a new jBPM Service Interface is injected, the RuntimeDataService
. The code is as follows:
@SpringBootApplication
@RestController
public class Application {
@Autowired
private ProcessService processService;
@Autowired
private RuntimeDataService runtimeDataService;
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@GetMapping("/hello")
public ResponseEntity<String> sayHello(@RequestParam String name) throws Exception {
Map<String, Object> vars = new HashMap<>();
vars.put("processVar1", name);
Long processInstanceId = processService.startProcess("business-application-kjar-1_0-SNAPSHOT", "SimpleProcess", vars);
for ( VariableDesc var: runtimeDataService.getVariablesCurrentState(processInstanceId) ) {
if ( var.getVariableId().equals("processVar1"))
return ResponseEntity.ok(var.getNewValue());
}
return ResponseEntity.status(HttpStatus.FAILED_DEPENDENCY).body("Processing Error");
}
}
The REST endpoint for this will be http://localhost:8090/hello and a name
parameter is required. The final test URL could be http://localhost:8090/hello?name=World. Invoking with endppoint with your browser will return the result Hello World
.
This capability is added by default to the jBPM sample applications.
- third-business-application-kjar: Same as second-business-application-kjar.
- third-business-application-service: pom.xml modified to point to ../third-business-application-kjar/target/ directory.
No modification is required to run the springboot-jBPM api under docker but the build process done by maven probably needs to be inspected and updated.
The third-business-application-service/pom.xml
file references the local-repository
directory in the third-business-application-kjar/target
.
<fileSet>
<directory>../third-business-application-kjar/target/local-repository/maven</directory>
<outputDirectory>opt/jboss/.m2/repository</outputDirectory>
</fileSet>
This is part of the fabric8-maven-plugin
maven plugin which invokes docker to build the docker image. In order for the plugin to build the correct docker image the <directory>
value must point to the kjar
project. Note that there are two places in the pom.xml
that has this setting. You can change them both -- one is for building a plain docker image and the other is for building an openshift
docker image.
Building the docker image is a two step process. First the kjar local-repository
must be built so that it can be included in the service project.
Build the kjar
project first from the third-business-application-kjar
project.
mvn clean install -Ddocker
Notice that the docker profile is activated with a property setting instead of the usual maven -P setting. Once built there should be a directory target/local-repository
in the third-business-application-kjar
project.
Build the docker image from the third-business-application-service
project. Be sure that you have an internet connection and docker is properly working on your system. The plugin will download the fabric8/java-jboss-openjdk8-jdk
image for the OS and Java runtime.
mvn clean install -Ddocker -Ph2
Now the docker image can be run.
docker run -p 8080:8090 apps/business-application-service:1.0-SNAPSHOT
and the API can be tested with curl
curl http://localhost:8080/hello?name=test
As another general issue working with jBPM is the movement of business process projects in and out of kie-workbench.
- forth-business-central-kjar: Project copied from kie-workbench
- forth-business-application-service: Updated with endpoints and users and groups to handle human tasks.
The heart of the business process is human interactions. The process will be expanded to perform an employee evaluation which is a typical human interaction example in jBPM.
The biggest concern with jBPM and springboot at the moment is creating and managing the business process. The active development for the BPMN process builder is in the kie-workbench but the Eclipse BPMN plugin is closest to the project. The default project so far has been able to handle docker well but the Eclipse plugin builder has not been able to create a project with correct properties as seen earlier when the process id what not able to be set.
The kie-workbench projects can be copied into a java project by using GIT. Git is available when the kie-workbench is running by accessing port 8001.
A project is created in the kie-workbench. As stated it does an employee evaluation the same as the evaluation examples you will find in the jBPM source. There are four process variables, employee, selfeval, hreval, and pmeval. The employee is mapped as an input variable and is assigned to a that employee-user in the Self Evaluation
task. The HR Evaluation
and PM Evaluation
are assigned to the groups HR
and PM
respectively. These must be claimed by members of those groups.
Running the project in the workbench allows each user to login and check for tasks in the task inbox
. When the task is claimed, started, and completed a form is presented that allows the evaluation variables to be entered and saved. Once completed the variables can be inspected in kie-workbench and through the REST interface. Familiarity with creating, running, and managing business processes in kie-workbench is expected.
After building, deploying and testing on kie/business-central workbench the project is pulled with git.
- A directory is created named forth-business-central-kjar
git init
is run in the directory to initialize gitgit add remote kie ssh://krisv@localhost:8001/MySpace/Evaluation
is run to create a connection to the kie/business-central workbench.git pull kie master
is run to pull the code from the workbench into the forth-business-central-kjar directory.
The project can be loaded into Eclipse and built with maven. The project will build properly as is with mvn test
and the kjar can be installed into the local repository with mvn install
.
After the new kjar is installed into the local maven repository with it can be deployed and started with the springboot application. The GAV for the new forth-business-central-kjar application is com.myspace:Evaluation:1.0.0-SNAPSHOT
Remember that currently the deployment in the application is handled by the business-application-service.xml
file. This will be changed to dreploy the new project.
<containers>
<container>
<containerId>Evaluation-1_0_0-SNAPSHOT</containerId>
<releaseId>
<groupId>com.myspace</groupId>
<artifactId>Evaluation</artifactId>
<version>1.0.0-SNAPSHOT</version>
</releaseId>
...
</containers>
The rest interface must be changed to start the evaluation process for a specific employee and then to complete the human tasks based on a userId
. Since there are three different inputs to the evaluation process, selfeval
, hreval
, and pmeval
there should be three different API endpoints for each stage of the evaluation process. In addition, appropriate users and roles must be added to the application so all human tasks can be claimed, started, and completed.
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("john").password("john").roles("PM");
auth.inMemoryAuthentication().withUser("mary").password("mary").roles("HR");
auth.inMemoryAuthentication().withUser("jack").password("jack").roles("user");
auth.inMemoryAuthentication().withUser("user").password("user").roles("kie-server");
auth.inMemoryAuthentication().withUser("wbadmin").password("wbadmin").roles("admin");
auth.inMemoryAuthentication().withUser("kieserver").password("kieserver1!").roles("kie-server");
}
and the new Application class:
@SpringBootApplication
@RestController
public class Application {
@Autowired
private ProcessService processService;
@Autowired
private RuntimeDataService runtimeDataService;
@Autowired
private UserTaskService userTaskService;
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@GetMapping("/evaluation")
public ResponseEntity<Long> startEvaluation(Principal principal, @RequestParam String employee) throws Exception {
Long processInstanceId = -1L;
if ( principal != null ) {
Map<String, Object> vars = new HashMap<>();
vars.put("employee", employee);
processInstanceId = processService.startProcess("Evaluation-1_0_0-SNAPSHOT", "Evaluation.Evaluation", vars);
}
return ResponseEntity.ok(processInstanceId);
}
@GetMapping("/selfeval")
public ResponseEntity<Integer> selfEvaluation(Principal principal, @RequestParam String selfeval) throws Exception {
Map<String, Object> params = new HashMap<>();
params.put("selfeval", selfeval);
List<TaskSummary> taskSummaries = runtimeDataService.getTasksAssignedAsPotentialOwner(principal.getName(), new QueryFilter());
taskSummaries.forEach(s->{
userTaskService.start(s.getId(), principal.getName());
userTaskService.complete(s.getId(), principal.getName(), params);
});
return ResponseEntity.ok(taskSummaries.size());
}
@GetMapping("/hreval")
public ResponseEntity<Integer> hrEvaluation(Principal principal, @RequestParam String hreval) throws Exception {
Map<String, Object> params = new HashMap<>();
params.put("hreval", hreval);
List<TaskSummary> taskSummaries = runtimeDataService.getTasksAssignedAsPotentialOwner(principal.getName(), new QueryFilter());
taskSummaries.forEach(s->{
userTaskService.claim(s.getId(), principal.getName());
userTaskService.start(s.getId(), principal.getName());
userTaskService.complete(s.getId(), principal.getName(), params);
});
return ResponseEntity.ok(taskSummaries.size());
}
@GetMapping("/pmeval")
public ResponseEntity<Integer> pmEvaluation(Principal principal, @RequestParam String pmeval) throws Exception {
Map<String, Object> params = new HashMap<>();
params.put("pmeval", pmeval);
List<TaskSummary> taskSummaries = runtimeDataService.getTasksAssignedAsPotentialOwner(principal.getName(), new QueryFilter());
taskSummaries.forEach(s->{
userTaskService.claim(s.getId(), principal.getName());
userTaskService.start(s.getId(), principal.getName());
userTaskService.complete(s.getId(), principal.getName(), params);
});
return ResponseEntity.ok(taskSummaries.size());
}
}
These new REST endpoints can be executed with authenticated HTTP requests
curl -u mary:mary http://localhost:8090/evaluation?employee=jack
52
curl -u jack:jack "http://localhost:8090/selfeval?selfeval=test+selfeval"
1
curl -u john:john "http://localhost:8090/pmeval?pmeval=test+pmeval"
1
curl -u mary:mary "http://localhost:8090/hreval?hreval=test+hreval"
1
Finally, completed evaluations should be retrievable along with their process variables. A new REST endpoint is made to access it.
@GetMapping("/completed")
public ResponseEntity<List<Collection<VariableDesc>>> completedEvaluations(Principal principal) throws Exception {
Collection<ProcessInstanceDesc> processInstances = runtimeDataService.getProcessInstances(Collections.singletonList(ProcessInstance.STATE_COMPLETED), principal.getName(), new QueryContext());
return ResponseEntity.ok(processInstances.stream()
.map(pi->{return runtimeDataService.getVariablesCurrentState(pi.getId());})
.collect(Collectors.toList())
);
}
The new REST endpoint accessed with an authenticated http request:
curl -u mary:mary "http://localhost:8090/completed"
[[{"variableId":"employee","variableInstanceId":"employee","oldValue":"","newValue":"jack","deploymentId":"Evaluation-1_0_0-SNAPSHOT","processInstanceId":1,"dataTimeStamp":"2019-06-28T14:51:30.728+0000"},{"variableId":"initiator","variableInstanceId":"initiator","oldValue":"","newValue":"mary","deploymentId":"Evaluation-1_0_0-SNAPSHOT","processInstanceId":1,"dataTimeStamp":"2019-06-28T14:51:30.733+0000"},{"variableId":"selfeval","variableInstanceId":"selfeval","oldValue":"","newValue":"selfeval jack","deploymentId":"Evaluation-1_0_0-SNAPSHOT","processInstanceId":1,"dataTimeStamp":"2019-06-28T14:52:21.523+0000"},{"variableId":"pmeval","variableInstanceId":"pmeval","oldValue":"","newValue":"pmeval okay","deploymentId":"Evaluation-1_0_0-SNAPSHOT","processInstanceId":1,"dataTimeStamp":"2019-06-28T14:52:50.350+0000"},{"variableId":"hreval","variableInstanceId":"hreval","oldValue":"","newValue":"hreval okay","deploymentId":"Evaluation-1_0_0-SNAPSHOT","processInstanceId":1,"dataTimeStamp":"2019-06-28T14:53:11.290+0000"}]]
The jBPM java libraries also include client support so a client can be written either for the custom API created or the jBPM client library. At this point it would be a matter of preference.
- fifth-business-central-kjar: Project copied from forth-business-central-kjar
- fifth-business-application-service: Project copied from forth-business-application-service and one API endpoint updated for compatibility with spring
RestTemplate
. - fifth-business-client: New project with client code to access jBPM workflow programatically.
The next obvious step it write some client code to access the springboot jBPM service. Client code should start the business process and interact while simulatin various users.
The process-flow in this example does an employee evaluation. Mary starts the evaluation process for Jack, Jack does his self evluation, and then Mary and John claim, start, and complete HR and PM evaluations respectively. When this is done the process variables are retrieved so the evaluation responses put in by the various users.
The point of the business process in this case is to both coordinate the various users and to obtain data from them. The bigger question this tutorial answers is about a business process creating informationn for an enterprise. A basic business process is about coordinating users but in the information age users are operating on digital data and not paper documents. jBPM use cases should be expanded to address this reality.
Here is the client code that was written for both the custom API and jBPM client library:
@SpringBootApplication
public class Application implements CommandLineRunner {
private static final Logger log = LoggerFactory.getLogger(Application.class);
private String containerId;
private String serverBaseUrl;
private String serverRestUrl;
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Override
public void run(String... args) throws Exception {
containerId = "Evaluation_1.0.0-SNAPSHOT";
serverBaseUrl = "http://localhost:8080";
serverRestUrl = serverBaseUrl + "/rest/server";
runCustomApi();
runjBPMClient();
}
private void runCustomApi() {
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.add("content-type", "application/json");
headers.add("accept", "application/json");
HttpHeaders headersMary = new HttpHeaders();
headersMary.addAll(headers);
headersMary.add("Authorization", "Basic " + new String(Base64.getEncoder().encode("mary:mary".getBytes())));
HttpHeaders headersJack = new HttpHeaders();
headersJack.addAll(headers);
headersJack.add("Authorization", "Basic " + new String(Base64.getEncoder().encode("jack:jack".getBytes())));
HttpHeaders headersJohn = new HttpHeaders();
headersJohn.addAll(headers);
headersJohn.add("Authorization", "Basic " + new String(Base64.getEncoder().encode("john:john".getBytes())));
ResponseEntity<Long> evaluation =
restTemplate.exchange(serverBaseUrl + "/evaluation?employee=jack",
HttpMethod.GET, new HttpEntity<Void>(headersMary), Long.class);
log.info("Started Process Instance: " + evaluation.getBody());
restTemplate.exchange(serverBaseUrl + "/selfeval?selfeval=did+great",
HttpMethod.GET, new HttpEntity<>(headersJack), Long.class);
restTemplate.exchange(serverBaseUrl + "/hreval?hreval=no+issues",
HttpMethod.GET, new HttpEntity<>(headersMary), Long.class);
restTemplate.exchange(serverBaseUrl + "/pmeval?pmeval=projects+done",
HttpMethod.GET, new HttpEntity<>(headersJohn), Long.class);
ResponseEntity<List<VariableInstance>> instances =
restTemplate.exchange(serverBaseUrl + "/instances?processInstanceId="+evaluation.getBody(),
HttpMethod.GET, new HttpEntity<>(headersMary), new ParameterizedTypeReference<List<VariableInstance>>() {
});
for( VariableInstance variableInstance: instances.getBody() ) {
log.info(variableInstance.toString());
}
}
private void runjBPMClient() {
KieServicesClient clientMary = KieServicesFactory.newKieServicesClient(KieServicesFactory.newRestConfiguration(serverRestUrl, "mary", "mary"));
KieServicesClient clientJack = KieServicesFactory.newKieServicesClient(KieServicesFactory.newRestConfiguration(serverRestUrl, "jack", "jack"));
KieServicesClient clientJohn = KieServicesFactory.newKieServicesClient(KieServicesFactory.newRestConfiguration(serverRestUrl, "john", "john"));
ProcessServicesClient processServices = clientMary.getServicesClient(ProcessServicesClient.class);
Long processId = processServices.startProcess(containerId, "Evaluation.Evaluation", Collections.singletonMap("employee", "jack"));
log.info("Started Process Instance: " + processId.toString());
performUserTask(clientJack, "jack", Collections.singletonMap("selfeval", "did lots of work"), false);
performUserTask(clientMary, "mary", Collections.singletonMap("hreval", "no incidents"), true);
performUserTask(clientJohn, "john", Collections.singletonMap("pmeval", "projects completed"), true);
List<VariableInstance> instances = processServices.findVariablesCurrentState(containerId, processId);
for( VariableInstance variableInstance: instances ) {
log.info(variableInstance.toString());
}
}
private void performUserTask(KieServicesClient client, String userId, Map<String, Object> params, boolean claim) {
UserTaskServicesClient userTaskServices = client.getServicesClient(UserTaskServicesClient.class);
List<TaskSummary> tasks = userTaskServices.findTasksAssignedAsPotentialOwner(userId, 0, 10);
for ( TaskSummary taskSummary: tasks ) {
if ( claim ) {
userTaskServices.claimTask(containerId, taskSummary.getId(), userId);
}
userTaskServices.startTask(containerId, taskSummary.getId(), userId);
userTaskServices.completeTask(containerId, taskSummary.getId(), userId, params);
};
}
}
The springboot jBPM server has support for using the javax.mail
api for sending notification emails when UserTasks
are encountered in the process flow. Notifications must be enabled in each user task and the javax.mail
client must be configured to connect to an email server. The springboot jBPM server also includes the jBPM REST interface so a client can be written either for the custom API created or the jBPM Rest API included. At this point it would be a matter of preference.
- sixth-business-central-kjar: Project copied from fifth-business-central-kjar and modified to include
UserTask
notifcations. - sixth-business-application-service: Project copied from fifth-business-application-service and one API endpoint added to read from test email server.
- sixth-business-client: Project copied from fifth-business-client and code added to use the jBPM Rest API.
A jBPM server should make notifications when UserTasks
are waiting to be completed. An effective and straight forward way to accomplish notifications in jBPM is to enable email notifications for each UserTask
and configure email services in the springboot jBPM server.
Editing notifications for a UserTask must be done in the business-central editor since the Eclipse Plugin editor does not have access to that feature. Updates to the business-application-kjar
should be pushed back to the kie-server/business-central workbench. This is done by starting business-central.
- The sixth-business-central-kjarthe directory copied with git information from the forth-business-central-kjar
git push kie master
is run to push the code from forth-business-central-kjar directory into the the workbench.
Once the kjar code is pushed back info workbench the UserTasks can be clicked on. Under properties and Execution settings can be found for Notifications.
- Self Evaluation:
- Type: Evaluation
- Period: 0
- from: mary
- To Users: #{employee}
- reply-to: mary
- Subject: Do Self Evaluation
- Body: Do Self Evaluation
- HR Evaluation
- Type: Evaluation
- Period: 0
- from: mary
- To Groups: HR
- reply-to: mary
- Subject: Evaluation Needed
- Body: Evaluation Needed
- PM Evaluation
- Type: Evaluation
- Period: 0
- from: mary
- To Groups: PM
- reply-to: mary
- Subject: Evaluation Needed
- Body: Evaluation Needed
The Evalation kjar source is copied back into the the sixth-business-central-kjar project and built and installed with mvn clean install
.
A userinfo.properties
file is added to the src/main/resources
directory of the sixth-business-application-service project.
- Administrator=[email protected]:en-UK:Administrator
- Administrators=[email protected]:en-UK:Administrators:[Administrator]
- john=[email protected]:en-UK:john
- jack=[email protected]:en-UK:jack
- mary=[email protected]:en-UK:mary
- HR=[email protected]:en-uk:HR:[mary]
- PM=[email protected]:en-uk:PM:[john]
A email.properties
file is added to the src/main/resources
directory of the sixth-business-application-service project.
- mail.transport.protocol=smtp
- mail.smtp.host=localhost
- mail.smtp.port=3025
For testing purposes a simple ebmedded email server is added to the project.
<dependency>
<groupId>com.icegreen</groupId>
<artifactId>greenmail-spring</artifactId>
<version>1.5.10</version>
</dependency>
and
@SpringBootApplication
@RestController
@ImportAutoConfiguration(GreenMailBean.class)
public class Application {
Finally, for debugging purposes, a printout REST service for the emails sent is created.
@GetMapping("/emails")
public ResponseEntity<List<String>> instances() throws Exception {
GreenMail greenMail = greenMailBean.getGreenMail();
MimeMessage[] emails = greenMail.getReceivedMessages();
List<String> emailStrings = new ArrayList<>();
for ( MimeMessage email: emails ) {
StringBuilder sb = new StringBuilder();
for ( Address from: email.getFrom()) {
sb.append(" From["+from.toString()+"]");
}
for ( Address recip: email.getAllRecipients()) {
sb.append(" Recipient["+recip.toString()+"]");
}
sb.append(" Subject["+email.getSubject()+"]");
sb.append(" Content["+email.getContent()+"]");
emailStrings.add(sb.toString());
}
return ResponseEntity.ok(emailStrings);
}
There is also another way to access the server which is through the jBPM server REST interface. A simple REST client is coded into the sixth-business-client
project.
private void runjBPMApi() throws IOException {
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.add("content-type", "application/json");
headers.add("accept", "application/json");
HttpHeaders headersMary = new HttpHeaders();
headersMary.addAll(headers);
headersMary.add("Authorization", "Basic " + new String(Base64.getEncoder().encode("mary:mary".getBytes())));
HttpHeaders headersJack = new HttpHeaders();
headersJack.addAll(headers);
headersJack.add("Authorization", "Basic " + new String(Base64.getEncoder().encode("jack:jack".getBytes())));
HttpHeaders headersJohn = new HttpHeaders();
headersJohn.addAll(headers);
headersJohn.add("Authorization", "Basic " + new String(Base64.getEncoder().encode("john:john".getBytes())));
ObjectMapper mapper = new ObjectMapper();
String startEval = "{\"employee\":\"jack\"}";
HttpEntity<String> requestEval = new HttpEntity<>(startEval, headersMary);
ResponseEntity<String> evaluation =
restTemplate.exchange(serverRestUrl+"/containers/"+containerId+"/processes/Evaluation.Evaluation/instances",
HttpMethod.POST,
requestEval, String.class );
Long processId = Long.parseLong( evaluation.getBody() );
log.info("Started Process Instance: " + processId.toString());
String selfEval = "{\"selfeval\":\"did lots of work\"}";
performUserTaskApi(restTemplate, headersJack, selfEval, false, mapper);
String pmEval = "{\"pmeval\":\"Projects Done\"}";
performUserTaskApi(restTemplate, headersJohn, pmEval, true, mapper);
String hrEval = "{\"hreval\":\"No Incidents\"}";
performUserTaskApi(restTemplate, headersMary, hrEval, true, mapper);
HttpEntity<String> requestVariables = new HttpEntity<>(headersMary);
ResponseEntity<String> variables =
restTemplate.exchange(serverRestUrl+"/containers/"+containerId+"/processes/instances/"+processId+"/variables/instances",
HttpMethod.GET,
requestVariables, String.class );
JsonNode variableTree = mapper.readTree(variables.getBody());
Iterator<JsonNode> variablesItr = variableTree.findValue("variable-instance").elements();
while ( variablesItr.hasNext() ) {
log.info( variablesItr.next().toString() );
}
}
private void performUserTaskApi(RestTemplate restTemplate, HttpHeaders userHeaders, String params, boolean claim, ObjectMapper mapper) throws IOException {
HttpEntity<String> emptyEntity = new HttpEntity<>(userHeaders);
HttpEntity<String> paramsEntity = new HttpEntity<>(params, userHeaders);
ResponseEntity<String> potOwners =
restTemplate.exchange(serverRestUrl+"/queries/tasks/instances/pot-owners",
HttpMethod.GET, emptyEntity, String.class );
JsonNode evalTree = mapper.readTree(potOwners.getBody());
Long taskId = evalTree.findValue("task-id").asLong();
if ( claim ) {
restTemplate.exchange(serverRestUrl+"/containers/"+containerId+"/tasks/"+taskId+"/states/claimed",
HttpMethod.PUT, emptyEntity, String.class );
}
restTemplate.exchange(serverRestUrl+"/containers/"+containerId+"/tasks/"+taskId+"/states/started",
HttpMethod.PUT, emptyEntity, String.class );
restTemplate.exchange(serverRestUrl+"/containers/"+containerId+"/tasks/"+taskId+"/states/completed",
HttpMethod.PUT, paramsEntity, String.class );
}
- seventh-business-central-kjar: Project copied from sixth-business-central-kjar and modified to use
EmployeeEvaluation
data model. - seventh-business-application-model: Project copied from origin-business-application-model and Plain Old Java Object
EmployeeEvaluation
added. - seventh-business-application-service: Project copied from sixth-business-application-service and changed to use
EmployeeEvaluation
model. - seventh-business-client: Project copied from sixth-business-client and changed to use
EmployeeEvaluation
model.
The origin-business-application-model project is copied and a POJO is created for EmployeeEvaluation
. EmployeeEvaluation contains the fields employee
, seflEval
, pmEval
, and hrEval
, all the current fields that are part of the current business process flow. The seventh-business-application-model project is built and installed into the local maven repository with mvn clean install
.
Start and open jBPM Business Central
server. Ensure the current project is loaded into the server. Open and edit the Evaluation
project. In settings add a dependency to the EmployeeEvaluation model project with GAV com.company:business-application-model:1.0-SNAPSHOT
. Remove the individual variables from the business process and add the employeeEvaluation
variable of type com.company.model.EmployeeEvaluation
.
For the UserTasks I added a variable for both the input and the output and set them both to employeeEvaluation
. The UserTasks take the process variable employeeEvaluation
as input and as output to the tasks. The UserTasks will not be directly using the input or output variable since the completion of the UserTasks includes an EmployeeEvaluation
as a parameter. However, since the employeeEvaluation data from selfEval is passed to both hrEval and pmEval then response from the selfEval is available to users completing hrEval or pmEval if desired.
Copy the Evaluation kjar project back to the maven project and build and install it into the local maven repository with mvn clean install
.
The Springboot jBPM server custom API is updated to handle the EmployeeEvaluation
. The API endpoints are modified to accept POST requests with EmployeeEvaluation
objects in the request body.
@PostMapping("/evaluation")
public ResponseEntity<Long> startEvaluation(Principal principal, @RequestBody EmployeeEvaluation employeeEvaluation) throws Exception {
Long processInstanceId = -1L;
if ( principal != null ) {
processInstanceId = processService.startProcess("Evaluation_1.0.0-SNAPSHOT", "Evaluation.Evaluation", Collections.singletonMap("employeeEvaluation", employeeEvaluation));
}
return ResponseEntity.ok(processInstanceId);
}
@PostMapping("/selfeval")
public ResponseEntity<Integer> selfEvaluation(Principal principal, @RequestBody EmployeeEvaluation employeeEvaluation) throws Exception {
List<TaskSummary> taskSummaries = runtimeDataService.getTasksAssignedAsPotentialOwner(principal.getName(), new QueryFilter());
taskSummaries.forEach(s->{
userTaskService.start(s.getId(), principal.getName());
userTaskService.complete(s.getId(), principal.getName(), Collections.singletonMap("employeeEvaluation", employeeEvaluation));
});
return ResponseEntity.ok(taskSummaries.size());
}
@PostMapping("/hreval")
public ResponseEntity<Integer> hrEvaluation(Principal principal, @RequestBody EmployeeEvaluation employeeEvaluation) throws Exception {
List<TaskSummary> taskSummaries = runtimeDataService.getTasksAssignedAsPotentialOwner(principal.getName(), new QueryFilter());
taskSummaries.forEach(s->{
userTaskService.claim(s.getId(), principal.getName());
userTaskService.start(s.getId(), principal.getName());
userTaskService.complete(s.getId(), principal.getName(), Collections.singletonMap("employeeEvaluation", employeeEvaluation));
});
return ResponseEntity.ok(taskSummaries.size());
}
@PostMapping("/pmeval")
public ResponseEntity<Integer> pmEvaluation(Principal principal, @RequestBody EmployeeEvaluation employeeEvaluation) throws Exception {
List<TaskSummary> taskSummaries = runtimeDataService.getTasksAssignedAsPotentialOwner(principal.getName(), new QueryFilter());
taskSummaries.forEach(s->{
userTaskService.claim(s.getId(), principal.getName());
userTaskService.start(s.getId(), principal.getName());
userTaskService.complete(s.getId(), principal.getName(), Collections.singletonMap("employeeEvaluation", employeeEvaluation));
});
return ResponseEntity.ok(taskSummaries.size());
}
@GetMapping("/instances")
public ResponseEntity<List<VariableInstance>> instances(Principal principal, @RequestParam Long processInstanceId) throws Exception {
VariableInstanceList vi = ConvertUtils.convertToVariablesList(runtimeDataService.getVariablesCurrentState(processInstanceId));
return ResponseEntity.ok(Arrays.asList(vi.getVariableInstances()));
}
The client is updated to pass an EmployeeEvaluation
object to the various APIs.
private void runCustomApi() {
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.add("content-type", "application/json");
headers.add("accept", "application/json");
HttpHeaders headersMary = new HttpHeaders();
headersMary.addAll(headers);
headersMary.add("Authorization", "Basic " + new String(Base64.getEncoder().encode("mary:mary".getBytes())));
HttpHeaders headersJack = new HttpHeaders();
headersJack.addAll(headers);
headersJack.add("Authorization", "Basic " + new String(Base64.getEncoder().encode("jack:jack".getBytes())));
HttpHeaders headersJohn = new HttpHeaders();
headersJohn.addAll(headers);
headersJohn.add("Authorization", "Basic " + new String(Base64.getEncoder().encode("john:john".getBytes())));
EmployeeEvaluation employeeEvaluation = new EmployeeEvaluation();
employeeEvaluation.setEmployee("jack");
ResponseEntity<Long> evaluation =
restTemplate.exchange(serverBaseUrl + "/evaluation",
HttpMethod.POST, new HttpEntity<EmployeeEvaluation>(employeeEvaluation, headersMary), Long.class);
log.info("Started Process Instance: " + evaluation.getBody());
employeeEvaluation.setSelfEval("Did lots of work");
restTemplate.exchange(serverBaseUrl + "/selfeval",
HttpMethod.POST, new HttpEntity<EmployeeEvaluation>(employeeEvaluation, headersJack), Long.class);
employeeEvaluation.setHrEval("no incidents");
restTemplate.exchange(serverBaseUrl + "/hreval",
HttpMethod.POST, new HttpEntity<EmployeeEvaluation>(employeeEvaluation, headersMary), Long.class);
employeeEvaluation.setHrEval("Projects Completed");
restTemplate.exchange(serverBaseUrl + "/pmeval",
HttpMethod.POST, new HttpEntity<EmployeeEvaluation>(employeeEvaluation, headersJohn), Long.class);
ResponseEntity<List<VariableInstance>> instances =
restTemplate.exchange(serverBaseUrl + "/instances?processInstanceId="+evaluation.getBody(),
HttpMethod.GET, new HttpEntity<>(headersMary), new ParameterizedTypeReference<List<VariableInstance>>() {
});
for( VariableInstance variableInstance: instances.getBody() ) {
log.info(variableInstance.toString());
}
}
private void runjBPMClient() throws JsonProcessingException {
KieServicesClient clientMary = KieServicesFactory.newKieServicesClient(KieServicesFactory.newRestConfiguration(serverRestUrl, "mary", "mary").setExtraClasses(Collections.singleton(EmployeeEvaluation.class)));
KieServicesClient clientJack = KieServicesFactory.newKieServicesClient(KieServicesFactory.newRestConfiguration(serverRestUrl, "jack", "jack").setExtraClasses(Collections.singleton(EmployeeEvaluation.class)));
KieServicesClient clientJohn = KieServicesFactory.newKieServicesClient(KieServicesFactory.newRestConfiguration(serverRestUrl, "john", "john").setExtraClasses(Collections.singleton(EmployeeEvaluation.class)));
ObjectMapper mapper = new ObjectMapper();
EmployeeEvaluation employeeEvaluation = new EmployeeEvaluation();
employeeEvaluation.setEmployee("jack");
ProcessServicesClient processServices = clientMary.getServicesClient(ProcessServicesClient.class);
Long processId = processServices.startProcess(containerId, "Evaluation.Evaluation", Collections.singletonMap("employeeEvaluation", employeeEvaluation));
log.info("Started Process Instance: " + processId.toString());
employeeEvaluation.setSelfEval("Did lots of work");
performUserTask(clientJack, "jack", employeeEvaluation, false, mapper);
employeeEvaluation.setHrEval("no incidents");
performUserTask(clientMary, "mary", employeeEvaluation, true, mapper);
employeeEvaluation.setPmEval("Projects Completed");
performUserTask(clientJohn, "john", employeeEvaluation, true, mapper);
List<VariableInstance> instances = processServices.findVariablesCurrentState(containerId, processId);
for( VariableInstance variableInstance: instances ) {
log.info(variableInstance.toString());
}
}
private void performUserTask(KieServicesClient client, String userId, EmployeeEvaluation employeeEvaluation, boolean claim, ObjectMapper mapper) throws JsonProcessingException {
UserTaskServicesClient userTaskServices = client.getServicesClient(UserTaskServicesClient.class);
List<TaskSummary> tasks = userTaskServices.findTasksAssignedAsPotentialOwner(userId, 0, 10);
for ( TaskSummary taskSummary: tasks ) {
if ( claim ) {
userTaskServices.claimTask(containerId, taskSummary.getId(), userId);
}
userTaskServices.startTask(containerId, taskSummary.getId(), userId);
userTaskServices.completeTask(containerId, taskSummary.getId(), userId, Collections.singletonMap("employeeEvaluation", employeeEvaluation));
};
}
private void runjBPMApi() throws IOException {
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.add("accept", "application/json");
HttpHeaders headersMary = new HttpHeaders();
headersMary.addAll(headers);
headersMary.add("Authorization", "Basic " + new String(Base64.getEncoder().encode("mary:mary".getBytes())));
HttpHeaders headersJack = new HttpHeaders();
headersJack.addAll(headers);
headersJack.add("Authorization", "Basic " + new String(Base64.getEncoder().encode("jack:jack".getBytes())));
HttpHeaders headersJohn = new HttpHeaders();
headersJohn.addAll(headers);
headersJohn.add("Authorization", "Basic " + new String(Base64.getEncoder().encode("john:john".getBytes())));
ObjectMapper mapper = new ObjectMapper();
EmployeeEvaluation employeeEvaluation = new EmployeeEvaluation();
employeeEvaluation.setEmployee("jack");
HttpEntity<String> requestEval = new HttpEntity<>(mapper.writeValueAsString(
Collections.singletonMap("employeeEvaluation", employeeEvaluation)), headersMary);
ResponseEntity<String> evaluation =
restTemplate.exchange(serverRestUrl+"/containers/"+containerId+"/processes/Evaluation.Evaluation/instances",
HttpMethod.POST,
requestEval, String.class );
Long processId = Long.parseLong( evaluation.getBody() );
log.info("Started Process Instance: " + processId.toString());
employeeEvaluation.setSelfEval("Did lots of work");
performUserTaskApi(restTemplate, headersJack, employeeEvaluation, false, mapper);
employeeEvaluation.setPmEval("Projects Completed");
performUserTaskApi(restTemplate, headersJohn, employeeEvaluation, true, mapper);
employeeEvaluation.setHrEval("no incidents");
performUserTaskApi(restTemplate, headersMary, employeeEvaluation, true, mapper);
HttpEntity<String> requestVariables = new HttpEntity<>(headersMary);
ResponseEntity<String> variables =
restTemplate.exchange(serverRestUrl+"/containers/"+containerId+"/processes/instances/"+processId+"/variables/instances",
HttpMethod.GET,
requestVariables, String.class );
JsonNode variableTree = mapper.readTree(variables.getBody());
Iterator<JsonNode> variablesItr = variableTree.findValue("variable-instance").elements();
while ( variablesItr.hasNext() ) {
JsonNode node = variablesItr.next();
log.info( node.toString() );
}
}
private void performUserTaskApi(RestTemplate restTemplate, HttpHeaders userHeaders, EmployeeEvaluation employeeEvaluation, boolean claim, ObjectMapper mapper) throws IOException {
HttpEntity<String> emptyEntity = new HttpEntity<>(userHeaders);
HttpEntity<String> paramsEntity = new HttpEntity<>(mapper.writeValueAsString(Collections.singletonMap("employeeEvaluation", employeeEvaluation)), userHeaders);
ResponseEntity<String> potOwners =
restTemplate.exchange(serverRestUrl+"/queries/tasks/instances/pot-owners",
HttpMethod.GET, emptyEntity, String.class );
JsonNode evalTree = mapper.readTree(potOwners.getBody());
Long taskId = evalTree.findValue("task-id").asLong();
if ( claim ) {
restTemplate.exchange(serverRestUrl+"/containers/"+containerId+"/tasks/"+taskId+"/states/claimed",
HttpMethod.PUT, emptyEntity, String.class );
}
restTemplate.exchange(serverRestUrl+"/containers/"+containerId+"/tasks/"+taskId+"/states/started",
HttpMethod.PUT, emptyEntity, String.class );
restTemplate.exchange(serverRestUrl+"/containers/"+containerId+"/tasks/"+taskId+"/states/completed",
HttpMethod.PUT, paramsEntity, String.class );
}
The result highlights an issue with using a custom external data model in the jBPM server.
Started Process Instance: 1
VariableInstance{variableName='initiator', oldValue='', value='mary', processInstanceId=1, date=Fri Jul 26 10:33:13 MST 2019}
VariableInstance{variableName='employeeEvaluation', oldValue='com.company.model.EmployeeEvaluation@5837855b', value='com.company.model.EmployeeEvaluation@25e1c51a', processInstanceId=1, date=Fri Jul 26 10:33:18 MST 2019}
Started Process Instance: 2
VariableInstance{variableName='initiator', oldValue='', value='mary', processInstanceId=2, date=Fri Jul 26 10:33:21 MST 2019}
VariableInstance{variableName='employeeEvaluation', oldValue='com.company.model.EmployeeEvaluation@3c0fb81c', value='com.company.model.EmployeeEvaluation@414b63ef', processInstanceId=2, date=Fri Jul 26 10:33:26 MST 2019}
Started Process Instance: 3
{"name":"initiator","old-value":"","value":"mary","process-instance-id":3,"modification-date":{"java.util.Date":1564162406734}}
{"name":"employeeEvaluation","old-value":"{employee=jack, selfEval=Did lots of work, hrEval=null, pmEval=Projects Completed}","value":"{employee=jack, selfEval=Did lots of work, hrEval=no incidents, pmEval=Projects Completed}","process-instance-id":3,"modification-date":{"java.util.Date":1564162411547}}
As noted in the jBPM documentation data variables are saved by invoking the toString()
method of the custom data model. For the springboot custom jBPM server and REST API, and for the jBPM java client libraries, the result is shown as an instance of a Java object. For the jBPM REST interface the data is retrieved but put into a VariableDescList
which holds values in a human readable format. Good for humans to read but not good for machines to read. In the sixth-business-application all the various client APIs retrieved the process variabes in String format as well but since all the process variables were strings they representation was the same. However, it was also in human readable format which is also not good for computer applications.
Since all the data submitted to the process has been submitted through the client then the client knows whats been submitted to the jBPM business process regardless of whether or not the data can be properly retrieved. The issue then is what happens if another client submits data to the business process and the business process is using a custom data model -- how will the first client be able to read that data if it needs to?
- eighth-business-application-service: Project copied from seventh-business-application-service and api endpoint added for
Rest Workitem Handler
. - eighth-business-central-kjar: Project copied from seventh-business-central-kjar and modified to use
Rest Workitem Handler
.
Using Workitems is an important part of a jBPM business process. There are several built in workitems in the jBPM distribution for sending emails, making rest calls, and the like.
A REST call can be used to make a callback to the Spring jBPM application to retrieve the process data, or do any other function, at any point in the business process.
A callback enpoint is created in the eighth-business-application-service
application to handle a rest callback from the jBPM process. It is made as a POST operation and receives the user login and EmployeeEvaluation
data.
@PostMapping("/datamodel")
public ResponseEntity<Void> datamodelCallback(Principal principal, @RequestBody EmployeeEvaluation employeeEvaluation) throws Exception {
logger.info("Principal: {}", principal);
logger.info("EmployeeEvaluation: {}, {}, {}, {}",
employeeEvaluation.getEmployee(),
employeeEvaluation.getSelfEval(),
employeeEvaluation.getPmEval(),
employeeEvaluation.getHrEval()
);
return ResponseEntity.ok().build();
}
A REST Workitem handler is installed in the business process and added to the process at the end of the flow. The URL
is set to the spring application process endpoint, the Method
is set to POST, the Content
is set as employeeEvaluation
, and a ContentType
is added as application/json
.
When executed the business process calls back to the spring application and sends the final evaluation data.