
com.effektif.workflow.impl.bpmn.BpmnWriterImpl Maven / Gradle / Ivy
/* Copyright (c) 2014, Effektif GmbH.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License. */
package com.effektif.workflow.impl.bpmn;
import com.effektif.workflow.api.bpmn.BpmnElement;
import com.effektif.workflow.api.bpmn.BpmnWriter;
import com.effektif.workflow.api.bpmn.XmlElement;
import com.effektif.workflow.api.model.Id;
import com.effektif.workflow.api.model.RelativeTime;
import com.effektif.workflow.api.types.DataType;
import com.effektif.workflow.api.workflow.*;
import com.effektif.workflow.api.workflow.diagram.*;
import com.effektif.workflow.impl.json.Mappings;
import org.joda.time.LocalDateTime;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.ISODateTimeFormat;
import java.util.*;
import java.util.stream.Collectors;
import static com.effektif.workflow.impl.bpmn.Bpmn.*;
/**
* @author Tom Baeyens
*/
public class BpmnWriterImpl implements BpmnWriter {
private static final double EXPORT_MARGIN = 50d;
public static DateTimeFormatter DATE_FORMAT = ISODateTimeFormat.dateTime();
protected BpmnMappings bpmnMappings;
protected String bpmnPrefix;
protected String bpmnDiagramPrefix;
protected String effektifPrefix;
/** how much the diagram needs to be transposed in the x direction to end up in the top left corner */
protected double xOffset;
/** how much the diagram needs to be transposed in the y direction to end up in the top left corner */
protected double yOffset;
/** stack of the current scopes */
protected Stack scopeStack = new Stack();
/** current scope (==scopeStack.peek()) */
protected Scope scope;
/** stack of the current xml elements */
protected Stack xmlStack = new Stack();
/** current xml element */
protected XmlElement xml;
public BpmnWriterImpl(BpmnMappings bpmnMappings) {
this.bpmnMappings = bpmnMappings;
}
protected void startElementBpmn(String localpart, Object source) {
startElementBpmn(localpart, source, null);
}
protected void startElementBpmn(String localpart, Object source, Integer index) {
if (source==null) {
startElementBpmn(localpart, index);
} else if (source instanceof XmlElement) {
XmlElement sourceElement = (XmlElement) source;
sourceElement.setElementParents();
if (xml!=null) {
xml.addElement(sourceElement, index);
}
sourceElement.setName(BPMN_URI, localpart);
startElement(sourceElement);
} else {
throw new RuntimeException("Unknown BPMN source: "+source);
}
}
@Override
public void startElementBpmn(String localPart) {
startElementBpmn(localPart, null);
}
@Override
public void startElementBpmn(String localPart, Integer index) {
XmlElement newXmlElement = null;
if (xml!=null) {
newXmlElement = xml.createElement(BPMN_URI, localPart, index);
} else {
newXmlElement = new XmlElement();
newXmlElement.setName(BPMN_URI, localPart);
}
startElement(newXmlElement);
}
@Override
public void startElementBpmnDiagram(String localPart) {
startElement(xml.createElement(BPMN_DI_URI, localPart));
}
@Override
public void startElementEffektif(String localPart) {
startElementEffektif(localPart, null);
}
@Override
public void startElementEffektif(Class modelClass) {
BpmnTypeMapping bpmnTypeMapping = bpmnMappings.getBpmnTypeMapping(modelClass);
String localPart = bpmnTypeMapping.getBpmnElementName();
startElementEffektif(localPart, null);
}
@Override
public void startElementEffektif(String localPart, Integer index) {
startElement(xml.createElement(EFFEKTIF_URI, localPart, index));
}
public void startOrGetElement(String namespaceUri, String localPart, Integer index) {
startElement(xml.getOrCreateChildElement(namespaceUri, localPart, index));
}
protected void startElement(XmlElement nestedXml) {
if (xml!=null) {
xmlStack.push(xml);
}
xml = nestedXml;
}
@Override
public void endElement() {
xml = xmlStack.pop();
}
@Override
public void startExtensionElements() {
// Set the extensionElements insertion index to first or second child element.
boolean xmlHasDocumentation = xml.getElement(BPMN_URI, "documentation") != null;
int extensionElementsIndex = xmlHasDocumentation ? 1 : 0;
// start or get is used as extensionElements might be added
// multiple times by different levels in the class hierarchy
// eg: a call activity might add stuff and it s super class activity might
// also add extensionElements
startOrGetElement(BPMN_URI, "extensionElements", extensionElementsIndex);
}
@Override
public void endExtensionElements() {
endElement();
xml.removeEmptyElement(BPMN_URI, "extensionElements");
}
public void startScope(Scope nestedScope) {
if (this.scope!=null) {
scopeStack.push(this.scope);
}
this.scope = nestedScope;
}
public void endScope() {
this.scope = scopeStack.pop();
}
protected void initializeNamespacePrefixes() {
if (xml.namespaces != null) {
bpmnPrefix = xml.namespaces.getPrefix(BPMN_URI);
effektifPrefix = xml.namespaces.getPrefix(EFFEKTIF_URI);
}
// Add any missing namespaces with their default prefixes.
if (bpmnPrefix == null) {
bpmnPrefix = "";
xml.addNamespace(BPMN_URI, bpmnPrefix);
}
if (bpmnDiagramPrefix == null) {
bpmnDiagramPrefix = "bpmndi";
xml.addNamespace(BPMN_DI_URI, bpmnDiagramPrefix);
xml.addNamespace(OMG_DC_URI, "omgdc");
xml.addNamespace(OMG_DI_URI, "omgdi");
}
if (effektifPrefix == null) {
effektifPrefix = "e";
xml.addNamespace(EFFEKTIF_URI, effektifPrefix);
}
}
protected XmlElement writeDefinitions(AbstractWorkflow workflow) {
startElementBpmn("definitions", workflow.getProperty(KEY_DEFINITIONS));
initializeNamespacePrefixes();
// Add the BPMN targetNamespace, which has nothing to do with XML schema and just identifies the modelled process.
if (xml.getAttribute(BPMN_URI, "targetNamespace") == null) {
xml.addAttribute(BPMN_URI, "targetNamespace", EFFEKTIF_URI);
}
writeWorkflow(workflow);
return xml;
}
protected void writeWorkflow(AbstractWorkflow workflow) {
startScope(workflow);
// Add the ‘process’ element as the first child element of the ‘definitions’ element.
startElementBpmn("process", workflow.getBpmn(), 0);
if (ExecutableWorkflow.class.isAssignableFrom(workflow.getClass())) {
ExecutableWorkflow executableWorkflow = (ExecutableWorkflow) workflow;
if (executableWorkflow.getSourceWorkflowId() == null && workflow.getId() != null) {
executableWorkflow.setSourceWorkflowId(workflow.getId().getInternal());
}
}
// Output documentation, workflow BPMN (extension elements) and scope (activities/transitions) in that order, as
// required by the BPMN schema.
writeDocumentation(workflow.getDescription());
workflow.writeBpmn(this);
writeScope();
endElement();
fixDiagramDuplicateIds(workflow);
writeDiagram(workflow);
}
/**
* Temporary #2932 fix - ensures that the export doesn’t generate XML that contains duplicate IDs (not well-formed).
* The fix is to preprend duplicate shape IDs
*/
private void fixDiagramDuplicateIds(AbstractWorkflow workflow) {
Diagram diagram = workflow.getDiagram();
if (diagram != null) {
// Replace shape IDs that duplicate activity IDs.
Set activityIds = workflow.getActivities() == null ? Collections.emptySet() :
workflow.getActivities().stream().map(activity -> activity.getId()).collect(Collectors.toSet());
if (diagram.canvas.hasChildren()) {
diagram.canvas.children.stream()
.filter(shape -> activityIds.contains(shape.id))
.forEach(shape -> shape.id("shape-" + shape.id));
}
if (diagram.edges != null) {
diagram.edges.stream()
.filter(edge -> activityIds.contains(edge.fromId))
.forEach(edge -> edge.fromId("shape-" + edge.fromId));
diagram.edges.stream()
.filter(edge -> activityIds.contains(edge.toId))
.forEach(edge -> edge.toId("shape-" + edge.toId));
// Replace edge IDs that duplicate transition IDs.
Set transitionIds = workflow.getTransitions() == null ? Collections.emptySet() :
workflow.getTransitions().stream().map(transition -> transition.getId()).collect(Collectors.toSet());
diagram.edges.stream()
.filter(edge -> transitionIds.contains(edge.id))
.forEach(edge -> edge.id("edge-" + edge.id));
}
}
}
private void writeDiagram(AbstractWorkflow workflow) {
Diagram diagram = workflow.getDiagram();
if (diagram != null) {
calculateOffsets(diagram);
startElementBpmnDiagram("BPMNDiagram");
writeIdAttributeBpmnDiagram("id", diagram.id);
writeStringAttributeBpmnDiagram("name", workflow.getName());
startElementBpmnDiagram("BPMNPlane");
writeIdAttributeBpmnDiagram("bpmnElement", workflow.getId().getInternal());
if (diagram.canvas.hasChildren()) {
for (Node shape : diagram.canvas.children) {
startElementBpmnDiagram("BPMNShape");
writeIdAttributeBpmnDiagram("id", shape.id);
writeIdAttributeBpmnDiagram("bpmnElement", shape.elementId);
if (shape.horizontal != null) {
writeStringAttributeBpmnDiagram("isHorizontal", shape.horizontal ? "true" : "false");
}
if (shape.expanded != null) {
writeStringAttributeBpmnDiagram("isExpanded", shape.expanded ? "true" : "false");
}
writeBpmnDiagramBounds(shape.bounds);
endElement();
}
}
if (diagram.hasEdges()) {
for (Edge edge : diagram.edges) {
startElementBpmnDiagram("BPMNEdge");
writeIdAttributeBpmnDiagram("id", edge.id);
writeIdAttributeBpmnDiagram("bpmnElement", edge.transitionId);
writeBpmnDiagramEdgeDockers(edge.dockers);
endElement();
}
}
endElement();
endElement();
}
}
protected void calculateOffsets(Diagram diagram) {
xOffset = Integer.MAX_VALUE;
yOffset = Integer.MAX_VALUE;
if (diagram!=null && diagram.canvas!=null && diagram.canvas.children!=null) {
for (Node child: diagram.canvas.children) {
scanOffset(child);
}
}
if (diagram!=null && diagram.edges!=null) {
for (Edge edge: diagram.edges) {
scanOffset(edge);
}
}
if (xOffset==Integer.MAX_VALUE) {
xOffset = 0;
} else {
xOffset -= EXPORT_MARGIN;
}
if (yOffset==Integer.MAX_VALUE) {
yOffset = 0;
} else {
yOffset -= EXPORT_MARGIN;
}
}
protected void scanOffset(Node node) {
if (node!=null
&& node.bounds!=null
&& node.bounds.upperLeft!=null) {
Point upperLeft = node.bounds.upperLeft;
xOffset = Math.min(xOffset, upperLeft.x);
yOffset = Math.min(yOffset, upperLeft.y);
}
if (node.children!=null) {
for (Node child: node.children) {
scanOffset(child);
}
}
}
protected void scanOffset(Edge edge) {
if (edge.dockers!=null) {
for (Point docker: edge.dockers) {
xOffset = Math.min(xOffset, docker.x);
yOffset = Math.min(yOffset, docker.y);
}
}
}
/**
* Writes a {@link Scope} as BPMN, which is implemented here instead of in {@link Scope#writeBpmn(BpmnWriter)}
* because it requires access to {@link #bpmnMappings}.
*/
public void writeScope() {
writeActivities(scope.getActivities());
writeTransitions(scope.getTransitions());
}
protected void writeActivities(List activities) {
if (activities!=null) {
for (Activity activity : activities) {
startScope(activity);
BpmnTypeMapping bpmnTypeMapping = getBpmnTypeMapping(activity.getClass());
startElementBpmn(bpmnTypeMapping.getBpmnElementName(), activity.getBpmn());
Map bpmnTypeAttributes = bpmnTypeMapping.getBpmnTypeAttributes();
if (bpmnTypeAttributes!=null) {
for (String attributeLocalPart: bpmnTypeAttributes.keySet()) {
String value = bpmnTypeAttributes.get(attributeLocalPart);
xml.addAttribute(EFFEKTIF_URI, attributeLocalPart, value);
}
}
activity.writeBpmn(this);
endElement();
endScope();
}
}
}
private BpmnTypeMapping getBpmnTypeMapping(Class extends Activity> activityClass) {
BpmnTypeMapping bpmnTypeMapping = bpmnMappings.getBpmnTypeMapping(activityClass);
if (bpmnTypeMapping == null) {
throw new RuntimeException("Register " + activityClass + " in class " + Mappings.class.getName() +
" with method registerSubClass and ensure annotation " + BpmnElement.class + " is set");
}
return bpmnTypeMapping;
}
protected void writeTransitions(List transitions) {
if (transitions!=null) {
for (Transition transition : transitions) {
startElementBpmn("sequenceFlow", transition.getBpmn());
transition.writeBpmn(this);
endElement();
}
}
}
/** Writes binding values as extension elements with the given local name and attribute name,
* e.g. or . */
@Override
public void writeBinding(Class modelClass, Binding binding) {
BpmnTypeMapping bpmnTypeMapping = bpmnMappings.getBpmnTypeMapping(modelClass);
String localPart = bpmnTypeMapping.getBpmnElementName();
writeBinding(localPart, binding);
}
/** Writes binding values as extension elements with the given local name and attribute name,
* e.g. or . */
@Override
public void writeBinding(String localPart, Binding binding) {
writeBinding(localPart, binding, null);
}
/** Writes binding values as extension elements with the given local name and attribute name,
* e.g. or . */
@Override
public void writeBinding(String localPart, Binding binding, String key) {
if (binding!=null) {
startElementEffektif(localPart);
if (key != null) {
writeStringAttributeEffektif("key", key);
}
T value = binding.getValue();
if (value!=null) {
writeStringAttributeEffektif("value", value);
writeTypeAttribute(bpmnMappings.getTypeByValue(value));
}
if (binding.getExpression()!=null) {
writeStringAttributeEffektif("expression", binding.getExpression());
}
if (binding.getMetadata() != null && !binding.getMetadata().isEmpty()) {
startElementEffektif("metadata");
writeSimpleProperties(binding.getMetadata());
endElement();
}
endElement();
}
}
@Override
public void writeBindings(String fieldName, List> bindings) {
if (bindings!=null) {
for (Binding binding: bindings) {
writeBinding(fieldName, binding);
}
}
}
/** Writes the given documentation string as a BPMN documentation
element. */
@Override
public void writeDocumentation(String documentation) {
if (documentation != null && !documentation.isEmpty()) {
// Set the insertion index to zero, because the BPMN spec requires this to be the first child element.
startElementBpmn("documentation");
xml.addText(documentation);
endElement();
}
}
/**
* Serialises properties with simple Java types as String values.
*/
@Override
public void writeSimpleProperties(Map properties) {
if (properties != null) {
for (Map.Entry property : properties.entrySet()) {
if (property.getValue() != null) {
Class> type = property.getValue().getClass();
boolean simpleType = type.isPrimitive() || type.getName().startsWith("java.lang.");
if (simpleType) {
startElementEffektif("property");
writeStringAttributeEffektif("key", property.getKey());
writeStringAttributeEffektif("value", property.getValue().toString());
writeStringAttributeEffektif("type", property.getValue().getClass().getName());
endElement();
}
}
}
}
}
@Override
public void writeStringAttributeBpmn(String localPart, Object value) {
if (value!=null) {
xml.addAttribute(BPMN_URI, localPart, value);
}
}
@Override
public void writeIdAttributeBpmnDiagram(String localPart, Object id) {
if (id != null) {
xml.addAttribute(BPMN_DI_URI, localPart, normaliseXmlId(id.toString()));
}
}
@Override
public void writeStringAttributeBpmnDiagram(String localPart, Object value) {
if (value!=null) {
xml.addAttribute(BPMN_DI_URI, localPart, value);
}
}
@Override
public void writeStringAttributeEffektif(String localPart, Object value) {
if (value!=null) {
xml.addAttribute(EFFEKTIF_URI, localPart, value);
}
}
@Override
public void writeIdAttributeBpmn(String localPart, Id id) {
if (id!=null) {
writeIdAttributeBpmn(localPart, id.getInternal());
}
}
@Override
public void writeIdAttributeBpmn(String localPart, String id) {
if (id != null && !id.isEmpty()) {
xml.addAttribute(BPMN_URI, localPart, normaliseXmlId(id));
}
}
/**
* Fixes an XML id attribute by prefixing it with an underscore if it doesn’t start with a letter character, as
* required by the XML Schema definition of NCName http://www.w3.org/TR/xmlschema-2/#NCName
*/
public static String normaliseXmlId(String id) {
if (id == null) {
return null;
}
boolean startsWithLetter = id.matches("^[_\\p{Ll}\\p{Lu}\\p{Lo}\\p{Lt}\\p{Nl}].*");
return startsWithLetter ? id : "_" + id;
}
@Override
public void writeIdAttributeEffektif(String localPart, Id value) {
if (value!=null) {
xml.addAttribute(EFFEKTIF_URI, localPart, value.getInternal());
}
}
@Override
public void writeCDataTextEffektif(String localPart, String value) {
if (value != null) {
xml.createElement(EFFEKTIF_URI, localPart).addCDataText(value);
}
}
@Override
public void writeDateAttributeBpmn(String localPart, LocalDateTime value) {
if (value!=null) {
xml.addAttribute(BPMN_URI, localPart, DATE_FORMAT.print(value));
}
}
@Override
public void writeDateAttributeEffektif(String localPart, LocalDateTime value) {
if (value != null) {
xml.addAttribute(EFFEKTIF_URI, localPart, DATE_FORMAT.print(value));
}
}
@Override
public void writeIntegerAttributeEffektif(String localPart, Integer value) {
if (value != null) {
xml.addAttribute(EFFEKTIF_URI, localPart, value.toString());
}
}
@Override
public void writeBooleanAttributeEffektif(String localPart, Boolean value) {
if (value != null) {
xml.addAttribute(EFFEKTIF_URI, localPart, value.toString());
}
}
@Override
public void writeRelativeTimeEffektif(String localPart, RelativeTime value) {
if (value != null && value.valid()) {
startElementEffektif(localPart);
value.writeBpmn(this);
endElement();
}
}
@Override
public void writeStringValue(String localPart, String attributeName, Object value) {
if (value!=null) {
xml.createElement(EFFEKTIF_URI, localPart).addAttribute(EFFEKTIF_URI, attributeName, value);
}
}
@Override
public void writeText(String value) {
if (value != null) {
xml.addText(value);
}
}
@Override
public void writeTextElementBpmn(String localPart, Object value) {
writeTextElement(BPMN_URI, localPart, value);
}
@Override
public void writeTextElementEffektif(String localPart, Object value) {
writeTextElement(EFFEKTIF_URI, localPart, value);
}
protected void writeTextElement(String namespaceUri, String localPart, Object value) {
if (value!=null) {
xml.createElement(namespaceUri, localPart).addText(value);
}
}
@Override
public void writeTypeAttribute(Object o) {
bpmnMappings.writeTypeAttribute(this, o, "type");
}
@Override
public void writeTypeElement(DataType type) {
if (type!=null) {
startElementEffektif("type");
bpmnMappings.writeTypeAttribute(this, type, "name");
type.writeBpmn(this);
endElement();
}
}
private void writeBpmnDiagramBounds(Bounds bounds) {
if (bounds != null && bounds.isValid()) {
startElement(xml.createElement(OMG_DC_URI, "Bounds"));
xml.addAttribute(OMG_DC_URI, "height", bounds.getHeight());
xml.addAttribute(OMG_DC_URI, "width", bounds.getWidth());
xml.addAttribute(OMG_DC_URI, "x", bounds.upperLeft.x-xOffset);
xml.addAttribute(OMG_DC_URI, "y", bounds.upperLeft.y-yOffset);
endElement();
}
}
private void writeBpmnDiagramEdgeDockers(List dockers) {
if (dockers != null) {
for (Point waypoint : dockers) {
startElement(xml.createElement(OMG_DI_URI, "waypoint"));
xml.addAttribute(OMG_DI_URI, "x", waypoint.x-xOffset);
xml.addAttribute(OMG_DI_URI, "y", waypoint.y-yOffset);
endElement();
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy