Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/inclusive gateway support #286

Merged
merged 9 commits into from Jan 24, 2023
38 changes: 24 additions & 14 deletions SpiffWorkflow/bpmn/parser/BpmnParser.py
Expand Up @@ -43,13 +43,23 @@
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,
CallActivityParser, ScriptTaskParser, SubWorkflowParser,
ServiceTaskParser)
from .event_parsers import (EventBasedGatewayParser, StartEventParser, EndEventParser, BoundaryEventParser,
IntermediateCatchEventParser, IntermediateThrowEventParser,
SendTaskParser, ReceiveTaskParser)
from .TaskParser import TaskParser
from .task_parsers import (
GatewayParser,
ConditionalGatewayParser,
CallActivityParser,
ScriptTaskParser,
SubWorkflowParser,
)
from .event_parsers import (
EventBasedGatewayParser,
StartEventParser, EndEventParser,
BoundaryEventParser,
IntermediateCatchEventParser,
IntermediateThrowEventParser,
SendTaskParser,
ReceiveTaskParser
)


XSD_PATH = os.path.join(os.path.dirname(__file__), 'schema', 'BPMN20.xsd')
Expand Down Expand Up @@ -94,17 +104,17 @@ class BpmnParser(object):
PARSER_CLASSES = {
full_tag('startEvent'): (StartEventParser, StartEvent),
full_tag('endEvent'): (EndEventParser, EndEvent),
full_tag('userTask'): (UserTaskParser, UserTask),
full_tag('task'): (NoneTaskParser, NoneTask),
full_tag('userTask'): (TaskParser, UserTask),
full_tag('task'): (TaskParser, NoneTask),
full_tag('subProcess'): (SubWorkflowParser, CallActivity),
full_tag('manualTask'): (ManualTaskParser, ManualTask),
full_tag('exclusiveGateway'): (ExclusiveGatewayParser, ExclusiveGateway),
full_tag('parallelGateway'): (ParallelGatewayParser, ParallelGateway),
full_tag('inclusiveGateway'): (InclusiveGatewayParser, InclusiveGateway),
full_tag('manualTask'): (TaskParser, ManualTask),
full_tag('exclusiveGateway'): (ConditionalGatewayParser, ExclusiveGateway),
full_tag('parallelGateway'): (GatewayParser, ParallelGateway),
full_tag('inclusiveGateway'): (ConditionalGatewayParser, InclusiveGateway),
full_tag('callActivity'): (CallActivityParser, CallActivity),
full_tag('transaction'): (SubWorkflowParser, TransactionSubprocess),
full_tag('scriptTask'): (ScriptTaskParser, ScriptTask),
full_tag('serviceTask'): (ServiceTaskParser, ServiceTask),
full_tag('serviceTask'): (TaskParser, ServiceTask),
full_tag('intermediateCatchEvent'): (IntermediateCatchEventParser, IntermediateCatchEvent),
full_tag('intermediateThrowEvent'): (IntermediateThrowEventParser, IntermediateThrowEvent),
full_tag('boundaryEvent'): (BoundaryEventParser, BoundaryEvent),
Expand Down
7 changes: 4 additions & 3 deletions SpiffWorkflow/bpmn/parser/TaskParser.py
Expand Up @@ -28,6 +28,7 @@
from ..specs.MultiInstanceTask import getDynamicMIClass
from ..specs.SubWorkflowTask import CallActivity, TransactionSubprocess, SubWorkflowTask
from ..specs.ExclusiveGateway import ExclusiveGateway
from ..specs.InclusiveGateway import InclusiveGateway
from ...dmn.specs.BusinessRuleTask import BusinessRuleTask
from ...operators import Attrib, PathAttrib
from .util import one, first
Expand Down Expand Up @@ -188,9 +189,9 @@ def parse_node(self):
children = sorted(children, key=lambda tup: float(tup[0]["y"]))

default_outgoing = self.node.get('default')
if not default_outgoing:
if len(children) == 1 or not isinstance(self.task, ExclusiveGateway):
(position, c, target_node, sequence_flow) = children[0]
if len(children) == 1 and isinstance(self.task, (ExclusiveGateway, InclusiveGateway)):
(position, c, target_node, sequence_flow) = children[0]
if self.parse_condition(sequence_flow) is None:
default_outgoing = sequence_flow.get('id')

for (position, c, target_node, sequence_flow) in children:
Expand Down
67 changes: 5 additions & 62 deletions SpiffWorkflow/bpmn/parser/task_parsers.py
Expand Up @@ -25,41 +25,19 @@
CAMUNDA_MODEL_NS = 'http://camunda.org/schema/1.0/bpmn'


class UserTaskParser(TaskParser):

"""
Base class for parsing User Tasks
"""
pass


class ManualTaskParser(UserTaskParser):

"""
Base class for parsing Manual Tasks. Currently assumes that Manual Tasks
should be treated the same way as User Tasks.
"""
pass


class NoneTaskParser(UserTaskParser):

"""
Base class for parsing unspecified Tasks. Currently assumes that such Tasks
should be treated the same way as User Tasks.
"""
pass
class GatewayParser(TaskParser):
def handles_multiple_outgoing(self):
return True


class ExclusiveGatewayParser(TaskParser):
class ConditionalGatewayParser(GatewayParser):
"""
Parses an Exclusive Gateway, setting up the outgoing conditions
appropriately.
"""

def connect_outgoing(self, outgoing_task, sequence_flow_node, is_default):
if is_default:
super(ExclusiveGatewayParser, self).connect_outgoing(outgoing_task, sequence_flow_node, is_default)
super().connect_outgoing(outgoing_task, sequence_flow_node, is_default)
else:
cond = self.parse_condition(sequence_flow_node)
if cond is None:
Expand All @@ -69,33 +47,6 @@ def connect_outgoing(self, outgoing_task, sequence_flow_node, is_default):
self.filename)
self.task.connect_outgoing_if(cond, outgoing_task)

