Search code examples
camundabpmn

Create multiple DMN using Java Camunda Springboot starter


I would like to create a Java service that would build a Camunda DMN via code. I'm using camunda-springboot-starter dependency and I'm able to generate the DMN DecisionTable and output of one decision is used in another. I'm using InformationRequirement for same but when I write the model to file and open that file in modeler the arrow is not coming for information requirement and after deploying the decision is not able to get that output.

<

public String generateDMN(String name, String title) throws FileNotFoundException {
    DmnModelInstance modelInstance = initializeEmptyDmnModel();

    Decision decision = modelInstance.newInstance(Decision.class);
    decision.setId(title);
    decision.setName(name);
    modelInstance.getDefinitions().addChildElement(decision);

    List<String[]> rules = new ArrayList<>();
    try (CSVReader reader = new CSVReader(new FileReader("/Users/rahulnehete/IdeaProjects/java-rules-engine/src/main/resources/file.csv"))) {
        rules = reader.readAll();
    } catch (Exception e) {
        log.error("Error reading CSV file ", e);
    }

    DecisionTable decisionTable = modelInstance.newInstance(DecisionTable.class);
    decisionTable.setId("DecisionTable_"+name+"_"+title);
    decisionTable.setHitPolicy(HitPolicy.COLLECT);
    decision.addChildElement(decisionTable);

    Decision decision2 = modelInstance.newInstance(Decision.class);
    decision2.setId(title + "_2");
    decision2.setName(name + "_2");
    modelInstance.getDefinitions().addChildElement(decision2);

    DecisionTable decisionTable2 = modelInstance.newInstance(DecisionTable.class);
    decisionTable2.setId("DecisionTable_"+name+"_"+title+"_2");
    decisionTable2.setHitPolicy(HitPolicy.COLLECT);
    decision2.addChildElement(decisionTable2);

    InformationRequirement infoRequirement = modelInstance.newInstance(InformationRequirement.class, "info1");
    decision2.getInformationRequirements().add(infoRequirement);
    infoRequirement.setRequiredDecision(decision);

    String[] header = rules.remove(0);
    String[] varTypes = rules.remove(0);
    String[] variables = rules.remove(0);

    Long numOfInputs = Arrays.stream(header).filter(k -> k.equalsIgnoreCase("input")).count();
    Long numOfOutputs = Arrays.stream(header).filter(k -> k.equalsIgnoreCase("output")).count();
    for(int i=0; i<numOfInputs; i++) {
        Input ip = generateElement(modelInstance, Input.class, "Input_"+i);
        InputExpression inputExpression = generateElement(modelInstance, InputExpression.class);
        Text text = modelInstance.newInstance(Text.class);
        inputExpression.setText(text);
        inputExpression.setTypeRef(varTypes[i]);
        ip.setLabel("Input " + i);
        ip.setCamundaInputVariable(variables[i]);
        ip.setInputExpression(inputExpression);
        decisionTable.addChildElement(ip);
    }
    for(int i=numOfInputs.intValue(); i<=numOfOutputs; i++) {
        Output op = generateElement(modelInstance, Output.class, "Output_"+i);
        op.setLabel("Output " + i);
        op.setName(variables[i]);
        op.setTypeRef(varTypes[i]);
        decisionTable.addChildElement(op);
    }

    for(int i = 1; i<rules.size(); i++) {
        String[] rule = rules.get(i);
        Rule dmnRule = createRule(modelInstance, numOfInputs.intValue(), numOfOutputs.intValue(), rule);
        decisionTable.addChildElement(dmnRule);
        log.info("Rule {} added", i);
    }

    Input ip = generateElement(modelInstance, Input.class, "Input_A");
    InputExpression inputExpression = generateElement(modelInstance, InputExpression.class);
    Text text = modelInstance.newInstance(Text.class);
    inputExpression.setText(text);
    inputExpression.setTypeRef("string");
    ip.setLabel("Input A");
    ip.setCamundaInputVariable("minPlafond");
    ip.setInputExpression(inputExpression);
    decisionTable2.addChildElement(ip);

    Output op = generateElement(modelInstance, Output.class, "Output_A");
    op.setLabel("Output A");
    op.setName("FinalOutput");
    op.setTypeRef("string");
    decisionTable2.addChildElement(op);

    // TODO: variable names
    String[] rule = new String[] {"-", "$(minPlafond + " + "\"_Success\")"};
    Rule dmnRule = createRule(modelInstance, 1, 1, rule);
    decisionTable2.addChildElement(dmnRule);
    log.info("Rule {} added", "new");

    Dmn.validateModel(modelInstance);
    try {
        FileOutputStream fos = null;
        fos = new FileOutputStream(name + "_" + title + ".dmn");
        Dmn.writeModelToStream(fos, modelInstance);
        fos.close();
    } catch (IOException e) {
        log.error("Error writing model instance ", e);
        return REConstants.FAILED;
    }
    return "DMN GENERATED";
}

