jasquat 742f549e98 Squashed 'SpiffWorkflow/' changes from 12f81480a..d27519a36
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
2022-10-27 10:50:48 -04:00

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