1. What is Camunda
Camunda is a java-based framework supporting BPMN for workflow and process automation. It has REST API that allows us to use the process engine from a remote application or a JavaScript.
Unfortunately their API (ver. 7.8) doesn't provide a method to get a form definition with validations , so we have to extend this API with a new functionality.
To achieve the intended result we have to:
get BPMN process definition,
find form representation in XML DOM using XPath,
parse Node to JSON as we want to be consistent with Camunda API.
To retrieve the BPMN 2.0 XML of a process definition we use Get XML method using Camunda REST API.
In this article we use sample process with the described by the following diagram:
Process definition in XML has the following structure:
Copy
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Definitions_11mw2m4" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="2.2.4">
<bpmn:process id="loanApprovalProcess" name="Loan Approval " isExecutable="true">
<bpmn:startEvent id="StartEvent_1pyi1kf" name="New LoanRequest recived" camunda:initiator="requestor">
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="firstname" label="Firstname" type="string">
<camunda:validation>
<camunda:constraint name="required" />
<camunda:constraint name="minlength" config="2" />
<camunda:constraint name="maxlength" config="25" />
</camunda:validation>
</camunda:formField>
<camunda:formField id="lastname" label="Lastname" type="string">
<camunda:validation>
<camunda:constraint name="required" />
<camunda:constraint name="minlength" config="1" />
<camunda:constraint name="maxlength" config="25" />
</camunda:validation>
</camunda:formField>
<camunda:formField id="amount" label="Amount" type="long">
<camunda:validation>
<camunda:constraint name="required" />
<camunda:constraint name="min" config="2000" />
<camunda:constraint name="max" config="500000" />
</camunda:validation>
</camunda:formField>
</camunda:formData>
</bpmn:extensionElements>
<bpmn:outgoing>SequenceFlow_1nj3h41</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="SequenceFlow_1nj3h41" sourceRef="StartEvent_1pyi1kf" targetRef="Task_0pj88q4" />
<bpmn:exclusiveGateway id="ExclusiveGateway_0v4xwi0" name="Approved?">
<bpmn:incoming>SequenceFlow_0uxtni3</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_17okpa8</bpmn:outgoing>
<bpmn:outgoing>SequenceFlow_1bdz40i</bpmn:outgoing>
</bpmn:exclusiveGateway>
<bpmn:sequenceFlow id="SequenceFlow_0uxtni3" sourceRef="Task_0pj88q4" targetRef="ExclusiveGateway_0v4xwi0" />
<bpmn:endEvent id="EndEvent_1c90l6v">
<bpmn:incoming>SequenceFlow_17okpa8</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="SequenceFlow_17okpa8" name="Yes" sourceRef="ExclusiveGateway_0v4xwi0" targetRef="EndEvent_1c90l6v">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">${approved}</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:sequenceFlow id="SequenceFlow_1bdz40i" name="No" sourceRef="ExclusiveGateway_0v4xwi0" targetRef="Task_0nqaamc">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">${not approved}</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:sequenceFlow id="SequenceFlow_0wp2eq2" sourceRef="Task_0nqaamc" targetRef="Task_0pj88q4" />
<bpmn:userTask id="Task_0pj88q4" name="Approve Request" camunda:assignee="assignee" camunda:candidateGroups="sales">
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="firstname" label="Firstname" type="string">
<camunda:validation>
<camunda:constraint name="readonly" />
</camunda:validation>
</camunda:formField>
<camunda:formField id="lastname" label="Lastname" type="string">
<camunda:validation>
<camunda:constraint name="readonly" />
</camunda:validation>
</camunda:formField>
<camunda:formField id="amount" label="Amount" type="long">
<camunda:validation>
<camunda:constraint name="readonly" />
</camunda:validation>
</camunda:formField>
<camunda:formField id="approved" label="Do you approve this request?" type="boolean" />
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>SequenceFlow_1nj3h41</bpmn:incoming>
<bpmn:incoming>SequenceFlow_0wp2eq2</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_0uxtni3</bpmn:outgoing>
</bpmn:userTask>
<bpmn:userTask id="Task_0nqaamc" name="Adjust Request" camunda:assignee="${requestor}">
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="amount" label="Amount" type="long">
<camunda:validation>
<camunda:constraint name="required" />
<camunda:constraint name="min" config="2000" />
<camunda:constraint name="max" config="500000" />
</camunda:validation>
</camunda:formField>
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>SequenceFlow_1bdz40i</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_0wp2eq2</bpmn:outgoing>
</bpmn:userTask>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="loanApprovalProcess">
<bpmndi:BPMNShape id="StartEvent_1pyi1kf_di" bpmnElement="StartEvent_1pyi1kf">
<dc:Bounds x="203" y="173" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="191" y="216" width="66" height="40" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_1nj3h41_di" bpmnElement="SequenceFlow_1nj3h41">
<di:waypoint x="239" y="191" />
<di:waypoint x="289" y="191" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="ExclusiveGateway_0v4xwi0_di" bpmnElement="ExclusiveGateway_0v4xwi0" isMarkerVisible="true">
<dc:Bounds x="591" y="166" width="50" height="50" />
<bpmndi:BPMNLabel>
<dc:Bounds x="590" y="136" width="54" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_0uxtni3_di" bpmnElement="SequenceFlow_0uxtni3">
<di:waypoint x="389" y="191" />
<di:waypoint x="591" y="191" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="EndEvent_1c90l6v_di" bpmnElement="EndEvent_1c90l6v">
<dc:Bounds x="868" y="173" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_17okpa8_di" bpmnElement="SequenceFlow_17okpa8">
<di:waypoint x="641" y="191" />
<di:waypoint x="868" y="191" />
<bpmndi:BPMNLabel>
<dc:Bounds x="746" y="173" width="19" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1bdz40i_di" bpmnElement="SequenceFlow_1bdz40i">
<di:waypoint x="616" y="216" />
<di:waypoint x="616" y="345" />
<bpmndi:BPMNLabel>
<dc:Bounds x="624" y="278" width="14" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_0wp2eq2_di" bpmnElement="SequenceFlow_0wp2eq2">
<di:waypoint x="566" y="385" />
<di:waypoint x="339" y="385" />
<di:waypoint x="339" y="231" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="UserTask_1i9uaoz_di" bpmnElement="Task_0pj88q4">
<dc:Bounds x="289" y="151" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_0jg625o_di" bpmnElement="Task_0nqaamc">
<dc:Bounds x="566" y="345" width="100" height="80" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>
To find form Node we use a simple Xpath expression .
Copy
import java.io.StringReader;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathFactory;
import org.camunda.bpm.engine.rest.dto.repository.ProcessDefinitionDiagramDto;
import org.camunda.bpm.engine.rest.dto.task.TaskDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;
private Node findNodeByTaskDefinitionId(String taskDefinitionKey, String xml) throws Exception {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(false);
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(new InputSource(new StringReader(xml)));
XPathFactory xPathfactory = XPathFactory.newInstance();
XPath xpath = xPathfactory.newXPath();
XPathExpression expr = xpath.compile("//*[@id='" + taskDefinitionKey + "']");
return (Node) expr.evaluate(doc, XPathConstants.NODE);
}
The last step is to convert org.w3c.dom.Node object into JSON string.
Copy
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
public static JSONObject getJSON(Node node, boolean withoutNamespaces) throws JSONException {
JSONObject jsonObject = new JSONObject();
jsonObject.putOpt(NODE_FILED_NAME, namespace(node.getNodeName(), withoutNamespaces));
if (node.hasAttributes()) {
JSONObject jsonAttr = new JSONObject();
NamedNodeMap attr = node.getAttributes();
int attrLenth = attr.getLength();
for (int i = 0; i < attrLenth; i++) {
Node attrItem = attr.item(i);
String name = namespace(attrItem.getNodeName(), withoutNamespaces);
String value = attrItem.getNodeValue();
jsonAttr.putOpt(name, value);
}
jsonObject.putOpt(NODE_FILED_ATTR, jsonAttr);
}
if (node.hasChildNodes()) {
NodeList children = node.getChildNodes();
int childrenCount = children.getLength();
if (childrenCount == 1) {
Node item = children.item(0);
int itemType = item.getNodeType();
if (itemType == Node.TEXT_NODE) {
jsonObject.putOpt(NODE_FILED_CONTENT, item.getNodeValue());
return jsonObject;
}
}
for (int i = 0; i < childrenCount; i++) {
Node item = children.item(i);
int itemType = item.getNodeType();
if (itemType == Node.DOCUMENT_NODE || itemType == Node.ELEMENT_NODE) {
JSONObject jsonItem = getJSON(item, withoutNamespaces);
if (jsonItem != null) {
String name = jsonItem.optString(NODE_FILED_NAME);
if (!jsonObject.has(NODE_FILED_CONTENT)) {
jsonObject.putOpt(NODE_FILED_CONTENT, new JSONObject());
}
JSONObject jsonContent = jsonObject.optJSONObject(NODE_FILED_CONTENT);
Object jsonByName = jsonContent.opt(name);
if (jsonByName == null) {
jsonContent.putOpt(name, jsonItem);
} else if (jsonByName instanceof JSONArray) {
((JSONArray) jsonByName).put(jsonItem);
} else {
JSONArray arr = new JSONArray();
arr.put(jsonByName);
arr.put(jsonItem);
jsonContent.putOpt(name, arr);
}
}
}
}
} else {
jsonObject.putOpt(NODE_FILED_CONTENT, node.getNodeValue());
}
return jsonObject;
}
For the given parameters:
Copy
Node formNode = findNodeByTaskDefinitionId('Task_0pj88q4', xml);
String json = getJSON(formNode).toString();
you will get following JSON output:
Copy
{
"name": "userTask",
"attr": {
"candidateGroups": "sales",
"name": "Approve Request",
"assignee": "assignee",
"id": "Task_0pj88q4"
},
"content": {
"incoming": [
{
"name": "incoming",
"content": "SequenceFlow_1nj3h41"
},
{
"name": "incoming",
"content": "SequenceFlow_0wp2eq2"
}
],
"outgoing": {
"name": "outgoing",
"content": "SequenceFlow_0uxtni3"
},
"extensionElements": {
"name": "extensionElements",
"content": {
"formData": {
"name": "formData",
"content": {
"formField": [
{
"name": "formField",
"attr": {
"id": "firstname",
"label": "Firstname",
"type": "string"
},
"content": {
"validation": {
"name": "validation",
"content": {
"constraint": {
"name": "constraint",
"attr": {
"name": "readonly"
}
}
}
}
}
},
{
"name": "formField",
"attr": {
"id": "lastname",
"label": "Lastname",
"type": "string"
},
"content": {
"validation": {
"name": "validation",
"content": {
"constraint": {
"name": "constraint",
"attr": {
"name": "readonly"
}
}
}
}
}
},
{
"name": "formField",
"attr": {
"id": "amount",
"label": "Amount",
"type": "long"
},
"content": {
"validation": {
"name": "validation",
"content": {
"constraint": {
"name": "constraint",
"attr": {
"name": "readonly"
}
}
}
}
}
},
{
"name": "formField",
"attr": {
"id": "approved",
"label": "Do you approve this request?",
"type": "boolean"
}
}
]
}
}
}
}
}
}
{{ 'Comments (%count%)' | trans {count:count} }}
{{ 'Comments are closed.' | trans }}