Ability to edit BpmnModelInstance before bpmn parse listener?

@camunda is there any way to inject a listener that allows editing of BpmnModelInstance before it reaches the parse listener stage during a deployment? I am looking to edit the model using the fluent api builder during deployment.

@thorben any ideas?

Some further usage context:

A Shared engine deployment occurs and before it gets processed into the engine, it can edited.
Looking at it as a example usage from a variation of: (Re-)using messages in element templates

Thanks!

Okay i have built a solution to this…

A very monkey patch application to get this to work, but unless i missed a class usage somewhere, the parser system is very old and not very flexible.

The biggest problem is that the BpmnParse is hardcoded into the system, it cannot be deactivated, where the DMN and CMMN parsers can be turned off. This is a issue because in order to init a parser you cant use the setCustomPreDeployer()/ setCustomPostDeployer() methods as they only append.

So the goal here was to provide a ~pluggable replacement that could be implemented as a plugin.

For quick testing i use the spring boot distribution.

The result is:

package io.digitalstate.camunda.custom.microservice

import groovy.transform.CompileStatic
import org.camunda.bpm.engine.ProcessEngine
import org.camunda.bpm.engine.impl.bpmn.deployer.BpmnDeployer
import org.camunda.bpm.engine.impl.bpmn.parser.BpmnParse
import org.camunda.bpm.engine.impl.cfg.ProcessEngineConfigurationImpl
import org.camunda.bpm.engine.impl.cfg.ProcessEnginePlugin
import org.camunda.bpm.engine.impl.core.model.Properties
import org.camunda.bpm.engine.impl.jobexecutor.JobDeclaration
import org.camunda.bpm.engine.impl.persistence.deploy.Deployer
import org.camunda.bpm.engine.impl.persistence.deploy.cache.DeploymentCache
import org.camunda.bpm.engine.impl.persistence.entity.DeploymentEntity
import org.camunda.bpm.engine.impl.persistence.entity.ProcessDefinitionEntity
import org.camunda.bpm.engine.impl.persistence.entity.ResourceEntity
import org.camunda.bpm.model.bpmn.Bpmn
import org.camunda.bpm.model.bpmn.BpmnModelInstance
import org.camunda.bpm.model.bpmn.instance.ScriptTask
import org.camunda.bpm.model.xml.instance.ModelElementInstance
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.BeanUtils
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.core.annotation.Order
import org.springframework.stereotype.Component;

@SpringBootApplication
public class Application {
    public static void main(String... args) {
        SpringApplication.run(Application.class, args);
    }
}


@Component
@Order(1)
public class MyCustomConfiguration implements ProcessEnginePlugin {
    private static final Logger LOGGER = LoggerFactory.getLogger(MyCustomConfiguration.class)

    @Override
    public void preInit(ProcessEngineConfigurationImpl processEngineConfiguration) {
    }
    @Override
    public void postInit(ProcessEngineConfigurationImpl processEngineConfiguration) {
        LOGGER.info('=========Deployers are Modified==========')
        Deployer bpmnDeployer = processEngineConfiguration.getDeployers().find{
            it.getClass() == BpmnDeployer
        }

        List<Deployer> deployers = processEngineConfiguration.getDeployers()
        deployers.remove(bpmnDeployer)

        CustomBpmnDeployer test = new CustomBpmnDeployer()
        BeanUtils.copyProperties(bpmnDeployer, test)

        deployers.add(test)
        processEngineConfiguration.setDeployers(deployers)
        processEngineConfiguration.deploymentCache = new DeploymentCache(processEngineConfiguration.cacheFactory, processEngineConfiguration.cacheCapacity);
        processEngineConfiguration.deploymentCache.setDeployers(deployers)
    }

    @Override
    public void postProcessEngineBuild(ProcessEngine processEngine) {
    }

}

@CompileStatic
class CustomBpmnDeployer extends BpmnDeployer{
    private static final Logger LOGGER = LoggerFactory.getLogger(CustomBpmnDeployer.class)

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

        byte[] updatedModel
        try {
            LOGGER.info('=========Fluent API TRANSFORM DEFINITION CODE ACTIVATED==========')
            BpmnModelInstance model = Bpmn.readModelFromStream(inputStream)
           // Below is where the model would be modified in the defined script using Fluent API!
            ModelElementInstance task = (ScriptTask) model.getModelElementById('Task_1cxd7gm')
            task.builder().scriptFormat('javascript').scriptText('//something').done()

            // Covert result of fluent api into input bytes for later use
            updatedModel = Bpmn.convertToString(model).getBytes('UTF-8')
        }catch(all){
            throw new Exception("Unable to edit model using Fluent API Builder: ${all}")
        }

        // sourceInputStream is updated to use the new updated model
        BpmnParse bpmnParse = bpmnParser
                .createParse()
                .sourceInputStream(new ByteArrayInputStream(updatedModel))
                .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());

        // Update the bytes of the resourceEntity so it will be used in the actual saving of the deployment
        resource.setBytes(updatedModel)

        return bpmnParse.getProcessDefinitions();
    }
}

@camunda i hope this example shows the valuable use case of having actual Pre-Deployment hooks that plugins can be attached to.

With this setup, you can deploy incomplete BPMN files, that will be passed through the Fluent API code for adjustment and then passed through the parser. This is extremely valuable to replace feature gaps in Camunda Model Element Templates: where Camunda Extension Properties can be used to “configure” an incomplete activity and then upon deployment the Fluent API will make the adjustments needed to fully configure the model.

This was based on the example of (Re-)using messages in element templates where not all activities and features have the ability to be configured in the element templates.

With a little further adjustment a plugin could be built that allows configurable class or groovy script usage allowing a global parser or per bpmn fluent api modifications based on Bpmn Model Extension Elements. Having this ability, essentially removes any limitation that the “templating” of BPMN provides, and allows direct BPMN builds in UIs that can be deployed directly to a engine without the need for manual compilation, and no need for new APIs that front the Model Fluent API!

FYI @thorben @nikku @Niall

3 Likes