Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 47 additions & 39 deletions SpiffWorkflow/bpmn/parser/BpmnParser.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@
# 02110-1301 USA

import glob
import os

from lxml import etree
from lxml.etree import DocumentInvalid

from SpiffWorkflow.bpmn.specs.events.event_definitions import NoneEventDefinition

Expand All @@ -37,6 +39,7 @@
from ..specs.ServiceTask import ServiceTask
from ..specs.UserTask import UserTask
from .ProcessParser import ProcessParser
from .node_parser import DEFAULT_NSMAP
from .util import full_tag, xpath_eval, first
from .task_parsers import (UserTaskParser, NoneTaskParser, ManualTaskParser,
ExclusiveGatewayParser, ParallelGatewayParser, InclusiveGatewayParser,
Expand All @@ -47,6 +50,28 @@
SendTaskParser, ReceiveTaskParser)


XSD_PATH = os.path.join(os.path.dirname(__file__), 'schema', 'BPMN20.xsd')

class BpmnValidator:

def __init__(self, xsd_path=XSD_PATH, imports=None):
schema = etree.parse(open(xsd_path))
if imports is not None:
for ns, fn in imports.items():
elem = etree.Element(
'{http://www.w3.org/2001/XMLSchema}import',
namespace=ns,
schemaLocation=fn
)
schema.getroot().insert(0, elem)
self.validator = etree.XMLSchema(schema)

def validate(self, bpmn, filename=None):
try:
self.validator.assertValid(bpmn)
except DocumentInvalid as di:
raise DocumentInvalid(str(di) + "file: " + filename)

class BpmnParser(object):
"""
The BpmnParser class is a pluggable base class that manages the parsing of
Expand Down Expand Up @@ -83,15 +108,16 @@ class BpmnParser(object):

PROCESS_PARSER_CLASS = ProcessParser

def __init__(self):
def __init__(self, namespaces=None, validator=None):
"""
Constructor.
"""
self.namespaces = namespaces or DEFAULT_NSMAP
self.validator = validator
self.process_parsers = {}
self.process_parsers_by_name = {}
self.collaborations = {}
self.process_dependencies = set()
self.dmn_dependencies = set()

def _get_parser_class(self, tag):
if tag in self.OVERRIDE_PARSER_CLASSES:
Expand Down Expand Up @@ -146,46 +172,31 @@ def add_bpmn_xml(self, bpmn, filename=None):
file
:param filename: Optionally, provide the source filename.
"""
xpath = xpath_eval(bpmn)
# do a check on our bpmn to ensure that no id appears twice
# this *should* be taken care of by our modeler - so this test
# should never fail.
ids = [x for x in xpath('.//bpmn:*[@id]')]
foundids = {}
for node in ids:
id = node.get('id')
if foundids.get(id,None) is not None:
raise ValidationException(
'The bpmn document should have no repeating ids but (%s) repeats'%id,
node=node,
filename=filename)
else:
foundids[id] = 1

for process in xpath('.//bpmn:process'):
self.create_parser(process, xpath, filename)

self._find_dependencies(xpath)

collaboration = first(xpath('.//bpmn:collaboration'))
if self.validator:
self.validator.validate(bpmn, filename)

self._add_processes(bpmn, filename)
self._add_collaborations(bpmn)

def _add_processes(self, bpmn, filename=None):
for process in bpmn.xpath('.//bpmn:process', namespaces=self.namespaces):
self._find_dependencies(process)
self.create_parser(process, filename)

def _add_collaborations(self, bpmn):
collaboration = first(bpmn.xpath('.//bpmn:collaboration', namespaces=self.namespaces))
if collaboration is not None:
collaboration_xpath = xpath_eval(collaboration)
name = collaboration.get('id')
self.collaborations[name] = [ participant.get('processRef') for participant in collaboration_xpath('.//bpmn:participant') ]

def _find_dependencies(self, xpath):
"""Locate all calls to external BPMN and DMN files, and store their
ids in our list of dependencies"""
for call_activity in xpath('.//bpmn:callActivity'):
def _find_dependencies(self, process):
"""Locate all calls to external BPMN, and store their ids in our list of dependencies"""
for call_activity in process.xpath('.//bpmn:callActivity', namespaces=self.namespaces):
self.process_dependencies.add(call_activity.get('calledElement'))
parser_cls, cls = self._get_parser_class(full_tag('businessRuleTask'))
if parser_cls:
for business_rule in xpath('.//bpmn:businessRuleTask'):
self.dmn_dependencies.add(parser_cls.get_decision_ref(business_rule))


