mirror of
https://github.com/status-im/spiff-arena.git
synced 2025-01-15 12:44:52 +00:00
742f549e98
d27519a36 Merge pull request #259 from sartography/bugfix/spiff-postscript-execution 21aa8a12c update execution order for postscripts d83fd3d81 Merge pull request #256 from sartography/feature/xml-validation 8303aaab5 uping the sleep time in a test slightly to see if we can get this test to pass consistently in CI. 1d251d55d determine whether to validate by passing in a validator instead of a parameter 2d3daad2d add spiff schema f8c65dc60 Minor changes to BPMN diagrams to assure all tests are run against valid BPMN Diagrams. Changes required: 9e06b25bf add DMN validation 1b7cbeba0 set parser to validate by default 53fdbba52 add schemas & validation option a212d9c5d general cleanup git-subtree-dir: SpiffWorkflow git-subtree-split: d27519a3631b9772094e5f24dba2f478b0c47135
262 lines
11 KiB
Python
262 lines
11 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
# Copyright (C) 2012 Matthew Hampton
|
|
#
|
|
# This library is free software; you can redistribute it and/or
|
|
# modify it under the terms of the GNU Lesser General Public
|
|
# License as published by the Free Software Foundation; either
|
|
# version 2.1 of the License, or (at your option) any later version.
|
|
#
|
|
# This library is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
# Lesser General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Lesser General Public
|
|
# License along with this library; if not, write to the Free Software
|
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
|
# 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
|
|
|
|
from .ValidationException import ValidationException
|
|
from ..specs.BpmnProcessSpec import BpmnProcessSpec
|
|
from ..specs.events import StartEvent, EndEvent, BoundaryEvent, IntermediateCatchEvent, IntermediateThrowEvent
|
|
from ..specs.events import SendTask, ReceiveTask
|
|
from ..specs.SubWorkflowTask import CallActivity, SubWorkflowTask, TransactionSubprocess
|
|
from ..specs.ExclusiveGateway import ExclusiveGateway
|
|
from ..specs.InclusiveGateway import InclusiveGateway
|
|
from ..specs.ManualTask import ManualTask
|
|
from ..specs.NoneTask import NoneTask
|
|
from ..specs.ParallelGateway import ParallelGateway
|
|
from ..specs.ScriptTask import ScriptTask
|
|
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,
|
|
CallActivityParser, ScriptTaskParser, SubWorkflowParser,
|
|
ServiceTaskParser)
|
|
from .event_parsers import (StartEventParser, EndEventParser, BoundaryEventParser,
|
|
IntermediateCatchEventParser, IntermediateThrowEventParser,
|
|
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
|
|
a set of BPMN files. It is intended that this class will be overriden by an
|
|
application that implements a BPMN engine.
|
|
|
|
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
|
|
"""
|
|
|
|
PARSER_CLASSES = {
|
|
full_tag('startEvent'): (StartEventParser, StartEvent),
|
|
full_tag('endEvent'): (EndEventParser, EndEvent),
|
|
full_tag('userTask'): (UserTaskParser, UserTask),
|
|
full_tag('task'): (NoneTaskParser, 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('callActivity'): (CallActivityParser, CallActivity),
|
|
full_tag('transaction'): (SubWorkflowParser, TransactionSubprocess),
|
|
full_tag('scriptTask'): (ScriptTaskParser, ScriptTask),
|
|
full_tag('serviceTask'): (ServiceTaskParser, ServiceTask),
|
|
full_tag('intermediateCatchEvent'): (IntermediateCatchEventParser, IntermediateCatchEvent),
|
|
full_tag('intermediateThrowEvent'): (IntermediateThrowEventParser, IntermediateThrowEvent),
|
|
full_tag('boundaryEvent'): (BoundaryEventParser, BoundaryEvent),
|
|
full_tag('receiveTask'): (ReceiveTaskParser, ReceiveTask),
|
|
full_tag('sendTask'): (SendTaskParser, SendTask),
|
|
}
|
|
|
|
OVERRIDE_PARSER_CLASSES = {}
|
|
|
|
PROCESS_PARSER_CLASS = ProcessParser
|
|
|
|
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()
|
|
|
|
def _get_parser_class(self, tag):
|
|
if tag in self.OVERRIDE_PARSER_CLASSES:
|
|
return self.OVERRIDE_PARSER_CLASSES[tag]
|
|
elif tag in self.PARSER_CLASSES:
|
|
return self.PARSER_CLASSES[tag]
|
|
return None, None
|
|
|
|
def get_process_parser(self, process_id_or_name):
|
|
"""
|
|
Returns the ProcessParser for the given process ID or name. It matches
|
|
by name first.
|
|
"""
|
|
if process_id_or_name in self.process_parsers_by_name:
|
|
return self.process_parsers_by_name[process_id_or_name]
|
|
elif process_id_or_name in self.process_parsers:
|
|
return self.process_parsers[process_id_or_name]
|
|
|
|
def get_process_ids(self):
|
|
"""Returns a list of process IDs"""
|
|
return list(self.process_parsers.keys())
|
|
|
|
def add_bpmn_file(self, filename):
|
|
"""
|
|
Add the given BPMN filename to the parser's set.
|
|
"""
|
|
self.add_bpmn_files([filename])
|
|
|
|
def add_bpmn_files_by_glob(self, g):
|
|
"""
|
|
Add all filenames matching the provided pattern (e.g. *.bpmn) to the
|
|
parser's set.
|
|
"""
|
|
self.add_bpmn_files(glob.glob(g))
|
|
|
|
def add_bpmn_files(self, filenames):
|
|
"""
|
|
Add all filenames in the given list to the parser's set.
|
|
"""
|
|
for filename in filenames:
|
|
f = open(filename, 'r')
|
|
try:
|
|
self.add_bpmn_xml(etree.parse(f), filename=filename)
|
|
finally:
|
|
f.close()
|
|
|
|
def add_bpmn_xml(self, bpmn, filename=None):
|
|
"""
|
|
Add the given lxml representation of the BPMN file to the parser's set.
|
|
|
|
:param svg: Optionally, provide the text data for the SVG of the BPMN
|
|
file
|
|
:param filename: Optionally, provide the source filename.
|
|
"""
|
|
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, 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'))
|
|
|
|
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:
|
|
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 get_dependencies(self):
|
|
return self.process_dependencies
|
|
|
|
def get_process_dependencies(self):
|
|
return self.process_dependencies
|
|
|
|
def get_spec(self, process_id_or_name):
|
|
"""
|
|
Parses the required subset of the BPMN files, in order to provide an
|
|
instance of BpmnProcessSpec (i.e. WorkflowSpec)
|
|
for the given process ID or name. The Name is matched first.
|
|
"""
|
|
parser = self.get_process_parser(process_id_or_name)
|
|
if parser is None:
|
|
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_subprocess_specs(self, name, specs=None):
|
|
used = specs or {}
|
|
wf_spec = self.get_spec(name)
|
|
for task_spec in wf_spec.task_specs.values():
|
|
if isinstance(task_spec, SubWorkflowTask) and task_spec.spec not in used:
|
|
used[task_spec.spec] = self.get_spec(task_spec.spec)
|
|
self.get_subprocess_specs(task_spec.spec, used)
|
|
return used
|
|
|
|
def find_all_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_collaboration(self, name):
|
|
self.find_all_specs()
|
|
spec = BpmnProcessSpec(name)
|
|
subprocesses = {}
|
|
start = StartEvent(spec, 'Start Collaboration', NoneEventDefinition())
|
|
spec.start.connect(start)
|
|
end = EndEvent(spec, 'End Collaboration', NoneEventDefinition())
|
|
end.connect(spec.end)
|
|
for process in self.collaborations[name]:
|
|
process_parser = self.get_process_parser(process)
|
|
if process_parser and process_parser.process_executable:
|
|
participant = CallActivity(spec, process, process)
|
|
start.connect(participant)
|
|
participant.connect(end)
|
|
subprocesses[process] = self.get_spec(process)
|
|
subprocesses.update(self.get_subprocess_specs(process))
|
|
return spec, subprocesses
|