Skip to content

Commit 73ee57e

Browse files
authored
Merge pull request #479 from sartography/feature/ad-hoc-subprocess
ad hoc subprocess support
2 parents 8c6e00f + 99956d0 commit 73ee57e

15 files changed

Lines changed: 542 additions & 20 deletions

File tree

SpiffWorkflow/bpmn/parser/BpmnParser.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
CallActivity,
3434
SubWorkflowTask,
3535
TransactionSubprocess,
36+
AdHocSubprocess,
3637
InclusiveGateway,
3738
ExclusiveGateway,
3839
ParallelGateway,
@@ -49,7 +50,7 @@
4950
from SpiffWorkflow.bpmn.specs.mixins.subworkflow_task import SubWorkflowTask as SubWorkflowTaskMixin
5051

5152
from .ValidationException import ValidationException
52-
from .ProcessParser import ProcessParser
53+
from .ProcessParser import ProcessParser, AdHocParser
5354
from .node_parser import DEFAULT_NSMAP
5455
from .spec_description import SPEC_DESCRIPTIONS
5556
from .util import full_tag, xpath_eval, first
@@ -60,6 +61,7 @@
6061
CallActivityParser,
6162
ScriptTaskParser,
6263
SubWorkflowParser,
64+
AdHocSubprocessParser,
6365
)
6466
from .event_parsers import (
6567
EventBasedGatewayParser,
@@ -132,6 +134,7 @@ class BpmnParser:
132134
full_tag('inclusiveGateway'): (ConditionalGatewayParser, InclusiveGateway),
133135
full_tag('callActivity'): (CallActivityParser, CallActivity),
134136
full_tag('transaction'): (SubWorkflowParser, TransactionSubprocess),
137+
full_tag('adHocSubProcess'): (AdHocSubprocessParser, AdHocSubprocess),
135138
full_tag('scriptTask'): (ScriptTaskParser, ScriptTask),
136139
full_tag('serviceTask'): (TaskParser, ServiceTask),
137140
full_tag('intermediateCatchEvent'): (IntermediateCatchEventParser, IntermediateCatchEvent),
@@ -145,6 +148,7 @@ class BpmnParser:
145148
OVERRIDE_PARSER_CLASSES = {}
146149

147150
PROCESS_PARSER_CLASS = ProcessParser
151+
AD_HOC_PARSER_CLASS = AdHocParser
148152

149153
DATA_STORE_CLASSES = {}
150154

@@ -402,6 +406,12 @@ def create_parser(self, node, filename=None, lane=None):
402406
raise ValidationException(f'Duplicate process ID: {parser.bpmn_id}', node=node, file_name=filename)
403407
self.process_parsers[parser.bpmn_id] = parser
404408

409+
def create_ad_hoc_parser(self, node, filename=None, lane=None):
410+
parser = self.AD_HOC_PARSER_CLASS(self, node, self.namespaces, self.data_stores, filename=filename, lane=lane)
411+
if parser.bpmn_id in self.process_parsers:
412+
raise ValidationException(f'Duplicate process ID: {parser.bpmn_id}', node=node, file_name=filename)
413+
self.process_parsers[parser.bpmn_id] = parser
414+
405415
def get_process_dependencies(self):
406416
return self.process_dependencies
407417

SpiffWorkflow/bpmn/parser/ProcessParser.py

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@
1818
# 02110-1301 USA
1919

2020
from .ValidationException import ValidationException
21-
from ..specs.bpmn_process_spec import BpmnProcessSpec
21+
from ..specs.bpmn_process_spec import BpmnProcessSpec, AdHocSubprocessSpec
2222
from ..specs.data_spec import DataObject
2323
from ..specs.control import StartEventJoin, StartEventSplit
2424
from .node_parser import NodeParser
25-
from .util import first
25+
from .util import first, full_tag
2626

2727

2828
class ProcessParser(NodeParser):
@@ -156,14 +156,8 @@ def parse_node(self, node):
156156
return task_spec
157157

158158
def _parse(self):
159-
# here we only look in the top level, We will have another
160-
# bpmn:startEvent if we have a subworkflow task
161-
start_node_list = self.xpath('./bpmn:startEvent')
162-
if not start_node_list and self.process_executable:
163-
raise ValidationException("No start event found", node=self.node, file_name=self.filename)
164-
if not self.process_executable:
165-
raise ValidationException(f"Process {self.bpmn_id} is not executable.", node=self.node, file_name=self.filename)
166-
self.spec = BpmnProcessSpec(name=self.bpmn_id, description=self.get_name(), filename=self.filename)
159+
160+
self.spec = self.create_spec()
167161

168162
# Get the data objects
169163
for obj in self.xpath('./bpmn:dataObject'):
@@ -175,9 +169,20 @@ def _parse(self):
175169
if io_spec is not None:
176170
self.spec.io_specification = self.parse_io_spec()
177171

178-
# set the data stores on the process spec so they can survive
179-
# serialization
180172
self.spec.data_stores = self.data_stores
173+
self.add_tasks()
174+
175+
def create_spec(self):
176+
if not self.process_executable:
177+
raise ValidationException(f"Process {self.bpmn_id} is not executable.", node=self.node, file_name=self.filename)
178+
return BpmnProcessSpec(name=self.bpmn_id, description=self.get_name(), filename=self.filename)
179+
180+
def add_tasks(self):
181+
182+
start_node_list = self.xpath('./bpmn:startEvent')
183+
if not start_node_list and self.process_executable:
184+
raise ValidationException("No start event found", node=self.node, file_name=self.filename)
185+
181186
for node in start_node_list:
182187
self.parse_node(node)
183188

@@ -207,3 +212,44 @@ def get_spec(self):
207212
if self.spec is None:
208213
self._parse()
209214
return self.spec
215+
216+
class AdHocParser(ProcessParser):
217+
218+
def _parse(self):
219+
super()._parse()
220+
self.spec.create_paths()
221+
222+
def create_spec(self):
223+
if self.attribute('ordering') == 'sequential':
224+
raise ValidationException(
225+
'Sequential ordering for ad hoc subprocesses not supported',
226+
node=self.node,
227+
file_name=self.filename
228+
)
229+
cancel_remaining = self.attribute('cancelRemainingInstances') in [None, 'true']
230+
condition = self.xpath('./bpmn:completionCondition')
231+
condition = condition[0].text if len(condition) > 0 else None
232+
return AdHocSubprocessSpec(
233+
completion_condition=condition,
234+
cancel_remaining=cancel_remaining,
235+
name=self.bpmn_id,
236+
description=self.get_name(),
237+
filename=self.filename,
238+
)
239+
240+
def add_tasks(self):
241+
start_node_list = []
242+
for node in self.node.getchildren():
243+
if node.tag not in self.parser.PARSER_CLASSES:
244+
continue
245+
elif node.tag in [full_tag('startEvent'), full_tag('endEvent')]:
246+
raise ValidationException(
247+
'Ad hoc subprocesses may not contain start or end events',
248+
node=self.node,
249+
file_name=self.filename,
250+
)
251+
elif len(node.xpath('./bpmn:incoming', namespaces=self.nsmap)) == 0:
252+
start_node_list.append(node)
253+
254+
for node in start_node_list:
255+
self.parse_node(node)

SpiffWorkflow/bpmn/parser/task_parsers.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,19 @@ def get_call_activity_spec(task_parser):
8989
file_name=task_parser.filename)
9090
return called_element
9191

