Skip to content

Commit

Permalink
Merge pull request #187 from sartography/enhancement/improve-subproce…
Browse files Browse the repository at this point in the history
…ss-handling

Enhancement/improve subprocess handling
  • Loading branch information
danfunk authored Jun 24, 2022
2 parents 4080633 + 23f3238 commit 0cf6194
Show file tree
Hide file tree
Showing 117 changed files with 2,397 additions and 1,805 deletions.
75 changes: 36 additions & 39 deletions SpiffWorkflow/bpmn/parser/BpmnParser.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from builtins import object

# Copyright (C) 2012 Matthew Hampton
#
# This library is free software; you can redistribute it and/or
Expand All @@ -21,10 +21,10 @@

from lxml import etree

from ..workflow import BpmnWorkflow
from SpiffWorkflow.bpmn.specs.BpmnProcessSpec import BpmnProcessSpec
from .ValidationException import ValidationException
from ..specs.events import StartEvent, EndEvent, BoundaryEvent, IntermediateCatchEvent, IntermediateThrowEvent
from ..specs.SubWorkflowTask import CallActivity, TransactionSubprocess
from ..specs.SubWorkflowTask import CallActivity, SubWorkflowTask, TransactionSubprocess
from ..specs.ExclusiveGateway import ExclusiveGateway
from ..specs.InclusiveGateway import InclusiveGateway
from ..specs.ManualTask import ManualTask
Expand All @@ -33,15 +33,14 @@
from ..specs.ScriptTask import ScriptTask
from ..specs.UserTask import UserTask
from .ProcessParser import ProcessParser
from .util import full_tag, xpath_eval, first
from .util import full_tag, xpath_eval
from .task_parsers import (UserTaskParser, NoneTaskParser, ManualTaskParser,
ExclusiveGatewayParser, ParallelGatewayParser, InclusiveGatewayParser,
CallActivityParser, TransactionSubprocessParser,
ScriptTaskParser, SubWorkflowParser)
from .event_parsers import (StartEventParser, EndEventParser, BoundaryEventParser,
IntermediateCatchEventParser, IntermediateThrowEventParser)

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

class BpmnParser(object):
"""
Expand All @@ -51,8 +50,7 @@ class BpmnParser(object):
Extension points: OVERRIDE_PARSER_CLASSES provides a map from full BPMN tag
name to a TaskParser and Task class. PROCESS_PARSER_CLASS provides a
subclass of ProcessParser WORKFLOW_CLASS provides a subclass of
BpmnWorkflow
subclass of ProcessParser
"""

PARSER_CLASSES = {
Expand All @@ -62,11 +60,9 @@ class BpmnParser(object):
full_tag('task'): (NoneTaskParser, NoneTask),
full_tag('subProcess'): (SubWorkflowParser, CallActivity),
full_tag('manualTask'): (ManualTaskParser, ManualTask),
full_tag('exclusiveGateway'): (ExclusiveGatewayParser,
ExclusiveGateway),
full_tag('exclusiveGateway'): (ExclusiveGatewayParser, ExclusiveGateway),
full_tag('parallelGateway'): (ParallelGatewayParser, ParallelGateway),
full_tag('inclusiveGateway'): (InclusiveGatewayParser,
InclusiveGateway),
full_tag('inclusiveGateway'): (InclusiveGatewayParser, InclusiveGateway),
full_tag('callActivity'): (CallActivityParser, CallActivity),
full_tag('transaction'): (TransactionSubprocessParser, TransactionSubprocess),
full_tag('scriptTask'): (ScriptTaskParser, ScriptTask),
Expand All @@ -80,7 +76,6 @@ class BpmnParser(object):
OVERRIDE_PARSER_CLASSES = {}

PROCESS_PARSER_CLASS = ProcessParser
WORKFLOW_CLASS = BpmnWorkflow

def __init__(self):
"""
Expand Down Expand Up @@ -108,7 +103,7 @@ def get_process_parser(self, process_id_or_name):

def get_process_ids(self):
"""Returns a list of process IDs"""
return self.process_parsers.keys()
return list(self.process_parsers.keys())

def add_bpmn_file(self, filename):
"""
Expand All @@ -134,7 +129,7 @@ def add_bpmn_files(self, filenames):
finally:
f.close()

def add_bpmn_xml(self, bpmn, svg=None, filename=None):
def add_bpmn_xml(self, bpmn, filename=None):
"""
Add the given lxml representation of the BPMN file to the parser's set.
Expand All @@ -160,38 +155,17 @@ def add_bpmn_xml(self, bpmn, svg=None, filename=None):

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

def create_parser(self, node, doc_xpath, svg=None, filename=None, current_lane=None):
parser = self.PROCESS_PARSER_CLASS(self, node, svg, filename=filename, doc_xpath=doc_xpath,
current_lane=current_lane)
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)
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:
raise ValidationException('Duplicate process name', node=node, filename=filename)
self.process_parsers[parser.get_id()] = parser
self.process_parsers_by_name[parser.get_name()] = parser

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

def parse_extensions(self, node, xpath=None):
extensions = {}
xpath = xpath or xpath_eval(node)
extension_nodes = xpath(
'.//bpmn:extensionElements/{%s}properties/{%s}property'%(
CAMUNDA_MODEL_NS,CAMUNDA_MODEL_NS))
for node in extension_nodes:
extensions[node.get('name')] = node.get('value')
return extensions

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

def get_spec(self, process_id_or_name):
"""
Parses the required subset of the BPMN files, in order to provide an
Expand All @@ -200,8 +174,31 @@ def get_spec(self, process_id_or_name):
"""
parser = self.get_process_parser(process_id_or_name)
if parser is None:
raise Exception(
raise ValidationException(
f"The process '{process_id_or_name}' was not found. "
f"Did you mean one of the following: "
f"{', '.join(self.get_process_ids())}?")
return parser.get_spec()

def get_process_specs(self):
# This is a little convoluted, but we might add more processes as we generate
# the dictionary if something refers to another subprocess that we haven't seen.
processes = dict((id, self.get_spec(id)) for id in self.get_process_ids())
while processes.keys() != self.process_parsers.keys():
for process_id in self.process_parsers.keys():
processes[process_id] = self.get_spec(process_id)
return processes

def get_top_level_spec(self, name, entry_points, parallel=True):
spec = BpmnProcessSpec(name)
current = spec.start
for process in entry_points:
task = SubWorkflowTask(spec, process, process)
current.connect(task)
if parallel:
task.connect(spec.end)
else:
current = task
if not parallel:
current.connect(spec.end)
return spec
100 changes: 16 additions & 84 deletions SpiffWorkflow/bpmn/parser/ProcessParser.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from builtins import object

# Copyright (C) 2012 Matthew Hampton
#
# This library is free software; you can redistribute it and/or
Expand All @@ -19,49 +19,30 @@

from .ValidationException import ValidationException
from ..specs.BpmnProcessSpec import BpmnProcessSpec
from .util import xpath_eval, DIAG_COMMON_NS
from .node_parser import NodeParser


class ProcessParser(object):
class ProcessParser(NodeParser):
"""
Parses a single BPMN process, including all of the tasks within that
process.
"""

def __init__(self, p, node, svg=None, filename=None, doc_xpath=None,
current_lane=None):
def __init__(self, p, node, filename=None, doc_xpath=None, lane=None):
"""
Constructor.
:param p: the owning BpmnParser instance
:param node: the XML node for the process
:param svg: the SVG representation of this process as a string
(optional)
:param filename: the source BPMN filename (optional)
: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)
self.parser = p
self.node = node
self.doc_xpath = doc_xpath
self.xpath = xpath_eval(node)
self.spec = BpmnProcessSpec(
name=self.get_id(), description=self.get_name(), svg=svg,
filename=filename)
self.parsing_started = False
self.is_parsed = False
self.parsed_nodes = {}
self.svg = svg
self.filename = filename
self.id_to_lane_lookup = None
self._init_lane_lookup()
self.id_to_coords_lookup = None # Dictionary of positional arguments for each node.
self._init_coord_lookup()
self.current_lane = current_lane