def create_parser(self, node, doc_xpath, filename=None, lane=None):
parser = self.PROCESS_PARSER_CLASS(self, node, filename=filename, doc_xpath=doc_xpath, lane=lane)
def create_parser(self, node, filename=None, lane=None):
parser = self.PROCESS_PARSER_CLASS(self, node, self.namespaces, filename=filename, lane=lane)
if parser.get_id() in self.process_parsers:
raise ValidationException('Duplicate process ID', node=node, filename=filename)
if parser.get_name() in self.process_parsers_by_name:
Expand All @@ -194,14 +205,11 @@ def create_parser(self, node, doc_xpath, filename=None, lane=None):
self.process_parsers_by_name[parser.get_name()] = parser

def get_dependencies(self):
return self.process_dependencies.union(self.dmn_dependencies)
return self.process_dependencies

def get_process_dependencies(self):
return self.process_dependencies

def get_dmn_dependencies(self):
return self.dmn_dependencies

def get_spec(self, process_id_or_name):
"""
Parses the required subset of the BPMN files, in order to provide an
Expand Down
11 changes: 5 additions & 6 deletions SpiffWorkflow/bpmn/parser/ProcessParser.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class ProcessParser(NodeParser):
process.
"""

def __init__(self, p, node, filename=None, doc_xpath=None, lane=None):
def __init__(self, p, node, nsmap, filename=None, lane=None):
"""
Constructor.

Expand All @@ -39,7 +39,7 @@ def __init__(self, p, node, filename=None, doc_xpath=None, lane=None):
:param doc_xpath: an xpath evaluator for the document (optional)
:param lane: the lane of a subprocess (optional)
"""
super().__init__(node, filename, doc_xpath, lane)
super().__init__(node, nsmap, filename=filename, lane=lane)
self.parser = p
self.parsed_nodes = {}
self.lane = lane
Expand All @@ -58,15 +58,14 @@ def parse_node(self, node):
can be called by a TaskParser instance, that is owned by this
ProcessParser.
"""

if node.get('id') in self.parsed_nodes:
return self.parsed_nodes[node.get('id')]

(node_parser, spec_class) = self.parser._get_parser_class(node.tag)
if not node_parser or not spec_class:
raise ValidationException("There is no support implemented for this task type.",
node=node, filename=self.filename)
np = node_parser(self, spec_class, node, self.lane)
np = node_parser(self, spec_class, node, lane=self.lane)
task_spec = np.parse_node()
return task_spec

Expand All @@ -82,12 +81,12 @@ def _parse(self):
# Check for an IO Specification.
io_spec = first(self.xpath('./bpmn:ioSpecification'))
if io_spec is not None:
data_parser = DataSpecificationParser(io_spec, self.filename, self.doc_xpath)
data_parser = DataSpecificationParser(io_spec, filename=self.filename)
self.spec.data_inputs, self.spec.data_outputs = data_parser.parse_io_spec()

# Get the data objects
for obj in self.xpath('./bpmn:dataObject'):
data_parser = DataSpecificationParser(obj, self.filename, self.doc_xpath)
data_parser = DataSpecificationParser(obj, filename=self.filename)
data_object = data_parser.parse_data_object()
self.spec.data_objects[data_object.name] = data_object

Expand Down
4 changes: 2 additions & 2 deletions SpiffWorkflow/bpmn/parser/TaskParser.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class TaskParser(NodeParser):
outgoing transitions, once the child tasks have all been parsed.
"""

def __init__(self, process_parser, spec_class, node, lane=None):
def __init__(self, process_parser, spec_class, node, nsmap=None, lane=None):
"""
Constructor.

Expand All @@ -56,7 +56,7 @@ def __init__(self, process_parser, spec_class, node, lane=None):
extending the TaskParser.
:param node: the XML node for this task
"""
super().__init__(node, process_parser.filename, process_parser.doc_xpath, lane)
super().__init__(node, nsmap, filename=process_parser.filename, lane=lane)
self.process_parser = process_parser
self.spec_class = spec_class
self.spec = self.process_parser.spec
Expand Down
38 changes: 28 additions & 10 deletions SpiffWorkflow/bpmn/parser/node_parser.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,41 @@
from SpiffWorkflow.bpmn.parser.ValidationException import ValidationException
from .util import xpath_eval, first
from .util import first

DEFAULT_NSMAP = {
'bpmn': 'http://www.omg.org/spec/BPMN/20100524/MODEL',
'bpmndi': 'http://www.omg.org/spec/BPMN/20100524/DI',
'dc': 'http://www.omg.org/spec/DD/20100524/DC',

}

CAMUNDA_MODEL_NS = 'http://camunda.org/schema/1.0/bpmn'

class NodeParser:

def __init__(self, node, filename, doc_xpath, lane=None):
def __init__(self, node, nsmap=None, filename=None, lane=None):

