spiff-arena/SpiffWorkflow/serializer/prettyxml.py

324 lines
13 KiB
Python

# -*- coding: utf-8 -*-
# Copyright (C) 2007-2012 Samuel Abels
#
# 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 re
import xml.dom.minidom as minidom
from .. import operators
from ..specs.Simple import Simple
from ..specs.WorkflowSpec import WorkflowSpec
from ..exceptions import StorageException
from .base import Serializer, spec_map, op_map
# Create a list of tag names out of the spec names.
_spec_map = spec_map()
_op_map = op_map()
_exc = StorageException
class XmlSerializer(Serializer):
"""
Parses XML into a WorkflowSpec object.
"""
def deserialize_assign(self, workflow, start_node):
"""
Reads the "pre-assign" or "post-assign" tag from the given node.
start_node -- the xml node (xml.dom.minidom.Node)
"""
name = start_node.getAttribute('name')
attrib = start_node.getAttribute('field')
value = start_node.getAttribute('value')
kwargs = {}
if name == '':
_exc('name attribute required')
if attrib != '' and value != '':
_exc('Both, field and right-value attributes found')
elif attrib == '' and value == '':
_exc('field or value attribute required')
elif value != '':
kwargs['right'] = value
else:
kwargs['right_attribute'] = attrib
return operators.Assign(name, **kwargs)
def deserialize_data(self, workflow, start_node):
"""
Reads a "data" or "define" tag from the given node.
start_node -- the xml node (xml.dom.minidom.Node)
"""
name = start_node.getAttribute('name')
value = start_node.getAttribute('value')
return name, value
def deserialize_assign_list(self, workflow, start_node):
"""
Reads a list of assignments from the given node.
workflow -- the workflow
start_node -- the xml structure (xml.dom.minidom.Node)
"""
# Collect all information.
assignments = []
for node in start_node.childNodes:
if node.nodeType != minidom.Node.ELEMENT_NODE:
continue
if node.nodeName.lower() == 'assign':
assignments.append(self.deserialize_assign(workflow, node))
else:
_exc('Unknown node: %s' % node.nodeName)
return assignments
def deserialize_logical(self, node):
"""
Reads the logical tag from the given node, returns a Condition object.
node -- the xml node (xml.dom.minidom.Node)
"""
term1_attrib = node.getAttribute('left-field')
term1_value = node.getAttribute('left-value')
op = node.nodeName.lower()
term2_attrib = node.getAttribute('right-field')
term2_value = node.getAttribute('right-value')
if op not in _op_map:
_exc('Invalid operator')
if term1_attrib != '' and term1_value != '':
_exc('Both, left-field and left-value attributes found')
elif term1_attrib == '' and term1_value == '':
_exc('left-field or left-value attribute required')
elif term1_value != '':
left = term1_value
else:
left = operators.Attrib(term1_attrib)
if term2_attrib != '' and term2_value != '':
_exc('Both, right-field and right-value attributes found')
elif term2_attrib == '' and term2_value == '':
_exc('right-field or right-value attribute required')
elif term2_value != '':
right = term2_value
else:
right = operators.Attrib(term2_attrib)
return _op_map[op](left, right)
def deserialize_condition(self, workflow, start_node):
"""
Reads the conditional statement from the given node.
workflow -- the workflow with which the concurrence is associated
start_node -- the xml structure (xml.dom.minidom.Node)
"""
# Collect all information.
condition = None
spec_name = None
for node in start_node.childNodes:
if node.nodeType != minidom.Node.ELEMENT_NODE:
continue
if node.nodeName.lower() == 'successor':
if spec_name is not None:
_exc('Duplicate task name %s' % spec_name)
if node.firstChild is None:
_exc('Successor tag without a task name')
spec_name = node.firstChild.nodeValue
elif node.nodeName.lower() in _op_map:
if condition is not None:
_exc('Multiple conditions are not yet supported')
condition = self.deserialize_logical(node)
else:
_exc('Unknown node: %s' % node.nodeName)
if condition is None:
_exc('Missing condition in conditional statement')
if spec_name is None:
_exc('A %s has no task specified' % start_node.nodeName)
return condition, spec_name
def deserialize_task_spec(self, workflow, start_node, read_specs):
"""
Reads the task from the given node and returns a tuple
(start, end) that contains the stream of objects that model
the behavior.
workflow -- the workflow with which the task is associated
start_node -- the xml structure (xml.dom.minidom.Node)
"""
# Extract attributes from the node.
nodetype = start_node.nodeName.lower()
name = start_node.getAttribute('name').lower()
context = start_node.getAttribute('context').lower()
mutex = start_node.getAttribute('mutex').lower()
cancel = start_node.getAttribute('cancel').lower()
success = start_node.getAttribute('success').lower()
times = start_node.getAttribute('times').lower()
times_field = start_node.getAttribute('times-field').lower()
threshold = start_node.getAttribute('threshold').lower()
threshold_field = start_node.getAttribute('threshold-field').lower()
file_name = start_node.getAttribute('file').lower()
file_field = start_node.getAttribute('file-field').lower()
kwargs = {'lock': [],
'data': {},
'defines': {},
'pre_assign': [],
'post_assign': []}
if nodetype not in _spec_map:
_exc('Invalid task type "%s"' % nodetype)
if nodetype == 'start-task':
name = 'start'
if name == '':
_exc('Invalid task name "%s"' % name)
if name in read_specs:
_exc('Duplicate task name "%s"' % name)
if cancel != '' and cancel != '0':
kwargs['cancel'] = True
if success != '' and success != '0':
kwargs['success'] = True
if times != '':
kwargs['times'] = int(times)
if times_field != '':
kwargs['times'] = operators.Attrib(times_field)
if threshold != '':
kwargs['threshold'] = int(threshold)
if threshold_field != '':
kwargs['threshold'] = operators.Attrib(threshold_field)
if file_name != '':
kwargs['file'] = file_name
if file_field != '':
kwargs['file'] = operators.Attrib(file_field)
if nodetype == 'choose':
kwargs['choice'] = []
if nodetype == 'trigger':
context = [context]
if mutex != '':
context = mutex
# Walk through the children of the node.
successors = []
for node in start_node.childNodes:
if node.nodeType != minidom.Node.ELEMENT_NODE:
continue
if node.nodeName == 'description':
kwargs['description'] = node.firstChild.nodeValue
elif node.nodeName == 'successor' \
or node.nodeName == 'default-successor':
if node.firstChild is None:
_exc('Empty %s tag' % node.nodeName)
successors.append((None, node.firstChild.nodeValue))
elif node.nodeName == 'conditional-successor':
successors.append(self.deserialize_condition(workflow, node))
elif node.nodeName == 'define':
key, value = self.deserialize_data(workflow, node)
kwargs['defines'][key] = value
# "property" tag exists for backward compatibility.
elif node.nodeName == 'data' or node.nodeName == 'property':
key, value = self.deserialize_data(workflow, node)
kwargs['data'][key] = value
elif node.nodeName == 'pre-assign':
kwargs['pre_assign'].append(
self.deserialize_assign(workflow, node))
elif node.nodeName == 'post-assign':
kwargs['post_assign'].append(
self.deserialize_assign(workflow, node))
elif node.nodeName == 'in':
kwargs['in_assign'] = self.deserialize_assign_list(
workflow, node)
elif node.nodeName == 'out':
kwargs['out_assign'] = self.deserialize_assign_list(
workflow, node)
elif node.nodeName == 'cancel':
if node.firstChild is None:
_exc('Empty %s tag' % node.nodeName)
if context == '':
context = []
elif not isinstance(context, list):
context = [context]
context.append(node.firstChild.nodeValue)
elif node.nodeName == 'lock':
if node.firstChild is None:
_exc('Empty %s tag' % node.nodeName)
kwargs['lock'].append(node.firstChild.nodeValue)
elif node.nodeName == 'pick':
if node.firstChild is None:
_exc('Empty %s tag' % node.nodeName)
kwargs['choice'].append(node.firstChild.nodeValue)
else:
_exc('Unknown node: %s' % node.nodeName)
# Create a new instance of the task spec.
module = _spec_map[nodetype]
if nodetype == 'start-task':
spec = module(workflow, **kwargs)
elif nodetype == 'multi-instance' or nodetype == 'thread-split':
if times == '' and times_field == '':
_exc('Missing "times" or "times-field" in "%s"' % name)
elif times != '' and times_field != '':
_exc('Both, "times" and "times-field" in "%s"' % name)
spec = module(workflow, name, **kwargs)
elif context == '':
spec = module(workflow, name, **kwargs)
else:
spec = module(workflow, name, context, **kwargs)
read_specs[name] = spec, successors
def deserialize_workflow_spec(self, s_state, filename=None):
"""
Reads the workflow from the given XML structure and returns a
WorkflowSpec instance.
"""
dom = minidom.parseString(s_state)
node = dom.getElementsByTagName('process-definition')[0]
name = node.getAttribute('name')
if name == '':
_exc('%s without a name attribute' % node.nodeName)
# Read all task specs and create a list of successors.
workflow_spec = WorkflowSpec(name, filename)
del workflow_spec.task_specs['Start']
end = Simple(workflow_spec, 'End'), []
read_specs = dict(end=end)
for child_node in node.childNodes:
if child_node.nodeType != minidom.Node.ELEMENT_NODE:
continue
if child_node.nodeName == 'name':
workflow_spec.name = child_node.firstChild.nodeValue
elif child_node.nodeName == 'description':
workflow_spec.description = child_node.firstChild.nodeValue
elif child_node.nodeName.lower() in _spec_map:
self.deserialize_task_spec(
workflow_spec, child_node, read_specs)
else:
_exc('Unknown node: %s' % child_node.nodeName)
# Remove the default start-task from the workflow.
workflow_spec.start = read_specs['start'][0]
# Connect all task specs.
for name in read_specs:
spec, successors = read_specs[name]
for condition, successor_name in successors:
if successor_name not in read_specs:
_exc('Unknown successor: "%s"' % successor_name)
successor, foo = read_specs[successor_name]
if condition is None:
spec.connect(successor)
else:
spec.connect_if(condition, successor)
return workflow_spec