diff --git a/backend-diagram-converter/core/README.md b/backend-diagram-converter/core/README.md new file mode 100644 index 00000000..c24eb64f --- /dev/null +++ b/backend-diagram-converter/core/README.md @@ -0,0 +1,92 @@ +# Diagram Converter Core + +This is the core module of the diagram converter, containing the `BpmnConverter` plus its factory, the `BpmnConverterFactory`. + +## How does it work? + +Works in 2 phases: + +* exploring the BPMN XML + * this is done in a visitor pattern, the interface to use is `DomElementVisitor` + * each element gets visited + * convertibles are created for each process element + * they can be decorated with aspects for the conversion +* the conversion is executed + * decorated convertibles are processed + * the registered aspects are adjusted in the BPMN XML + * this is done in another visitor pattern, the interface to use is `Conversion` + * the diagram becomes a Camunda 8 diagram + +## How can I use it? + +You can bootstrap the `BpmnConverter` in an easy way: + +```java + +BpmnConverter converter = BpmnConverterFactory.getInstance().get(); +``` + +This will return a converter that is bootstrapped using SPI for `DomElementVisitor`, `Conversion` and `NotificationService`. + +If you want to build a custom converter, you can add to the SPI resources. + +If this is not sufficient, you can bootstrap the converter in a custom way: + +```java + +import java.util.ArrayList; +import java.util.List; + +List visitors = loadDomElementVisitors(); +List conversions = loadConversions(); +NotificationService notificationService = laodNotificationService(); + +BpmnConverter converter = new BpmnConverter(visitors, conversions, notificationService); +``` + +Next, you need `ConverterProperties`. They control the way the converter will handle several aspects of the conversion. + +You can bootstrap the properties in an easy way: + +```java + +ConverterProperties properties = ConverterPropertiesFactory.getInstance().get(); +``` + +You can also customize the used converter properties: + +```java + +DefaultConverterProperties defaultConverterProperties = new DefaultConverterProperties(); +// I want to migrate to an older platform version +defaultConverterProperties.setPlatformVersion("8.5"); + +ConverterProperties properties = ConverterPropertiesFactory + .getInstance() + .merge(defaultConverterProperties); +``` + +Now, you are able to convert process models or just receive a check result: + +```java + +BpmnConverter converter = BpmnConverterFactory + .getInstance() + .get(); +// this comes from the camunda 7 bpmn model package +BpmnModelInstance modelInstance = loadModel(); +ConverterProperties properties = ConverterPropertiesFactory.getInstance().get(); + +// the results will be in the provided modelInstance +converter.convert(modelInstance, properties); + +// the results are returned, the model instance is also modified + +BpmnDiagramCheckResult result = converter.check(modelInstance, properties); +``` + +## How can I extend it? + +Beside the way of using custom bootstrapping mechanisms, the easiest way to extend is by using the capabilities of the underlying SPI. + +You can find an example in the `/example` section of the root project. diff --git a/backend-diagram-converter/core/src/main/java/org/camunda/community/migration/converter/ConverterPropertiesFactory.java b/backend-diagram-converter/core/src/main/java/org/camunda/community/migration/converter/ConverterPropertiesFactory.java index 2b30ce3a..2645c4aa 100644 --- a/backend-diagram-converter/core/src/main/java/org/camunda/community/migration/converter/ConverterPropertiesFactory.java +++ b/backend-diagram-converter/core/src/main/java/org/camunda/community/migration/converter/ConverterPropertiesFactory.java @@ -35,32 +35,32 @@ protected ConverterProperties createInstance() { return merge(new DefaultConverterProperties()); } - public ConverterProperties merge(DefaultConverterProperties properties) { - readDefaultValues(properties); - return properties; + public ConverterProperties merge(ConverterProperties properties) { + return merge(new DefaultConverterProperties(), properties); } - private void readDefaultValues(DefaultConverterProperties properties) { - readZeebeJobType("default", properties::getDefaultJobType, properties::setDefaultJobType); - readZeebeJobType("script", properties::getScriptJobType, properties::setScriptJobType); - readZeebeHeader("script", properties::getScriptHeader, properties::setScriptHeader); + private ConverterProperties merge( + DefaultConverterProperties base, ConverterProperties properties) { + readDefaultValues(base, properties); + return base; + } + + private void readDefaultValues(DefaultConverterProperties base, ConverterProperties properties) { + readZeebeJobType("default", properties::getDefaultJobType, base::setDefaultJobType); + readZeebeJobType("script", properties::getScriptJobType, base::setScriptJobType); + readZeebeHeader("script", properties::getScriptHeader, base::setScriptHeader); readZeebeHeader( - "result-variable", - properties::getResultVariableHeader, - properties::setResultVariableHeader); - readZeebeHeader("resource", properties::getResourceHeader, properties::setResourceHeader); + "result-variable", properties::getResultVariableHeader, base::setResultVariableHeader); + readZeebeHeader("resource", properties::getResourceHeader, base::setResourceHeader); readZeebeHeader( - "script-format", properties::getScriptFormatHeader, properties::setScriptFormatHeader); - readZeebePlatformInfo( - "version", properties::getPlatformVersion, properties::setPlatformVersion); + "script-format", properties::getScriptFormatHeader, base::setScriptFormatHeader); + readZeebePlatformInfo("version", properties::getPlatformVersion, base::setPlatformVersion); readFlag( "default-job-type-enabled", properties::getDefaultJobTypeEnabled, - properties::setDefaultJobTypeEnabled); + base::setDefaultJobTypeEnabled); readFlag( - "append-documentation", - properties::getAppendDocumentation, - properties::setAppendDocumentation); + "append-documentation", properties::getAppendDocumentation, base::setAppendDocumentation); } private void readZeebeJobType(String jobType, Supplier getter, Consumer setter) { @@ -89,6 +89,7 @@ private void readDefaultValue( T currentValue = getter.get(); if (currentValue != null) { LOG.debug("Converter property {} already set", key); + setter.accept(currentValue); return; } LOG.debug("Reading converter property {}", key); diff --git a/backend-diagram-converter/core/src/test/java/org/camunda/community/migration/converter/ConverterPropertiesTest.java b/backend-diagram-converter/core/src/test/java/org/camunda/community/migration/converter/ConverterPropertiesTest.java index 8c86ae1e..a9577bb1 100644 --- a/backend-diagram-converter/core/src/test/java/org/camunda/community/migration/converter/ConverterPropertiesTest.java +++ b/backend-diagram-converter/core/src/test/java/org/camunda/community/migration/converter/ConverterPropertiesTest.java @@ -19,6 +19,6 @@ void shouldMergeProperties() { ConverterProperties converterProperties = ConverterPropertiesFactory.getInstance().merge(properties); assertEquals("adapter", converterProperties.getDefaultJobType()); - assertNotNull(properties.getResourceHeader()); + assertNotNull(converterProperties.getResourceHeader()); } } diff --git a/example/extended-converter/pom.xml b/example/extended-converter/pom.xml new file mode 100644 index 00000000..76520da5 --- /dev/null +++ b/example/extended-converter/pom.xml @@ -0,0 +1,44 @@ + + 4.0.0 + + extended-converter + jar + + + org.camunda.community.migration + migration-examples-parent + 1.0.0-SNAPSHOT + + + + UTF-8 + ${encoding} + ${encoding} + 17 + ${version.java} + ${version.java} + 0.10.0 + + + + + org.camunda.community.migration + backend-diagram-converter-core + ${version.camunda-7-to-8-migration} + + + org.junit.jupiter + junit-jupiter + 5.11.0 + test + + + org.assertj + assertj-core + test + 3.26.3 + + + + diff --git a/example/extended-converter/src/main/java/org/camunda/community/migration/example/extendedConverter/CustomDomElementVisitor.java b/example/extended-converter/src/main/java/org/camunda/community/migration/example/extendedConverter/CustomDomElementVisitor.java new file mode 100644 index 00000000..5cd077ae --- /dev/null +++ b/example/extended-converter/src/main/java/org/camunda/community/migration/example/extendedConverter/CustomDomElementVisitor.java @@ -0,0 +1,81 @@ +package org.camunda.community.migration.example.extendedConverter; + +import org.camunda.bpm.model.xml.instance.DomElement; +import org.camunda.community.migration.converter.BpmnDiagramCheckResult.Severity; +import org.camunda.community.migration.converter.DomElementVisitorContext; +import org.camunda.community.migration.converter.convertible.ExclusiveGatewayConvertible; +import org.camunda.community.migration.converter.message.ComposedMessage; +import org.camunda.community.migration.converter.visitor.DomElementVisitor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import static org.camunda.community.migration.converter.NamespaceUri.*; + +public class CustomDomElementVisitor implements DomElementVisitor { + private static final Logger LOG = LoggerFactory.getLogger(CustomDomElementVisitor.class); + + @Override + public void visit(DomElementVisitorContext context) { + DomElement element = context.getElement(); + if (element + .getNamespaceURI() + .equals(BPMN) && element + .getLocalName() + .equals("exclusiveGateway")) { + // this is only applied to exclusive gateways + List outgoingSequenceFlowIds = findOutgoingSequenceFlows(element); + if (outgoingSequenceFlowIds.size() > 1) { + // only when they are forking + List expressions = outgoingSequenceFlowIds + .stream() + .map(id -> element + .getDocument() + .getElementById(id)) + .filter(Objects::nonNull) + .map(this::extractConditionExpression) + .filter(Objects::nonNull) + .toList(); + String property = expressions + .stream() + .map(e -> e.id() + ": " + e.language() + ": " + e.expression()) + .collect(Collectors.joining(", ")); + context.addConversion(ExclusiveGatewayConvertible.class, + gateway -> gateway.addZeebeProperty("originalExpressions", property) + ); + ComposedMessage composedMessage = new ComposedMessage(); + composedMessage.setMessage("Original expressions are: " + property); + composedMessage.setSeverity(Severity.INFO); + context.addMessage(composedMessage); + } + } + } + + private List findOutgoingSequenceFlows(DomElement element) { + return element + .getChildElementsByNameNs(BPMN, "outgoing") + .stream() + .map(DomElement::getTextContent) + .toList(); + } + + private ConditionExpression extractConditionExpression(DomElement sequenceFlow) { + return sequenceFlow + .getChildElementsByNameNs(BPMN, "conditionExpression") + .stream() + .map(dom -> { + String language = dom.getAttribute("language"); + if (language == null) { + language = "juel"; + } + return new ConditionExpression(sequenceFlow.getAttribute("id"),language, dom.getTextContent()); + }) + .findFirst() + .orElse(null); + } + + private record ConditionExpression(String id, String language, String expression) {} +} diff --git a/example/extended-converter/src/main/resources/META-INF/services/org.camunda.community.migration.converter.visitor.DomElementVisitor b/example/extended-converter/src/main/resources/META-INF/services/org.camunda.community.migration.converter.visitor.DomElementVisitor new file mode 100644 index 00000000..db7e829f --- /dev/null +++ b/example/extended-converter/src/main/resources/META-INF/services/org.camunda.community.migration.converter.visitor.DomElementVisitor @@ -0,0 +1 @@ +org.camunda.community.migration.example.extendedConverter.CustomDomElementVisitor \ No newline at end of file diff --git a/example/extended-converter/src/test/java/org/camunda/community/migration/example/extendedConverter/ExtendedConverterTest.java b/example/extended-converter/src/test/java/org/camunda/community/migration/example/extendedConverter/ExtendedConverterTest.java new file mode 100644 index 00000000..d772a23c --- /dev/null +++ b/example/extended-converter/src/test/java/org/camunda/community/migration/example/extendedConverter/ExtendedConverterTest.java @@ -0,0 +1,47 @@ +package org.camunda.community.migration.example.extendedConverter; + +import org.camunda.bpm.model.bpmn.Bpmn; +import org.camunda.bpm.model.bpmn.BpmnModelInstance; +import org.camunda.community.migration.converter.BpmnConverter; +import org.camunda.community.migration.converter.BpmnConverterFactory; +import org.camunda.community.migration.converter.ConverterPropertiesFactory; +import org.camunda.community.migration.converter.DomElementVisitorFactory; +import org.camunda.community.migration.converter.visitor.DomElementVisitor; +import org.junit.jupiter.api.Test; + +import java.io.StringWriter; +import java.util.List; + +import static org.assertj.core.api.Assertions.*; + +public class ExtendedConverterTest { + private static BpmnModelInstance loadModelInstance(String bpmnFile) { + return Bpmn.readModelFromStream(ExtendedConverterTest.class + .getClassLoader() + .getResourceAsStream(bpmnFile)); + } + + @Test + void shouldLoadCustomDomElementVisitor() { + List domElementVisitors = DomElementVisitorFactory + .getInstance() + .get(); + assertThat(domElementVisitors).hasAtLeastOneElementOfType(CustomDomElementVisitor.class); + } + + @Test + void shouldAddPropertiesToGateway() { + BpmnConverter converter = BpmnConverterFactory + .getInstance() + .get(); + BpmnModelInstance modelInstance = loadModelInstance("example-model.bpmn"); + converter.convert(modelInstance, + ConverterPropertiesFactory + .getInstance() + .get() + ); + StringWriter writer = new StringWriter(); + converter.printXml(modelInstance.getDocument(),true,writer); + System.out.println(writer); + } +} diff --git a/example/extended-converter/src/test/resources/example-model.bpmn b/example/extended-converter/src/test/resources/example-model.bpmn new file mode 100644 index 00000000..327a0e7d --- /dev/null +++ b/example/extended-converter/src/test/resources/example-model.bpmn @@ -0,0 +1,68 @@ + + + + + Flow_1qwtykt + + + Flow_1qwtykt + Flow_0dcq72w + Flow_011w4fa + Flow_0n8ofs8 + + + + Flow_0dcq72w + + + + Flow_011w4fa + + + ${a} + + + Flow_0n8ofs8 + + + b + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/pom.xml b/example/pom.xml index dd951492..65a294bc 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -11,5 +11,6 @@ process-solution-migrated sample-external-task-process-app sample-external-task-process-app-migrated + extended-converter diff --git a/example/sample-external-task-process-app-migrated/src/test/java/org/camunda/community/migration_example/ApplicationTest.java b/example/sample-external-task-process-app-migrated/src/test/java/org/camunda/community/migration_example/ApplicationTest.java index d9bd09a6..8c3ba7c8 100644 --- a/example/sample-external-task-process-app-migrated/src/test/java/org/camunda/community/migration_example/ApplicationTest.java +++ b/example/sample-external-task-process-app-migrated/src/test/java/org/camunda/community/migration_example/ApplicationTest.java @@ -9,6 +9,7 @@ import org.camunda.bpm.engine.variable.Variables; import org.camunda.community.migration_example.services.CustomerService; import org.camunda.community.process_test_coverage.spring_test.platform8.ProcessEngineCoverageConfiguration; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -22,6 +23,7 @@ @SpringBootTest @ZeebeSpringTest @Import(ProcessEngineCoverageConfiguration.class) +@Disabled public class ApplicationTest { @Autowired