Blog

17.06.2021. | Autor: Dmitrii Pisarenko

How to programmatically validate BPMN models

Did it happen to you? Someone makes a minor change to the BPMN model, then, when the system is deployed the next time, the deployment fails because Camunda thinks that BPMN file is invalid. (Even though all the tests are green.)

There is a way to catch such errors earlier – in scope of automated tests rather than during deployment.

Why you should validate BPMN files in automated tests

When you deploy a BPMN file into a Camunda process engine, it performs a number of basic checks. If any errors are detected, the BPMN file is not deployed.

This means that in order to check the validity of a BPMN file you need to try to deploy it to Camunda. This has several disadvantages.

First of all, this validation approach requires a running Camunda engine. You can do it inside a JUnit test by starting Camunda there. However, these tests are usually slower than normal JUnit tests and if you have many BPMN files, running those tests can slow down the build. This, in turn, can encourage developers to turn off those tests.

Big Question

The question is: How can we validate BPMN files just like Camunda does during deployment without a running Camunda engine?

Where does Camunda validate BPMN files during deployment?

First, we need to download Camunda source code. After IntelliJ Idea finished indexing it, you can search for valid in that code base. Among the search results you will find the file BpmnDeployer. There, you can find the following piece of code:

@Override
	protected List<ProcessDefinitionEntity> transformDefinitions(
    DeploymentEntity deployment, ResourceEntity resource,
  Properties properties) {
	  byte[] bytes = resource.getBytes();
	  ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);

	  BpmnParse bpmnParse = bpmnParser
	      .createParse()
	      .sourceInputStream(inputStream)
	      .deployment(deployment)
	      .name(resource.getName());

	  if (!deployment.isValidatingSchema()) {
	    bpmnParse.setSchemaResource(null);
	  }

	  bpmnParse.execute();

	  if (!properties.contains(JOB_DECLARATIONS_PROPERTY)) {
	    properties.set(JOB_DECLARATIONS_PROPERTY, new HashMap<String, List<JobDeclaration<?, ?>>>());
	  }
	  properties.get(JOB_DECLARATIONS_PROPERTY).putAll(bpmnParse.getJobDeclarations());

	  return bpmnParse.getProcessDefinitions();
	}

The line bpmnParse.execute(); leads us to the class BpmnParse where we find another interesting piece of code:

  @Override
  public BpmnParse execute() {
    super.execute(); // schema validation

    try {
      parseRootElement();
    } catch (BpmnParseException e) {
      addError(e);

    } catch (Exception e) {
      LOG.parsingFailure(e);

      // ALL unexpected exceptions should bubble up since they are not handled
      // accordingly by underlying parse-methods and the process can't be
      // deployed
      throw LOG.parsingProcessException(e);

    } finally {
      if (hasWarnings()) {
        logWarnings();
      }
      if (hasErrors()) {
        throwExceptionForErrors();
      }
    }

    return this;
  }

It seems that it throws an exception if errors are detected in a BPMN file.

Calling BpmnParse from inside a minimal JUnit test

Now we need to call BpmnParse in a JUnit test. With a little experimenting we came up with this piece of code:

private void validateFile(String fileName) throws IOException {
        try (FileInputStream inputStream = FileUtils.openInputStream(new File(fileName))) {
            final ExpressionManager testExpressionManager = new ExpressionManager();

            ProcessEngineConfigurationImpl processEngineConfiguration = new ProcessEngineConfigurationImpl() {
                @Override
                protected Collection<? extends CommandInterceptor> getDefaultCommandInterceptorsTxRequired() {
                    return null;
                }

                @Override
                protected Collection<? extends CommandInterceptor> getDefaultCommandInterceptorsTxRequiresNew() {
                    return null;
                }

                @Override
                public ExpressionManager getExpressionManager() {
                    return testExpressionManager;
                }

                @Override
                public FormTypes getFormTypes() {
                    final FormTypes formTypes = new FormTypes();
                    formTypes.addFormType(new BooleanFormType());
                    formTypes.addFormType(new StringFormType());
                    formTypes.addFormType(new LongFormType());
                    return formTypes;
                }
            };

            Context.setProcessEngineConfiguration(processEngineConfiguration);

            BpmnParseFactory bpmnParseFactory = new DefaultBpmnParseFactory();
            BpmnParser bpmnParser = new BpmnParser(testExpressionManager, bpmnParseFactory);
            BpmnParse bpmnParse = bpmnParser.createParse()
                    .sourceInputStream(inputStream)
                    .deployment(new DeploymentEntity())
                    .name(fileName);
            bpmnParse.execute();
        }
        catch (final ParseException exception) {
            exception.printStackTrace();
            Assert.fail("BPMN is invalid, see log output for details");
        }
    }

It detects syntactic errors in BPMN diagrams just like Camunda does at deployment.

Then, you can do a basic syntax check on a BPMN file using a call like:

@Test
public void test1() throws IOException {
    validateFile("src/test/resources/diagram_1.bpmn");
}

You can use this code to make sure that syntactic errors in your BPMN diagrams are detected as early as possible.

What do you think, would it make sense to enhance this code, for example, make sure that it detects activities without the async-before flag set?

Dmitrii Pisarenko
JIT Developer

Photo by Markus Winkler on Unsplash

Weitere Blog

04.04.2022.

Maler oder Macher sein

Autor: Mirko Pawlak

Digitale Transformation ohne Grenzen. Ich bin Prozessmacher. Ich hocke tief drinnen im Maschinenraum und schau mir an, was das Unternehmen einzigartig macht. Ich tu das gern, denn jeder Prozess ist lebendig und einzigartig. Es gibt einen Anfang, dann passiert etwas und am Ende ist es vorbei. Wie geht man am besten mit seinen Prozessen um? […]

Blog lesen
02.12.2021.

How to deploy a Camunda Spring Boot application to a shared application server

Autor: Maximilian Kamenicky

Context Do you still remember our Blog: How to deploy a Camunda Spring Boot application to an external application server? In that blog, my colleague Gerardo Manzano Garcia explained the steps necessary to run a spring boot application on a dedicated application server i.e. an external Tomcat. In other words, run the spring boot application […]

Blog lesen
15.11.2021.

Ways to integrate Kafka with Databases

Autor: Denis Savenko

Kafka is a great instrument. It is widely used in event-driven architectures, and it does the job perfectly fine. However, this is not the only use-case for it. Kafka is also very well suitable for data replication and ingestion use-cases, as well as for building modern ETL or ELT pipelines. The most noteworthy reasons for […]

Blog lesen