def handles_multiple_outgoing(self):
return True


class ParallelGatewayParser(TaskParser):

"""
Parses a Parallel Gateway.
"""

def handles_multiple_outgoing(self):
return True


class InclusiveGatewayParser(TaskParser):

"""
Parses an Inclusive Gateway.
"""

def handles_multiple_outgoing(self):
"""
At the moment I haven't implemented support for diverging inclusive
gateways
"""
return False


class SubprocessParser:

Expand Down Expand Up @@ -200,11 +151,3 @@ def get_script(self):
f"Invalid Script Task. No Script Provided. " + str(ae),
node=self.node, file_name=self.filename)


class ServiceTaskParser(TaskParser):

"""
Parses a ServiceTask node.
"""
pass

33 changes: 18 additions & 15 deletions SpiffWorkflow/bpmn/serializer/task_spec_converters.py
Expand Up @@ -176,51 +176,54 @@ def from_dict(self, dct):
return self.task_spec_from_dict(dct)


class ExclusiveGatewayConverter(BpmnTaskSpecConverter):

def __init__(self, data_converter=None, typename=None):
super().__init__(ExclusiveGateway, data_converter, typename)
class ConditionalGatewayConverter(BpmnTaskSpecConverter):

def to_dict(self, spec):
dct = self.get_default_attributes(spec)
dct.update(self.get_bpmn_attributes(spec))
dct['default_task_spec'] = spec.default_task_spec
dct['cond_task_specs'] = [ self.bpmn_condition_to_dict(cond) for cond in spec.cond_task_specs ]
dct['choice'] = spec.choice
return dct

def from_dict(self, dct):
conditions = dct.pop('cond_task_specs')
default_task_spec = dct.pop('default_task_spec')
spec = self.task_spec_from_dict(dct)
spec.cond_task_specs = [ self.bpmn_condition_from_dict(cond) for cond in conditions ]
spec.default_task_spec = default_task_spec
return spec

def bpmn_condition_from_dict(self, dct):
return (_BpmnCondition(dct['condition']), dct['task_spec'])
return (_BpmnCondition(dct['condition']) if dct['condition'] is not None else None, dct['task_spec'])

def bpmn_condition_to_dict(self, condition):

expr, task_spec = condition
return {
'condition': expr.args[0],
'condition': expr.args[0] if expr is not None else None,
'task_spec': task_spec
}

class InclusiveGatewayConverter(BpmnTaskSpecConverter):