92+
@staticmethod
93+
def get_ad_hoc_spec(task_parser):
94+
task_parser.process_parser.parser.create_ad_hoc_parser(
95+
task_parser.node,
96+
filename=task_parser.filename,
97+
lane=task_parser.lane
98+
)
99+
spec_id = task_parser.node.get('id')
100+
# This parser makes me want to cry
101+
spec_parser = task_parser.process_parser.parser.process_parsers[spec_id]
102+
spec_parser.parent = task_parser.process_parser
103+
return spec_id
104+
92105

93106
class SubWorkflowParser(TaskParser):
94107

@@ -109,6 +122,13 @@ def create_task(self):
109122
return self.spec_class(self.spec, self.bpmn_id, subworkflow_spec=subworkflow_spec, **self.bpmn_attributes)
110123

111124

125+
class AdHocSubprocessParser(TaskParser):
126+
127+
def create_task(self):
128+
subworkflow_spec = SubprocessParser.get_ad_hoc_spec(self)
129+
return self.spec_class(self.spec, self.bpmn_id, subworkflow_spec=subworkflow_spec, **self.bpmn_attributes)
130+
131+
112132
class ScriptTaskParser(TaskParser):
113133
"""Parses a script task"""
114134

SpiffWorkflow/bpmn/serializer/config.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from SpiffWorkflow.bpmn import BpmnWorkflow, BpmnEvent
2222
from SpiffWorkflow.bpmn.util.subworkflow import BpmnSubWorkflow
2323

