/* 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 static com.effektif.workflow.impl.bpmn.Bpmn.*; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Set; import java.util.SortedSet; import java.util.Stack; import java.util.stream.Collectors; import com.effektif.workflow.api.bpmn.XmlNamespaces; import com.effektif.workflow.api.condition.SingleBindingCondition; import com.effektif.workflow.api.workflow.*; import com.effektif.workflow.impl.workflow.boundary.BoundaryEventTimer; import org.joda.time.LocalDateTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.effektif.workflow.api.bpmn.BpmnReadable; import com.effektif.workflow.api.bpmn.BpmnReader; import com.effektif.workflow.api.bpmn.XmlElement; import com.effektif.workflow.api.condition.Condition; 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.diagram.Bounds; import com.effektif.workflow.api.workflow.diagram.Diagram; import com.effektif.workflow.api.workflow.diagram.Edge; import com.effektif.workflow.api.workflow.diagram.Node; import com.effektif.workflow.api.workflow.diagram.Point; import com.effektif.workflow.impl.exceptions.BadRequestException; import com.effektif.workflow.impl.json.JsonObjectReader; import com.effektif.workflow.impl.json.JsonStreamMapper; import com.effektif.workflow.impl.json.JsonTypeMapper; import com.effektif.workflow.impl.json.PolymorphicMapping; import com.effektif.workflow.impl.json.TypeMapping; import com.effektif.workflow.impl.json.types.LocalDateTimeStreamMapper; /** * This implementation of the BPMN reader is based on reading single values from XML elements and attributes into * single-valued (mostly primitive) types. Complex types and arbitrary Java beans are not supported * * To support complex types in the future, a preferable alternative to implementing an bean mapping framework will be to * leverage the existing JSON mapping implementation, which supports nested structures, and read complex objects from * JSON embedded in CDATA sections in the BPMN. For example, something like: * * <pre> * <e:input key="user"> * <e:binding type="json"><![CDATA[ * { type: "user", id: 42, name: "Joe Bloggs" } * ]]></e:binding> * </e:input> * </pre> * * TODO Refactor to make reading use a more consistent API than the current read methods: * a mix between model class readBpmn methods, and read* methods in this class with inconsistent parameter lists. * * @author Tom Baeyens */ public class BpmnReaderImpl implements BpmnReader { private static final Logger log = LoggerFactory.getLogger(BpmnReaderImpl.class); /** global mappings */ protected BpmnMappings bpmnMappings; /** stack of scopes */ protected Stack<Scope> scopeStack = new Stack<Scope>(); /** current scope */ protected Scope scope; /** stack of xml elements */ protected Stack<XmlElement> xmlStack = new Stack<XmlElement>(); /** current xml element */ protected XmlElement currentXml; protected Class<?> currentClass; protected JsonStreamMapper jsonStreamMapper; public BpmnReaderImpl(BpmnMappings bpmnMappings, JsonStreamMapper jsonStreamMapper) { this.bpmnMappings = bpmnMappings; this.jsonStreamMapper = jsonStreamMapper; } /** * The BPMN <code>definitions</code> element includes the document’s XML namespace declarations. * Ideally, namespaces should be read in a stack, so that each element can add new namespaces. * The addPrefixes() should then be refactored to pushPrefixes and popPrefixes. * The current implementation assumes that all namespaces are defined in the root element. */ protected AbstractWorkflow readDefinitions(XmlElement definitionsXml) { AbstractWorkflow workflow = null; if (definitionsXml.elements != null) { Iterator<XmlElement> iterator = definitionsXml.elements.iterator(); while (iterator.hasNext()) { XmlElement definitionElement = iterator.next(); boolean processAlreadyParsed = workflow != null; if (definitionElement.is(BPMN_URI, "process") && !processAlreadyParsed) { iterator.remove(); workflow = readWorkflow(definitionElement); } } } if (workflow == null) { workflow = new ExecutableWorkflow(); } readDiagram(workflow, definitionsXml); definitionsXml.cleanEmptyElements(); workflow.property(KEY_DEFINITIONS, definitionsXml); return workflow; } protected AbstractWorkflow readWorkflow(XmlElement processXml) { AbstractWorkflow workflow = new ExecutableWorkflow(); this.currentXml = processXml; this.scope = workflow; workflow.readBpmn(this); attachTimers(workflow); readLanes(workflow); removeDanglingTransitions(workflow); setUnparsedBpmn(workflow, processXml); workflow.cleanUnparsedBpmn(); return workflow; } protected void attachTimers(AbstractWorkflow workflow) { List<Timer> timers = workflow.getTimers(); if (timers != null && timers.size() > 0) { Iterator<Timer> timerIterator = timers.iterator(); while (timerIterator.hasNext()) { Timer timer = timerIterator.next(); // todo: make generic if (timer instanceof BoundaryEventTimer) { BoundaryEvent boundaryEvent = ((BoundaryEventTimer) timer).boundaryEvent; if (boundaryEvent != null) { Activity act = workflow.findActivity(boundaryEvent.getFromId()); if (act != null) act.timer(timer); timerIterator.remove(); } } } } } protected void readLanes(AbstractWorkflow workflow) { // Not supported. } public void readScope() { if (currentXml.elements!=null) { Iterator<XmlElement> iterator = currentXml.elements.iterator(); while (iterator.hasNext()) { XmlElement scopeElement = iterator.next(); startElement(scopeElement); if (scopeElement.is(BPMN_URI, "extensionElements")) { scope.setProperties(readSimpleProperties()); } else if (scopeElement.is(BPMN_URI, "sequenceFlow")) { Transition transition = new Transition(); transition.readBpmn(this); scope.transition(transition); // Remove the sequenceFlow as it has been parsed in the model. iterator.remove(); } else if (scopeElement.is(BPMN_URI, "boundaryEvent")) { // <bpmn:boundaryEvent id="BoundaryEvent_1ymyt09" attachedToRef="Task_02wgtff"> // <bpmn:outgoing>SequenceFlow_0se37xg</bpmn:outgoing> // <bpmn:timerEventDefinition> // <bpmn:timeDuration>PT5M</bpmn:timeDuration> // </bpmn:timerEventDefinition> // </bpmn:boundaryEvent> // <bpmn:sequenceFlow id="SequenceFlow_0se37xg" sourceRef="BoundaryEvent_1ymyt09" targetRef="Task_13koiv2" /> startElement(scopeElement); BoundaryEvent boundaryEvent = new BoundaryEvent(); boundaryEvent.readBpmn(this); for (XmlElement xmlElement : currentXml.getElements()) { BpmnTypeMapping typeMapping = bpmnMappings.getBpmnTypeMapping(xmlElement, this); if (typeMapping != null) { BoundaryEventTimer timer = new BoundaryEventTimer(); startElement(xmlElement); timer.readBpmn(this); timer.boundaryEvent = boundaryEvent; endElement(); scope.timer(timer); } } iterator.remove(); endElement(); } else { BpmnTypeMapping bpmnTypeMapping = getBpmnTypeMapping(); if (bpmnTypeMapping != null) { // Check whether the BPMN type mapping is to an Activity or Timer, etc. Object bpmnElement = bpmnTypeMapping.instantiate(); if (bpmnElement instanceof Activity) { Activity activity = (Activity) bpmnElement; // read the fields activity.readBpmn(this); scope.activity(activity); setUnparsedBpmn(activity, currentXml); activity.cleanUnparsedBpmn(); // Remove the activity XML element as it has been parsed in the model. iterator.remove(); } } } endElement(); } currentXml.removeEmptyElement(BPMN_URI, "extensionElements"); } } /** * Check if the XML element can be parsed as one of the activity types. */ protected BpmnTypeMapping getBpmnTypeMapping() { return bpmnMappings.getBpmnTypeMapping(currentXml, this); } protected void setUnparsedBpmn(Scope scope, XmlElement unparsedBpmn) { unparsedBpmn.name = null; scope.setBpmn(unparsedBpmn); } @Override public List<XmlElement> readElementsBpmn(String localPart) { if (currentXml==null) { return Collections.EMPTY_LIST; } return currentXml.removeElements(BPMN_URI, localPart); } @Override public List<XmlElement> readElementsEffektif(Class modelClass) { BpmnTypeMapping bpmnTypeMapping = bpmnMappings.getBpmnTypeMapping(modelClass); String localPart = bpmnTypeMapping.getBpmnElementName(); return readElementsEffektif(localPart); } @Override public List<XmlElement> readElementsEffektif(String localPart) { if (currentXml==null) { return Collections.EMPTY_LIST; } return currentXml.removeElements(EFFEKTIF_URI, localPart); } @Override public XmlElement readElementEffektif(String localPart) { if (currentXml==null) { return null; } List<XmlElement> xmlElements = currentXml.removeElements(EFFEKTIF_URI, localPart); return !xmlElements.isEmpty() ? xmlElements.get(0) : null; } @Override public void startElement(XmlElement xmlElement) { if (currentXml!=null) { xmlStack.push(currentXml); } currentXml = xmlElement; } @Override public void endElement() { currentXml = xmlStack.empty() ? null : xmlStack.pop(); } public void startScope(Scope scope) { if (this.scope!=null) { scopeStack.push(this.scope); } this.scope = scope; } public void endScope() { this.scope = scopeStack.pop(); } @Override public void startExtensionElements() { XmlElement extensionsXmlElement = currentXml.getElement(BPMN_URI, "extensionElements"); startElement(extensionsXmlElement); } @Override public void endExtensionElements() { endElement(); currentXml.removeEmptyElement(BPMN_URI, "extensionElements"); } @Override public Boolean readBooleanAttributeEffektif(String localPart) { if (currentXml==null) { return null; } String booleanStringValue = currentXml.removeAttribute(BPMN_URI, localPart); if (booleanStringValue==null) { return null; } return Boolean.valueOf(booleanStringValue); } @Override public String readStringAttributeBpmn(String localPart) { if (currentXml==null) { return null; } return currentXml.removeAttribute(BPMN_URI, localPart); } @Override public String readStringAttributeEffektif(String localPart) { if (currentXml==null) { return null; } return currentXml.removeAttribute(EFFEKTIF_URI, localPart); } @Override public <T extends Id> T readIdAttributeBpmn(String localPart, Class<T> idType) { if (currentXml==null) { return null; } return toId(readStringAttributeBpmn(localPart), idType); } @Override public <T extends Id> T readIdAttributeEffektif(String localPart, Class<T> idType) { if (currentXml==null) { return null; } return toId(readStringAttributeEffektif(localPart), idType); } @Override public Integer readIntegerAttributeEffektif(String localPart) { if (currentXml==null) { return null; } String valueString = readStringAttributeEffektif(localPart); try { return new Integer(valueString); } catch (NumberFormatException e) { return null; } } @Override public <T> Binding<T> readBinding(Class modelClass, Class<T> type) { BpmnTypeMapping bpmnTypeMapping = bpmnMappings.getBpmnTypeMapping(modelClass); String localPart = bpmnTypeMapping.getBpmnElementName(); if (currentXml != null && currentXml.getName().equals(localPart)) { // in some cases the respective element which contains the binding information was already started return readBindingFromCurrentElement(); } return readBinding(localPart, type); } /** Returns a binding from the first extension element with the given name. */ @Override public <T> Binding<T> readBinding(String localPart, Class<T> type) { if (currentXml==null) { return null; } List<Binding<T>> bindings = readBindings(localPart); if (bindings.isEmpty()) { return new Binding<T>(); } else { return bindings.get(0); } } /** Returns a list of bindings from the extension elements with the given name. */ @Override public <T> List<Binding<T>> readBindings(String localPart) { if (currentXml==null) { return null; } List<Binding<T>> bindings = new ArrayList<>(); for (XmlElement element: currentXml.removeElements(EFFEKTIF_URI, localPart)) { Binding binding = new Binding(); String value = element.getAttribute(EFFEKTIF_URI, "value"); String typeName = element.getAttribute(EFFEKTIF_URI, "type"); startElement(element); XmlElement metadataElement = readElementEffektif("metadata"); Map<String, Object> metadata = null; if (metadataElement != null) { startElement(metadataElement); metadata = readSimpleProperties(); endElement(); } endElement(); DataType type = convertType(typeName); binding.setValue(parseText(value, (Class<Object>) type.getValueType())); binding.setExpression(element.getAttribute(EFFEKTIF_URI, "expression")); binding.setMetadata(metadata); bindings.add(binding); } return bindings; } private <T> Binding<T> readBindingFromCurrentElement() { if (currentXml != null) { Binding binding = new Binding(); String value = currentXml.getAttribute(EFFEKTIF_URI, "value"); String typeName = currentXml.getAttribute(EFFEKTIF_URI, "type"); XmlElement metadataElement = readElementEffektif("metadata"); Map<String, Object> metadata = null; if (metadataElement != null) { startElement(metadataElement); metadata = readSimpleProperties(); endElement(); } DataType type = convertType(typeName); binding.setValue(parseText(value, (Class<Object>) type.getValueType())); binding.setExpression(currentXml.getAttribute(EFFEKTIF_URI, "expression")); binding.setMetadata(metadata); return binding; } return null; } @SuppressWarnings("unchecked") protected <T> T parseText(String value, Class<T> type) { if (value==null) { return null; } if (type==String.class) { return (T) value; } if (type==Boolean.class) { return (T) Boolean.valueOf(value); } if (type==Double.class) { return (T) Double.valueOf(value); } if (type==Long.class) { return (T) Long.valueOf(value); } if (Id.class.isAssignableFrom(type)) { return (T) toId(value, (Class<Id>) type); } if (type==LocalDateTime.class) { return (T) LocalDateTimeStreamMapper.PARSER.parseLocalDateTime(value); } if (type==Number.class) { return (T) Double.valueOf(value); } // Use a registered JSON type mapper to parse the value. JsonObjectReader jsonReader = new JsonObjectReader(bpmnMappings); JsonTypeMapper typeMapper = bpmnMappings.getTypeMapper(type); return (T) typeMapper.read(value, jsonReader); } /** * Returns an ID type instance, constructed from the given JSON string ID. */ private static final Class< ? >[] ID_CONSTRUCTOR_PARAMETERS = new Class< ? >[] { String.class }; public static <T extends Id> T toId(Object jsonId, Class<T> idType) { if (jsonId==null) { return null; } try { jsonId = jsonId.toString(); Constructor<T> c = idType.getDeclaredConstructor(ID_CONSTRUCTOR_PARAMETERS); return (T) c.newInstance(new Object[] { jsonId }); } catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { throw new RuntimeException(e); } } /** Returns the contents of the BPMN <code>documentation</code> element. */ @Override public String readDocumentation() { if (currentXml==null) { return null; } XmlElement documentationElement = currentXml.removeElement(BPMN_URI, "documentation"); if (documentationElement!=null) { return documentationElement.getText(); } return null; } @Override public Trigger readTriggerEffektif() { try { PolymorphicMapping triggerMapping = bpmnMappings.getPolymorphicMapping(Trigger.class); TypeMapping triggerSubclassMapping = triggerMapping.getTypeMapping(this); Trigger type = (Trigger) triggerSubclassMapping.instantiate(); type.readBpmn(this); return type; } catch (Exception e) { throw new RuntimeException(e); } } @Override public <T extends BpmnReadable> T readPolymorphicEffektif(XmlElement xmlElement, Class<T> type) { try { startElement(xmlElement); PolymorphicMapping polymorphicMapping = bpmnMappings.getPolymorphicMapping(type); TypeMapping subclassMapping = polymorphicMapping.getTypeMapping(this); T object = (T) subclassMapping.instantiate(); object.readBpmn(this); endElement(); return object; } catch (Exception e) { throw new RuntimeException(e); } } @Override public DataType readTypeAttributeEffektif() { return readTypeAttributeEffektif("type"); } private DataType readTypeAttributeEffektif(String attributeName) { String typeName = readStringAttributeEffektif(attributeName); return convertType(typeName); } @Override public LocalDateTime readDateAttributeEffektif(String attributeName) { String dateStringName = readStringAttributeEffektif(attributeName); return dateStringName!=null ? LocalDateTimeStreamMapper.PARSER.parseLocalDateTime(dateStringName) : null; } private DataType convertType(String typeName) { if (typeName == null) { typeName = "text"; } PolymorphicMapping dataTypeMapping = bpmnMappings.getPolymorphicMapping(DataType.class); DataType type = (DataType) dataTypeMapping.getTypeMapping(typeName).instantiate(); type.readBpmn(this); return type; } @Override public DataType readTypeElementEffektif() { XmlElement typeElement = readElementEffektif("type"); DataType type = null; if (typeElement!=null) { startElement(typeElement); type = readTypeAttributeEffektif("name"); endElement(); } return type; } @Override public RelativeTime readRelativeTimeEffektif(String localPart) { RelativeTime relativeTime = null; XmlElement element = currentXml != null ? currentXml.removeElement(EFFEKTIF_URI, localPart) : null; if (element != null) { startElement(element); relativeTime = RelativeTime.readBpmnPolymorphic(this); endElement(); } return relativeTime; } @Override public LocalDateTime readDateValue(String localPart) { XmlElement element = currentXml != null ? currentXml.removeElement(EFFEKTIF_URI, localPart) : null; if (element != null) { String value = element.getAttribute(EFFEKTIF_URI, "value"); if (value != null) { return LocalDateTimeStreamMapper.PARSER.parseLocalDateTime(value); } } return null; } /** * Reads nested property elements: currently only String, Boolean, Integer and Double properties are supported. */ @Override public Map<String,Object> readSimpleProperties() { Map<String,Object> properties = new HashMap<>(); for (XmlElement element : readElementsEffektif("property")) { startElement(element); String key = readStringAttributeEffektif("key"); String value = readStringAttributeEffektif("value"); String type = readStringAttributeEffektif("type"); if (key != null && value != null && type != null) { try { if (String.class.getName().equals(type)) { properties.put(key, value.toString()); } else if (Boolean.class.getName().equals(type)) { properties.put(key, Boolean.valueOf(value)); } else if (Integer.class.getName().equals(type)) { properties.put(key, Integer.valueOf(value)); } else if (Double.class.getName().equals(type)) { properties.put(key, Double.valueOf(value)); } else { log.warn(String.format("Unsupported property type ‘%s’ for property %s=%s", type, key, value)); } } catch (NumberFormatException e) { log.warn(String.format("Unsupported value format for type ‘%s’ for property %s=%s", type, key, value)); } } endElement(); } return properties; } @Override public String readStringValue(String localPart) { XmlElement element = currentXml != null ? currentXml.removeElement(EFFEKTIF_URI, localPart) : null; if (element != null) { return element.getAttribute(EFFEKTIF_URI, "value"); } return null; } @Override public String readTextBpmn(String localPart) { return readText(BPMN_URI, localPart); } @Override public String readTextEffektif(String localPart) { return readText(EFFEKTIF_URI, localPart); } private String readText(String namespaceUri, String localPart) { XmlElement textElement = currentXml!=null ? currentXml.removeElement(namespaceUri, localPart) : null; if (textElement!=null) { return textElement.getText(); } return null; } @Override public XmlElement getUnparsedXml() { return currentXml; } public Condition readCondition() { List<Condition> conditions = readConditions(); if (conditions.size() > 0) { return conditions.get(0); } return null; } /** * Returns a list of {@link Condition} instances by using this reader to read BPMN for all of the condition types. */ @Override public List<Condition> readConditions() { List<Condition> conditions = new ArrayList<>(); SortedSet<Class<?>> bpmnClasses = bpmnMappings.getBpmnClasses(); for (Class bpmnClass : bpmnClasses) { if (Condition.class.isAssignableFrom(bpmnClass)) { for (XmlElement xmlElement : readElementsEffektif(bpmnClass)) { startElement(xmlElement); try { Condition condition = (Condition) bpmnClass.newInstance(); condition.readBpmn(this); if (!condition.isEmpty()) { conditions.add(condition); } } catch (Exception e) { throw new RuntimeException("Could not read condition type " + bpmnClass.getName()); } endElement(); } } } return conditions; } /** * Removes transitions to or from a missing activity, probably due to the activity not being imported. */ private void removeDanglingTransitions(AbstractWorkflow workflow) { if (workflow.getTransitions() == null || workflow.getTransitions().isEmpty()) { return; } Set<String> activityIds = new HashSet<>(); for (Activity activity : workflow.getActivities()) { activityIds.add(activity.getId()); // Transitions from Boundary event timers should be included as well // todo: make generic List<Timer> activityTimers = activity.getTimers(); if (activityTimers != null) { for (Timer timer : activityTimers) { if (timer instanceof BoundaryEventTimer) { BoundaryEvent boundaryEvent = ((BoundaryEventTimer) timer).boundaryEvent; activityIds.add(boundaryEvent.getBoundaryId()); activityIds.addAll(boundaryEvent.getToTransitionIds()); } } } } ListIterator<Transition> transitionIterator = workflow.getTransitions().listIterator(); while(transitionIterator.hasNext()){ Transition transition = transitionIterator.next(); if (!activityIds.contains(transition.getFromId()) || !activityIds.contains(transition.getToId())) { transitionIterator.remove(); } } } /** * Reads the workflow name, description and diagram from BPMN. */ private void readDiagram(AbstractWorkflow workflow, XmlElement definitionsXml) { if (definitionsXml==null) { return; } for (XmlElement diagramElement: definitionsXml.removeElements(BPMN_DI_URI, "BPMNDiagram")) { startElement(diagramElement); if (currentXml==null) { return; } workflow.setName(currentXml.removeAttribute(BPMN_DI_URI, "name")); if (workflow.getDescription() == null) { workflow.setDescription(currentXml.removeAttribute(BPMN_DI_URI, "documentation")); } Diagram diagram = new Diagram(); for (XmlElement planeElement: diagramElement.removeElements(BPMN_DI_URI, "BPMNPlane")) { List<Node> shapes = readShapes(planeElement); diagram.addNodes(shapes); if (workflow.getTransitions() != null) { diagram.edges(readEdges(shapes, workflow.getTransitions(), planeElement)); } } // Reference the process we’re importing from the diagram, setting it directly because the BPMNPlane/@elementId // may refer to multiple processes via definitions/collaboration and its nested participants. if (workflow.getId() != null) { diagram.canvas.elementId = workflow.getId().getInternal(); } workflow.setDiagram(diagram); removeOrphanedDiagramElements(workflow, definitionsXml); endElement(); } } /** * Removes diagram shapes that don’t correspond to an imported workflow activity, such as those not supported. */ private void removeOrphanedDiagramElements(AbstractWorkflow workflow, XmlElement definitionsXml) { Diagram diagram = workflow.getDiagram(); if (diagram == null || !diagram.hasChildren()) { return; } // Collect valid participant (pool) IDs. Set<String> participantIds = findParticipantIds(definitionsXml); participantIds.forEach(id -> log.debug("POOL = " + id)); // Collect valid activity IDs and variable IDs, which lane IDs are mapped to. Set<String> activityIds = workflow.getActivities() == null ? new HashSet<>() : workflow.getActivities().stream().map(activity -> activity.getId()).collect(Collectors.toSet()); Set<String> variableIds = workflow.getVariables() == null ? new HashSet<>() : workflow.getVariables().stream().map(variable -> variable.getId()).collect(Collectors.toSet()); // Remove orphaned shapes/nodes. Set<String> shapeIds = new HashSet<>(); if (diagram.hasChildren()) { Iterator<Node> shapeIterator = diagram.canvas.children.iterator(); while (shapeIterator.hasNext()) { Node shape = shapeIterator.next(); // Keep shapes for lanes by checking against variable IDs, since lanes are the only shapes mapped to variables. boolean poolShape = participantIds.contains(shape.elementId); boolean laneShape = activityIds.contains(shape.elementId) || variableIds.contains(shape.elementId); if (!poolShape && !laneShape) { shapeIterator.remove(); } } // Collect valid shape IDs. for (Node shape : diagram.canvas.children) { shapeIds.add(shape.id); } } // Remove orphaned edges. if (diagram.hasEdges()) { Iterator<Edge> edgeIterator = diagram.edges.iterator(); while (edgeIterator.hasNext()) { Edge edge = edgeIterator.next(); boolean transitionDefined = workflow.findTransition(edge.transitionId) != null; boolean edgeValid = shapeIds.contains(edge.fromId) && shapeIds.contains(edge.toId) && transitionDefined; if (!edgeValid) { edgeIterator.remove(); } } } } private Set<String> findParticipantIds(XmlElement definitions) { Set<String> ids = definitions == null ? new HashSet<>() : definitions.elements.stream() .filter(element -> element.name.equals("collaboration")) .flatMap(collaboration -> collaboration.elements.stream()) .filter(element -> element.name.equals("participant")) .map(participant -> participant.getAttribute(BPMN_URI, "id")) .collect(Collectors.toSet()); return ids; } private List<Node> readShapes(XmlElement planeElement) { List<Node> nodes = new ArrayList<>(); for (XmlElement shapeElement: planeElement.removeElements(BPMN_DI_URI, "BPMNShape")) { startElement(shapeElement); String id = currentXml.removeAttribute(BPMN_DI_URI, "id"); String elementId = currentXml.removeAttribute(BPMN_DI_URI, "bpmnElement"); Node node = new Node() .id(id) .elementId(elementId); // Read the optional BPMN attribute that indicates lane orientation. String horizontal = currentXml.removeAttribute(BPMN_DI_URI, "isHorizontal"); if (horizontal != null) { node.horizontal(horizontal.equals("true")); } String expanded = currentXml.removeAttribute(BPMN_DI_URI, "isExpanded"); if (expanded != null) { node.expanded(expanded.equals("true")); } for (XmlElement boundsElement: shapeElement.removeElements(OMG_DC_URI, "Bounds")) { startElement(boundsElement); double x = Double.valueOf(currentXml.removeAttribute(OMG_DC_URI, "x")); double y = Double.valueOf(currentXml.removeAttribute(OMG_DC_URI, "y")); double width = Double.valueOf(currentXml.removeAttribute(OMG_DC_URI, "width")); double height = Double.valueOf(currentXml.removeAttribute(OMG_DC_URI, "height")); node.bounds(new Bounds(new Point(x, y), width, height)); nodes.add(node); endElement(); } endElement(); } return nodes; } /** * Returns a list of edges read from sequenceFlow element transitions, and BPMNEdge coordinates. */ private List<Edge> readEdges(List<Node> shapes, List<Transition> transitions, XmlElement planeElement) { Map<String, Edge> edgesBySequenceFlowId = readEdgesBySequenceFlowId(planeElement); // Map shape activity IDs to shape IDs, which are needed for edge from/to IDs. Map<String,String> nodeIdByActivityId = new HashMap<>(); for (Node shape : shapes) { nodeIdByActivityId.put(shape.elementId, shape.id); } // Add node IDs from the previously-parsed workflow transitions and diagram nodes. List<Edge> edges = new ArrayList<>(); for (Transition transition : transitions) { String sequenceFlowId = transition.getId(); Edge edge = edgesBySequenceFlowId.get(sequenceFlowId); if (edge==null) { BadRequestException.checkNotNull(edge, "No edge for sequenceFlow " + sequenceFlowId); } edge.fromId(nodeIdByActivityId.get(transition.getFromId())); edge.toId(nodeIdByActivityId.get(transition.getToId())); edges.add(edge); } return edges; } private Map<String, Edge> readEdgesBySequenceFlowId(XmlElement planeElement) { Map<String, Edge> edges = new HashMap<>(); for (XmlElement edgeElement: planeElement.removeElements(BPMN_DI_URI, "BPMNEdge")) { startElement(edgeElement); List<Point> edgeWaypoints = new ArrayList<>(); for (XmlElement pointElement: edgeElement.removeElements(OMG_DI_URI, "waypoint")) { startElement(pointElement); double x = Double.valueOf(currentXml.removeAttribute(OMG_DI_URI, "x")); double y = Double.valueOf(currentXml.removeAttribute(OMG_DI_URI, "y")); edgeWaypoints.add(new Point(x, y)); endElement(); } String id = currentXml.removeAttribute(BPMN_DI_URI, "id"); String sequenceFlowId = currentXml.removeAttribute(BPMN_DI_URI, "bpmnElement"); Edge edge = new Edge().id(id).transitionId(sequenceFlowId).dockers(edgeWaypoints); edges.put(sequenceFlowId, edge); endElement(); } return edges; } }