private DmnModelInstance initializeEmptyDmnModel() {
    DmnModelInstance dmnModel = Dmn.createEmptyModel();
    Definitions definitions = generateNamedElement(dmnModel, Definitions.class, "definitions", "definitions");
    definitions.setNamespace(DmnModelConstants.CAMUNDA_NS);
    dmnModel.setDefinitions(definitions);
    return dmnModel;
}

private <E extends DmnElement> E generateElement(DmnModelInstance modelInstance, Class<E> elementClass, String id) {
    E element = modelInstance.newInstance(elementClass);
    element.setId(id);
    return element;
}
private <E extends NamedElement> E generateNamedElement(DmnModelInstance modelInstance, Class<E> elementClass, String name, String id) {
    E element = generateElement(modelInstance, elementClass, id);
    element.setName(name);
    return element;
}
private <E extends DmnElement> E generateElement(DmnModelInstance modelInstance, Class<E> elementClass) {
    String generatedId = elementClass.getSimpleName() + UUID.randomUUID();
    return generateElement(modelInstance, elementClass, generatedId);
}

private Rule createRule(DmnModelInstance dmnModelInstance, int numberOfInputs, int numberOfOutputs, String[] ruleInput) {
    Rule rule = dmnModelInstance.newInstance(Rule.class);
    for(int i=0; i<numberOfOutputs; i++) {
        OutputEntry outputEntry = createOutputEntry(dmnModelInstance, ruleInput[numberOfInputs + i]);
        rule.getOutputEntries().add(outputEntry);
    }
    for (int i = 0; i < numberOfInputs; i++) {
        InputEntry inputEntry = createInputEntry(dmnModelInstance, ruleInput[i]);
        rule.getInputEntries().add(inputEntry);
    }
    return rule;
}

private InputEntry createInputEntry(DmnModelInstance dmnModelInstance, String expression) {
    Text text = dmnModelInstance.newInstance(Text.class);
    text.setTextContent(expression);
    InputEntry inputEntry = dmnModelInstance.newInstance(InputEntry.class);
    inputEntry.setText(text);
    return inputEntry;
}

private OutputEntry createOutputEntry(DmnModelInstance dmnModelInstance, String expression) {
    Text text = dmnModelInstance.newInstance(Text.class);
    text.setTextContent(expression);
    OutputEntry outputEntry = dmnModelInstance.newInstance(OutputEntry.class);
    outputEntry.setText(text);
    return outputEntry;
}

}


Solution

  • You implemented the decisions, decisions table, input and output as well as info requirement correctly on model level (xmlns="https://www.omg.org/spec/DMN/20191111/MODEL/"). (Dmn.writeModelToFile(new File("src/main/resources/my.dmn"), modelInstance); is little more compact than writing the stream yourself.)

    When you compare to a model created by the Modeler, you see that it uses additional namespaces such as xmlns:dmndi="https://www.omg.org/spec/DMN/20191111/DMNDI/". This namespaces defines the diagram information separately from the model, e.g:

      <dmndi:DMNDI>
        <dmndi:DMNDiagram id="DMNDiagram_12sfwjw">
          <dmndi:DMNShape id="DMNShape_1m8k4eq" dmnElementRef="Decision1">
            <dc:Bounds height="80" width="180" x="150" y="80" />
          </dmndi:DMNShape>
          <dmndi:DMNShape id="DMNShape_15h0omx" dmnElementRef="Decision2">
            <dc:Bounds height="80" width="180" x="240" y="200" />
          </dmndi:DMNShape>
          <dmndi:DMNEdge id="DMNEdge_15fnvnh" dmnElementRef="InformationRequirement_0a97hay">
            <di:waypoint x="330" y="200" />
            <di:waypoint x="240" y="180" />
            <di:waypoint x="240" y="160" />
          </dmndi:DMNEdge>
        </dmndi:DMNDiagram>
      </dmndi:DMNDI>
    

    Unfortunately, the Camunda API currently offers no way of auto-generating or creating this diagram information.You would have to approach this a level lower with an XML library.

    The model is executable without the diagram information. So, you will have a working model. Maybe that is acceptable and only if a human cares about the DRD it can be created manually?

    An alternative would be to abandon the Java approach and try https://bpmn.io/toolkit/dmn-js/.