24-
from SpiffWorkflow.bpmn.specs import BpmnProcessSpec
24+
from SpiffWorkflow.bpmn.specs import BpmnProcessSpec, AdHocSubprocessSpec
2525
from SpiffWorkflow.bpmn.specs.defaults import (
2626
ManualTask,
2727
NoneTask,
@@ -38,6 +38,7 @@
3838
CallActivity,
3939
TransactionSubprocess,
4040
EventSubprocess,
41+
AdHocSubprocess,
4142
StartEvent,
4243
EndEvent,
4344
IntermediateCatchEvent,
@@ -83,7 +84,7 @@
8384
BpmnEventConverter,
8485
)
8586
from .helpers import BpmnDataSpecificationConverter, EventDefinitionConverter
86-
from .default import BpmnProcessSpecConverter
87+
from .default import BpmnProcessSpecConverter, AdHocSubprocessSpecConverter
8788
from .default.task_spec import (
8889
BpmnTaskSpecConverter,
8990
ScriptTaskConverter,
@@ -116,6 +117,7 @@
116117
TaskDataReference: BpmnDataSpecificationConverter,
117118
BpmnIoSpecification: IOSpecificationConverter,
118119
BpmnProcessSpec: BpmnProcessSpecConverter,
120+
AdHocSubprocessSpec: AdHocSubprocessSpecConverter,
119121
SimpleBpmnTask: BpmnTaskSpecConverter,
120122
BpmnStartTask: BpmnStartTaskConverter,
121123
_EndJoin: BpmnTaskSpecConverter,
@@ -130,6 +132,7 @@
130132
CallActivity: SubWorkflowConverter,
131133
TransactionSubprocess: SubWorkflowConverter,
132134
EventSubprocess: SubWorkflowConverter,
135+
AdHocSubprocess: SubWorkflowConverter,
133136
BoundaryEventSplit: BpmnTaskSpecConverter,
134137
BoundaryEventJoin: EventJoinConverter,
135138
ExclusiveGateway: ExclusiveGatewayConverter,

SpiffWorkflow/bpmn/serializer/default/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
# 02110-1301 USA
1919

2020
from .workflow import BpmnWorkflowConverter, BpmnSubWorkflowConverter, BpmnEventConverter
21-
from .process_spec import BpmnProcessSpecConverter
21+
from .process_spec import BpmnProcessSpecConverter, AdHocSubprocessSpecConverter
2222
from .task_spec import (
2323
BpmnTaskSpecConverter,
2424
ScriptTaskConverter,
@@ -36,4 +36,4 @@
3636
ErrorEscalationEventDefinitionConverter,
3737
MessageEventDefinitionConverter,
3838
MultipleEventDefinitionConverter,
39-
)
39+
)

SpiffWorkflow/bpmn/serializer/default/process_spec.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,34 @@ def from_dict(self, dct):
8686
spec.bpmn_start_events = [spec.task_specs.get(name) for name in dct.get('bpmn_start_events', [])]
8787

8888
return spec
89+
90+
class AdHocSubprocessSpecConverter(BpmnProcessSpecConverter):
91+
92+
def to_dict(self, spec):
93+
dct = super().to_dict(spec)
94+
dct['completion_condition'] = spec.completion_condition
95+
dct['parallel'] = True
96+
dct['cancel_remaining'] = True
97+
dct['conditional_paths'] = [sp.name for sp in spec.conditional_paths]
98+
return dct
99+
100+
def from_dict(self, dct):
101+
102+
completion_condition = dct.pop('completion_condition')
103+
parallel = dct.pop('parallel')
104+
cancel_remaining = dct.pop('cancel_remaining')
105+
conditional = dct.pop('conditional_paths')
106+
107+
spec = super().from_dict(dct)
108+
spec.completion_condition = completion_condition
109+
spec.parallel = parallel
110+
spec.cancel_remaining = cancel_remaining
111+
112+
for name in conditional:
113+
spec.conditional_paths.append(spec.task_specs.get(name))
114+
115+
for task_spec in spec.task_specs.values():
116+
if spec.end.name in task_spec._outputs:
117+
task_spec.completed_event.connect(spec.path_complete)
118+
119+
return spec

SpiffWorkflow/bpmn/specs/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,4 @@
1919

2020
from .bpmn_task_spec import BpmnTaskSpec
2121
from .data_spec import BpmnDataSpecification, BpmnDataStoreSpecification
22-
from .bpmn_process_spec import BpmnProcessSpec
22+
from .bpmn_process_spec import BpmnProcessSpec, AdHocSubprocessSpec

0 commit comments

Comments
 (0)