How to get Task Form definition with Validations in Camunda?

January 15, 2019 No comments Camunda Java Validations Form Task Definition

For those who do not know, Camunda is a java-based framework supporting BPMN for workflow and process automation. It has REST API which allows you 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 form definition with validations, so we have to extend API with a new functionality. To achieve the intended result we have to:

  1. Get BPMN process definition,
  2. Find form representation in XML DOM using XPath,
  3. 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.

My sample process looks like this: Sample camunda process definition in modeler

It's definition in XML:

<?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 simple Xpath expression.


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.

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:

Node formNode = findNodeByTaskDefinitionId('Task_0pj88q4', xml);
String json = getJSON(formNode).toString();

you will get following JSON output:

{
    "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"
                                }
                            }
                        ]
                    }
                }
            }
        }
    }
}
{{ message }}

{{ 'Comments are closed.' | trans }}