def get_id(self):
"""
Returns the process ID
"""
return self.node.get('id')
self.lane = lane
self.spec = None

def get_name(self):
"""
Expand All @@ -81,77 +62,28 @@ def parse_node(self, node):

(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.",
raise ValidationException("There is no support implemented for this task type.",
node=node, filename=self.filename)
np = node_parser(self, spec_class, node)
np = node_parser(self, spec_class, node, self.lane)
task_spec = np.parse_node()

return task_spec

def get_lane(self, id):
"""
Return the name of the lane that contains the specified task
As we may be in a sub_process, adopt the current_lane if we
have no lane ourselves. In the future we might consider supporting
being in two lanes at once.
"""
lane = self.id_to_lane_lookup.get(id, None)
if lane:
return lane
else:
return self.current_lane

def _init_lane_lookup(self):
self.id_to_lane_lookup = {}
for lane in self.xpath('.//bpmn:lane'):
name = lane.get('name')
if name:
for ref in xpath_eval(lane)('bpmn:flowNodeRef'):
id = ref.text
if id:
self.id_to_lane_lookup[id] = name


def get_coord(self, id):
"""
Return the x,y coordinates of the given task, if available.
"""
return self.id_to_coords_lookup.get(id, {'x':0, 'y':0})

def _init_coord_lookup(self):
"""Creates a lookup table with the x/y coordinates of each shape.
Only tested with the output from the Camunda modeler, which provides
these details in the bpmndi / and dc namespaces."""
self.id_to_coords_lookup = {}
for position in self.doc_xpath('.//bpmndi:BPMNShape'):
bounds = xpath_eval(position)("dc:Bounds")
if len(bounds) > 0 and 'bpmnElement' in position.attrib:
bound = bounds[0]
self.id_to_coords_lookup[position.attrib['bpmnElement']] = \
{'x': float(bound.attrib['x']), 'y': float(bound.attrib['y'])}

def _parse(self):
# here we only look in the top level, We will have another
# bpmn:startEvent if we have a subworkflow task
start_node_list = self.xpath('./bpmn:startEvent')
if not start_node_list:
raise ValidationException(
"No start event found", node=self.node, filename=self.filename)
self.parsing_started = True
raise ValidationException("No start event found", node=self.node, filename=self.filename)
self.spec = BpmnProcessSpec(name=self.get_id(), description=self.get_name(), filename=self.filename)

for node in start_node_list:
self.parse_node(node)
self.is_parsed = True

def get_spec(self):
"""
Parse this process (if it has not already been parsed), and return the
workflow spec.
"""
if self.is_parsed:
return self.spec
if self.parsing_started:
raise NotImplementedError(
'Recursive call Activities are not supported.')
self._parse()
if self.spec is None:
self._parse()
return self.spec
Loading

0 comments on commit 0cf6194

Please sign in to comment.