How to get Task Form definition with Validations in Camunda?

January 04, 2020 No comments Camunda Java Validations Form Task Definition

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: Sample camunda process definition in modeler

Process definition in XML has the following structure:

<?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.


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 }}