self.node = node
self.nsmap = nsmap or DEFAULT_NSMAP
self.filename = filename
self.doc_xpath = doc_xpath
self.xpath = xpath_eval(node)
self.lane = self._get_lane() or lane
self.position = self._get_position() or {'x': 0.0, 'y': 0.0}

def get_id(self):
return self.node.get('id')

def xpath(self, xpath, extra_ns=None):
return self._xpath(self.node, xpath, extra_ns)

def doc_xpath(self, xpath, extra_ns=None):
root = self.node.getroottree().getroot()
return self._xpath(root, xpath, extra_ns)

def parse_condition(self, sequence_flow):
xpath = xpath_eval(sequence_flow)
expression = first(xpath('.//bpmn:conditionExpression'))
expression = first(self._xpath(sequence_flow, './/bpmn:conditionExpression'))
return expression.text if expression is not None else None

def parse_documentation(self, sequence_flow=None):
xpath = xpath_eval(sequence_flow) if sequence_flow is not None else self.xpath
documentation_node = first(xpath('.//bpmn:documentation'))
documentation_node = first(self._xpath(sequence_flow or self.node, './/bpmn:documentation'))
return None if documentation_node is None else documentation_node.text

def parse_incoming_data_references(self):
Expand All @@ -50,8 +61,7 @@ def parse_outgoing_data_references(self):
def parse_extensions(self, node=None):
extensions = {}
extra_ns = {'camunda': CAMUNDA_MODEL_NS}
xpath = xpath_eval(self.node, extra_ns) if node is None else xpath_eval(node, extra_ns)
extension_nodes = xpath( './/bpmn:extensionElements/camunda:properties/camunda:property')
extension_nodes = self.xpath('.//bpmn:extensionElements/camunda:properties/camunda:property', extra_ns)
for node in extension_nodes:
extensions[node.get('name')] = node.get('value')
return extensions
Expand All @@ -65,3 +75,11 @@ def _get_position(self):
bounds = first(self.doc_xpath(f".//bpmndi:BPMNShape[@bpmnElement='{self.get_id()}']//dc:Bounds"))
if bounds is not None:
return {'x': float(bounds.get('x', 0)), 'y': float(bounds.get('y', 0))}

def _xpath(self, node, xpath, extra_ns=None):
if extra_ns is not None:
nsmap = self.nsmap.copy()
nsmap.update(extra_ns)
else:
nsmap = self.nsmap
return node.xpath(xpath, namespaces=nsmap)
33 changes: 33 additions & 0 deletions SpiffWorkflow/bpmn/parser/schema/BPMN20.xsd
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema elementFormDefault="qualified" attributeFormDefault="unqualified" xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" targetNamespace="http://www.omg.org/spec/BPMN/20100524/MODEL">

<xsd:import namespace="http://www.omg.org/spec/BPMN/20100524/DI" schemaLocation="BPMNDI.xsd"/>
<xsd:include schemaLocation="Semantic.xsd"/>

<xsd:element name="definitions" type="tDefinitions"/>
<xsd:complexType name="tDefinitions">
<xsd:sequence>
<xsd:element ref="import" minOccurs="0" maxOccurs="unbounded"/>
<xsd:element ref="extension" minOccurs="0" maxOccurs="unbounded"/>
<xsd:element ref="rootElement" minOccurs="0" maxOccurs="unbounded"/>
<xsd:element ref="bpmndi:BPMNDiagram" minOccurs="0" maxOccurs="unbounded"/>
<xsd:element ref="relationship" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
<xsd:attribute name="id" type="xsd:ID" use="optional"/>
<xsd:attribute name="name" type="xsd:string"/>
<xsd:attribute name="targetNamespace" type="xsd:anyURI" use="required"/>
<xsd:attribute name="expressionLanguage" type="xsd:anyURI" use="optional" default="http://www.w3.org/1999/XPath"/>
<xsd:attribute name="typeLanguage" type="xsd:anyURI" use="optional" default="http://www.w3.org/2001/XMLSchema"/>
<xsd:attribute name="exporter" type="xsd:string"/>
<xsd:attribute name="exporterVersion" type="xsd:string"/>
<xsd:anyAttribute namespace="##other" processContents="lax"/>
</xsd:complexType>

<xsd:element name="import" type="tImport"/>
<xsd:complexType name="tImport">
<xsd:attribute name="namespace" type="xsd:anyURI" use="required"/>
<xsd:attribute name="location" type="xsd:string" use="required"/>
<xsd:attribute name="importType" type="xsd:anyURI" use="required"/>
</xsd:complexType>

</xsd:schema>
Loading