class ExclusiveGatewayConverter(ConditionalGatewayConverter):

def __init__(self, data_converter=None, typename=None):
super().__init__(InclusiveGateway, data_converter, typename)
super().__init__(ExclusiveGateway, data_converter, typename)

def to_dict(self, spec):
dct = self.get_default_attributes(spec)
dct.update(self.get_bpmn_attributes(spec))
dct.update(self.get_join_attributes(spec))
dct = super().to_dict(spec)
dct['default_task_spec'] = spec.default_task_spec
return dct

def from_dict(self, dct):
return self.task_spec_from_dict(dct)
default_task_spec = dct.pop('default_task_spec')
spec = super().from_dict(dct)
spec.default_task_spec = default_task_spec
return spec


class InclusiveGatewayConverter(ConditionalGatewayConverter):

def __init__(self, data_converter=None, typename=None):
super().__init__(InclusiveGateway, data_converter, typename)


class ParallelGatewayConverter(BpmnTaskSpecConverter):
Expand Down
6 changes: 6 additions & 0 deletions SpiffWorkflow/bpmn/serializer/version_migration.py
Expand Up @@ -17,6 +17,8 @@ def version_1_1_to_1_2(old):

Cycle timers no longer connect back to themselves. New children are created from a single
tasks rather than reusing previously executed tasks.

All conditions (including the default) are included in the conditions for gateways.
"""
new = deepcopy(old)

Expand Down Expand Up @@ -82,6 +84,10 @@ def td_to_iso(td):
task['children'].remove(remove['id'])
new['tasks'].pop(remove['id'])

for spec in [ts for ts in new['spec']['task_specs'].values() if ts['typename'] == 'ExclusiveGateway']:
if (None, spec['default_task_spec']) not in spec['cond_task_specs']:
spec['cond_task_specs'].append((None, spec['default_task_spec']))

new['VERSION'] = "1.2"
return new

Expand Down
6 changes: 4 additions & 2 deletions SpiffWorkflow/bpmn/specs/BpmnSpecMixin.py
Expand Up @@ -32,7 +32,6 @@ def _matches(self, task):
return task.workflow.script_engine.evaluate(task, self.args[0])



class BpmnSpecMixin(TaskSpec):
"""
All BPMN spec classes should mix this superclass in. It adds a number of
Expand Down Expand Up @@ -70,7 +69,10 @@ def connect_outgoing_if(self, condition, taskspec):
evaluates to true. This should only be called if the task has a
connect_if method (e.g. ExclusiveGateway).
"""
self.connect_if(_BpmnCondition(condition), taskspec)
if condition is None:
self.connect(taskspec)
else:
self.connect_if(_BpmnCondition(condition), taskspec)

def _on_ready_hook(self, my_task):
super()._on_ready_hook(my_task)
Expand Down
24 changes: 3 additions & 21 deletions SpiffWorkflow/bpmn/specs/ExclusiveGateway.py
Expand Up @@ -16,38 +16,20 @@
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
from ...exceptions import WorkflowException

from .BpmnSpecMixin import BpmnSpecMixin
from ...specs.base import TaskSpec
from ...specs.ExclusiveChoice import ExclusiveChoice
from ...specs.MultiChoice import MultiChoice


class ExclusiveGateway(ExclusiveChoice, BpmnSpecMixin):

"""
Task Spec for a bpmn:exclusiveGateway node.
"""

def test(self):
"""
Checks whether all required attributes are set. Throws an exception
if an error was detected.
"""
# This has been overridden to allow a single default flow out (without a
# condition) - useful for the converging type
TaskSpec.test(self)
# if len(self.cond_task_specs) < 1:
# raise WorkflowException(self, 'At least one output required.')
for condition, name in self.cond_task_specs:
if name is None:
raise WorkflowException('Condition with no task spec.', task_spec=self)
task_spec = self._wf_spec.get_task_spec_from_name(name)
if task_spec is None:
msg = 'Condition leads to non-existent task ' + repr(name)
raise WorkflowException(msg, task_spec=self)
if condition is None:
continue
# Bypass the check for no default output -- this is not required in BPMN
MultiChoice.test(self)

@property
def spec_type(self):
Expand Down