class Foreach(FlowExpression): """Foreach activity.""" allowed_child_types = _get_supported_activities() is_cond_allowed = True def __init__(self, parent_id, element, fei, context): """Constructor.""" FlowExpression.__init__(self, parent_id, element, fei, context) # these are needed to reset local context upon every iteration self._parent_ctx = context self._element = element def _activate(self, msg): if self._is_start_message(msg): selection = self._parent_ctx for key in self._element.attrib["select"].split('.'): selection = selection.get(key) assert isinstance(selection, list), "%s is not list" % selection self.context.set('inst:selection', selection) self.context.set('inst:iteration', 1) if len(selection) > 0: self.context.set('inst:current', selection[0]) def handle_message(self, channel, msg): """Handle message.""" def guard(): selection_size = len(self.context.get('inst:selection')) if self.context.get('inst:iteration') < selection_size: return True else: return False def restart(): iteration = self.context.get('inst:iteration') selection = self.context.get('inst:selection') self.context = Context(self._parent_ctx) for child in self._element: if child.tag == 'context': self.context.parse(child) break self.reset_children() self.context.set('inst:iteration', iteration + 1) self.context.set('inst:current', selection[iteration]) self.context.set('inst:selection', selection) channel.send( Message(name='start', target=self.children[0].id, origin=self.id)) return FlowExpression.handle_message(self, channel, msg) or \ self._activate(msg) or \ self._was_activated(channel, msg, guard) or \ self._was_sequence_completed(channel, msg, lambda: not guard(), restart) or \ self._was_consumed_by_child(channel, msg) or 'ignored'
class Foreach(FlowExpression): """Foreach activity.""" allowed_child_types = _get_supported_activities() is_cond_allowed = True def __init__(self, parent_id, element, fei, context): """Constructor.""" FlowExpression.__init__(self, parent_id, element, fei, context) # these are needed to reset local context upon every iteration self._parent_ctx = context self._element = element def _activate(self, msg): if self._is_start_message(msg): selection = self._parent_ctx for key in self._element.attrib["select"].split('.'): selection = selection.get(key) assert isinstance(selection, list), "%s is not list" % selection self.context.set('inst:selection', selection) self.context.set('inst:iteration', 1) if len(selection) > 0: self.context.set('inst:current', selection[0]) def handle_message(self, channel, msg): """Handle message.""" def guard(): selection_size = len(self.context.get('inst:selection')) if self.context.get('inst:iteration') < selection_size: return True else: return False def restart(): iteration = self.context.get('inst:iteration') selection = self.context.get('inst:selection') self.context = Context(self._parent_ctx) for child in self._element: if child.tag == 'context': self.context.parse(child) break self.reset_children() self.context.set('inst:iteration', iteration + 1) self.context.set('inst:current', selection[iteration]) self.context.set('inst:selection', selection) channel.send(Message(name='start', target=self.children[0].id, origin=self.id)) return FlowExpression.handle_message(self, channel, msg) or \ self._activate(msg) or \ self._was_activated(channel, msg, guard) or \ self._was_sequence_completed(channel, msg, lambda: not guard(), restart) or \ self._was_consumed_by_child(channel, msg) or 'ignored'
class While(FlowExpression): """While activity.""" allowed_child_types = _get_supported_activities() is_cond_allowed = True def __init__(self, parent_id, element, fei, context): """Constructor.""" self.conditions = [] FlowExpression.__init__(self, parent_id, element, fei, context) # these are needed to reset local context upon every iteration self._parent_ctx = context self._element = element def evaluate(self): """Check if conditions are met.""" for cond in self.conditions: if eval(cond, {"context": self.context.as_dictionary()}): LOG.debug("Condition %s evaluated to True", cond) return True else: LOG.debug("Condition %s evaluated to False", cond) return False def handle_message(self, channel, msg): """Handle message.""" def restart(): self.context = Context(self._parent_ctx) for child in self._element: if child.tag == 'context': self.context.parse(child) break self.reset_children() channel.send( Message(name='start', target=self.children[0].id, origin=self.id)) return FlowExpression.handle_message(self, channel, msg) or \ self._was_activated(channel, msg, self.evaluate) or \ self._was_sequence_completed(channel, msg, lambda: not self.evaluate(), restart) or \ self._was_consumed_by_child(channel, msg) or 'ignored'
class While(FlowExpression): """While activity.""" allowed_child_types = _get_supported_activities() is_cond_allowed = True def __init__(self, parent_id, element, fei, context): """Constructor.""" self.conditions = [] FlowExpression.__init__(self, parent_id, element, fei, context) # these are needed to reset local context upon every iteration self._parent_ctx = context self._element = element def evaluate(self): """Check if conditions are met.""" for cond in self.conditions: if eval(cond, {"context": self.context.as_dictionary()}): LOG.debug("Condition %s evaluated to True", cond) return True else: LOG.debug("Condition %s evaluated to False", cond) return False def handle_message(self, channel, msg): """Handle message.""" def restart(): self.context = Context(self._parent_ctx) for child in self._element: if child.tag == 'context': self.context.parse(child) break self.reset_children() channel.send(Message(name='start', target=self.children[0].id, origin=self.id)) return FlowExpression.handle_message(self, channel, msg) or \ self._was_activated(channel, msg, self.evaluate) or \ self._was_sequence_completed(channel, msg, lambda: not self.evaluate(), restart) or \ self._was_consumed_by_child(channel, msg) or 'ignored'
class FlowExpression(object): """Flow expression.""" allowed_child_types = () is_ctx_allowed = True # TODO: refactor to introduce simple and complex activities is_cond_allowed = False is_handler = False def __init__(self, parent_id, element, fei, context): """Constructor.""" self.fe_name = self.__class__.__name__.lower() LOG.debug("Creating %s", self.fe_name) self.state = 'ready' self.id = fei self.parent_id = parent_id self.children = [] self.faults = None if self.is_ctx_allowed: self.context = Context(context) else: self.context = context el_index = 0 for child in element: if child.tag in self.allowed_child_types: fexpr = _create_fe_from_element(self.id, child, "%s_%d" % (fei, el_index), self.context) self.children.append(fexpr) el_index = el_index + 1 else: self._parse_non_child(child) def __str__(self): """String representation.""" return "%s" % self.id def __repr__(self): """Instance representation.""" return "<%s[%s, state='%s']>" % (self.__class__.__name__, self, self.state) def _parse_non_child(self, element): """Parse disallowed child element. Some flow expressions can contain child elements in their definition which are just attributes of them, not other flow expression. As an example consider <condition> inside <case>. """ if element.tag == 'context' and self.is_ctx_allowed: self.context.parse(element) for child in element: if child.tag == 'faults': self.faults = Faults(self.id, child, "%s_faults" % self.id, self.context) break elif element.tag == 'condition' and self.is_cond_allowed: html_parser = HTMLParser() self.conditions.append(html_parser.unescape(element.text)) else: raise FlowExpressionError("'%s' is disallowed child type" % \ element.tag) def snapshot(self): """Return flow expression snapshot.""" snapshot = { "id": self.id, "state": self.state, "type": self.fe_name, "children": [child.snapshot() for child in self.children] } if self.is_ctx_allowed: snapshot["context"] = self.context.localprops if self.faults: snapshot["faults"] = self.faults.snapshot() return snapshot def reset_state(self, state): """Reset activity's state.""" LOG.debug("Resetting %s's state", self.fe_name) assert state["type"] == self.fe_name assert state["id"] == self.id self.state = state["state"] if self.is_ctx_allowed: self.context.localprops = state["context"] if self.faults: self.faults.reset_state(state["faults"]) for child, childstate in zip(self.children, state["children"]): child.reset_state(childstate) def handle_message(self, channel, msg): """Handle message. This is common code used by all derived classes. """ result = '' if self.state == 'completed': LOG.debug("%r is done already, %r is ignored", self, msg) result = 'ignored' elif msg.target is not None and \ not msg.target.startswith(self.id): LOG.debug("%r is not for %r", msg, self) result = 'ignored' elif (self.is_ctx_allowed or self.is_handler) and \ self.state == 'active' and \ msg.name == 'fault' and msg.target == self.id: self.state = 'aborting' self.context.throw(code=msg.payload.get('code', 'GenericError'), message=msg.payload.get('message', '')) all_in_final_state = True for child in self.children: if not is_state_final(child.state): all_in_final_state = False channel.send(Message(name='terminate', target=child.id, origin=self.id)) if all_in_final_state: self.state = 'aborted' channel.send(Message(name='fault', target=self.parent_id, origin=self.id, payload=msg.payload)) result = 'consumed' elif self.is_ctx_allowed and self.state == 'active' and \ msg.name == 'terminate' and msg.target == self.id: self.state = 'aborting' for child in self.children: if not is_state_final(child.state): channel.send(Message(name='terminate', target=child.id, origin=self.id)) result = 'consumed' elif self.is_ctx_allowed and self.state == 'ready' and \ msg.name == 'terminate' and msg.target == self.id: self.state = 'canceling' for child in self.children: if not is_state_final(child.state): channel.send(Message(name='terminate', target=child.id, origin=self.id)) result = 'consumed' elif (self.is_ctx_allowed or self.is_handler) and \ self.state == 'aborting' and \ (msg.name == 'canceled' or msg.name == 'aborted' or \ msg.name == 'completed') and \ msg.target == self.id: if msg.name == 'completed' and msg.origin.endswith("faults"): self.state = 'completed' del self.context._props['inst:fault'] channel.send(Message(name='completed', target=self.parent_id, origin=self.id)) elif sum([int(is_state_final(child.state)) for child in self.children]) == len(self.children): # all children are in a final state if self.faults and self.faults.state == 'ready': channel.send(Message(name='start', target=self.faults.id, origin=self.id)) else: self.state = 'aborted' channel.send(Message(name='fault', target=self.parent_id, origin=self.id, payload=self.context.get('inst:fault'))) result = 'consumed' elif self.is_ctx_allowed and self.state == 'aborting' and \ msg.name == 'fault' and msg.target == self.id: self.state = 'aborted' channel.send(Message(name='fault', target=self.parent_id, origin=self.id, payload=self.context.get('inst:fault'))) result = 'consumed' elif self.is_ctx_allowed and self.state == 'canceling' and \ (msg.name == 'canceled' or msg.name == 'aborted' or \ msg.name == 'completed') and msg.target == self.id: if sum([int(is_state_final(child.state)) for child in self.children]) == len(self.children): # all children are in a final state channel.send(Message(name='canceled', target=self.parent_id, origin=self.id)) self.state = 'canceled' result = 'consumed' elif not self.is_ctx_allowed and self.state == 'active' and \ msg.name == 'terminate' and msg.target == self.id: self.state = 'aborted' channel.send(Message(name='aborted', target=self.parent_id, origin=self.id)) result = 'consumed' elif not self.is_ctx_allowed and self.state == 'ready' and \ msg.name == 'terminate' and msg.target == self.id: self.state = 'canceled' channel.send(Message(name='canceled', target=self.parent_id, origin=self.id)) result = 'consumed' elif not self.is_ctx_allowed and self.state == 'aborting' and \ msg.name == 'terminate' and msg.target == self.id: self.state = 'aborted' channel.send(Message(name='aborted', target=self.parent_id, origin=self.id)) result = 'consumed' return result def _was_activated(self, channel, msg, guard=lambda: True): """Check if it's activation message.""" if self._is_start_message(msg): if not guard(): LOG.debug("Conditions for %r don't hold", self) self.state = 'completed' channel.send(Message(name='completed', origin=self.id, target=self.parent_id)) return 'consumed' if len(self.children) > 0: self.state = 'active' channel.send(Message(name='start', origin=self.id, target=self.children[0].id)) else: self.state = 'completed' channel.send(Message(name='completed', origin=self.id, target=self.parent_id)) return 'consumed' else: return '' def _was_sequence_completed(self, channel, msg, guard=lambda: True, compensate=lambda: None): """Check if all children in sequence were completed.""" res = '' if self._is_complete_message(msg): for index, child in zip(range(0, len(self.children)), self.children): if child.id == msg.origin: if (index + 1) < len(self.children): channel.send(Message(name='start', origin=self.id, target="%s_%d" % (self.id, index + 1))) else: if guard(): self.state = 'completed' channel.send(Message(name='completed', origin=self.id, target=self.parent_id)) else: compensate() res = 'consumed' break return res def _was_consumed_by_child(self, channel, msg): """Check if a child consumed the message.""" for child in self.children: if msg.target.startswith(child.id): return child.handle_message(channel, msg) if self.state == 'aborting' and self.faults and \ msg.target.startswith(self.faults.id): return self.faults.handle_message(channel, msg) def _is_start_message(self, msg): """Return True if the message destined to start the activity.""" return self.state == 'ready' and msg.name == 'start' \ and msg.target == self.id def _is_complete_message(self, msg): """Return True if the message is from completed child.""" return self.state == 'active' and msg.name == 'completed' \ and msg.target == self.id def reset_children(self): """Reset children state to ready.""" for child in self.children: child.state = 'ready' child.reset_children()
class FlowExpression(object): """Flow expression.""" allowed_child_types = () is_ctx_allowed = True # TODO: refactor to introduce simple and complex activities is_cond_allowed = False is_handler = False def __init__(self, parent_id, element, fei, context): """Constructor.""" self.fe_name = self.__class__.__name__.lower() LOG.debug("Creating %s", self.fe_name) self.state = 'ready' self.id = fei self.parent_id = parent_id self.children = [] self.faults = None if self.is_ctx_allowed: self.context = Context(context) else: self.context = context el_index = 0 for child in element: if child.tag in self.allowed_child_types: fexpr = _create_fe_from_element(self.id, child, "%s_%d" % (fei, el_index), self.context) self.children.append(fexpr) el_index = el_index + 1 else: self._parse_non_child(child) def __str__(self): """String representation.""" return "%s" % self.id def __repr__(self): """Instance representation.""" return "<%s[%s, state='%s']>" % (self.__class__.__name__, self, self.state) def _parse_non_child(self, element): """Parse disallowed child element. Some flow expressions can contain child elements in their definition which are just attributes of them, not other flow expression. As an example consider <condition> inside <case>. """ if element.tag == 'context' and self.is_ctx_allowed: self.context.parse(element) for child in element: if child.tag == 'faults': self.faults = Faults(self.id, child, "%s_faults" % self.id, self.context) break elif element.tag == 'condition' and self.is_cond_allowed: html_parser = HTMLParser() self.conditions.append(html_parser.unescape(element.text)) else: raise FlowExpressionError("'%s' is disallowed child type" % \ element.tag) def snapshot(self): """Return flow expression snapshot.""" snapshot = { "id": self.id, "state": self.state, "type": self.fe_name, "children": [child.snapshot() for child in self.children] } if self.is_ctx_allowed: snapshot["context"] = self.context.localprops if self.faults: snapshot["faults"] = self.faults.snapshot() return snapshot def reset_state(self, state): """Reset activity's state.""" LOG.debug("Resetting %s's state", self.fe_name) assert state["type"] == self.fe_name assert state["id"] == self.id self.state = state["state"] if self.is_ctx_allowed: self.context.localprops = state["context"] if self.faults: self.faults.reset_state(state["faults"]) for child, childstate in zip(self.children, state["children"]): child.reset_state(childstate) def handle_message(self, channel, msg): """Handle message. This is common code used by all derived classes. """ result = '' if self.state == 'completed': LOG.debug("%r is done already, %r is ignored", self, msg) result = 'ignored' elif msg.target is not None and \ not msg.target.startswith(self.id): LOG.debug("%r is not for %r", msg, self) result = 'ignored' elif (self.is_ctx_allowed or self.is_handler) and \ self.state == 'active' and \ msg.name == 'fault' and msg.target == self.id: self.state = 'aborting' self.context.throw(code=msg.payload.get('code', 'GenericError'), message=msg.payload.get('message', '')) all_in_final_state = True for child in self.children: if not is_state_final(child.state): all_in_final_state = False channel.send( Message(name='terminate', target=child.id, origin=self.id)) if all_in_final_state: self.state = 'aborted' channel.send( Message(name='fault', target=self.parent_id, origin=self.id, payload=msg.payload)) result = 'consumed' elif self.is_ctx_allowed and self.state == 'active' and \ msg.name == 'terminate' and msg.target == self.id: self.state = 'aborting' for child in self.children: if not is_state_final(child.state): channel.send( Message(name='terminate', target=child.id, origin=self.id)) result = 'consumed' elif self.is_ctx_allowed and self.state == 'ready' and \ msg.name == 'terminate' and msg.target == self.id: self.state = 'canceling' for child in self.children: if not is_state_final(child.state): channel.send( Message(name='terminate', target=child.id, origin=self.id)) result = 'consumed' elif (self.is_ctx_allowed or self.is_handler) and \ self.state == 'aborting' and \ (msg.name == 'canceled' or msg.name == 'aborted' or \ msg.name == 'completed') and \ msg.target == self.id: if msg.name == 'completed' and msg.origin.endswith("faults"): self.state = 'completed' del self.context._props['inst:fault'] channel.send( Message(name='completed', target=self.parent_id, origin=self.id)) elif sum([ int(is_state_final(child.state)) for child in self.children ]) == len(self.children): # all children are in a final state if self.faults and self.faults.state == 'ready': channel.send( Message(name='start', target=self.faults.id, origin=self.id)) else: self.state = 'aborted' channel.send( Message(name='fault', target=self.parent_id, origin=self.id, payload=self.context.get('inst:fault'))) result = 'consumed' elif self.is_ctx_allowed and self.state == 'aborting' and \ msg.name == 'fault' and msg.target == self.id: self.state = 'aborted' channel.send( Message(name='fault', target=self.parent_id, origin=self.id, payload=self.context.get('inst:fault'))) result = 'consumed' elif self.is_ctx_allowed and self.state == 'canceling' and \ (msg.name == 'canceled' or msg.name == 'aborted' or \ msg.name == 'completed') and msg.target == self.id: if sum([ int(is_state_final(child.state)) for child in self.children ]) == len(self.children): # all children are in a final state channel.send( Message(name='canceled', target=self.parent_id, origin=self.id)) self.state = 'canceled' result = 'consumed' elif not self.is_ctx_allowed and self.state == 'active' and \ msg.name == 'terminate' and msg.target == self.id: self.state = 'aborted' channel.send( Message(name='aborted', target=self.parent_id, origin=self.id)) result = 'consumed' elif not self.is_ctx_allowed and self.state == 'ready' and \ msg.name == 'terminate' and msg.target == self.id: self.state = 'canceled' channel.send( Message(name='canceled', target=self.parent_id, origin=self.id)) result = 'consumed' elif not self.is_ctx_allowed and self.state == 'aborting' and \ msg.name == 'terminate' and msg.target == self.id: self.state = 'aborted' channel.send( Message(name='aborted', target=self.parent_id, origin=self.id)) result = 'consumed' return result def _was_activated(self, channel, msg, guard=lambda: True): """Check if it's activation message.""" if self._is_start_message(msg): if not guard(): LOG.debug("Conditions for %r don't hold", self) self.state = 'completed' channel.send( Message(name='completed', origin=self.id, target=self.parent_id)) return 'consumed' if len(self.children) > 0: self.state = 'active' channel.send( Message(name='start', origin=self.id, target=self.children[0].id)) else: self.state = 'completed' channel.send( Message(name='completed', origin=self.id, target=self.parent_id)) return 'consumed' else: return '' def _was_sequence_completed(self, channel, msg, guard=lambda: True, compensate=lambda: None): """Check if all children in sequence were completed.""" res = '' if self._is_complete_message(msg): for index, child in zip(range(0, len(self.children)), self.children): if child.id == msg.origin: if (index + 1) < len(self.children): channel.send( Message(name='start', origin=self.id, target="%s_%d" % (self.id, index + 1))) else: if guard(): self.state = 'completed' channel.send( Message(name='completed', origin=self.id, target=self.parent_id)) else: compensate() res = 'consumed' break return res def _was_consumed_by_child(self, channel, msg): """Check if a child consumed the message.""" for child in self.children: if msg.target.startswith(child.id): return child.handle_message(channel, msg) if self.state == 'aborting' and self.faults and \ msg.target.startswith(self.faults.id): return self.faults.handle_message(channel, msg) def _is_start_message(self, msg): """Return True if the message destined to start the activity.""" return self.state == 'ready' and msg.name == 'start' \ and msg.target == self.id def _is_complete_message(self, msg): """Return True if the message is from completed child.""" return self.state == 'active' and msg.name == 'completed' \ and msg.target == self.id def reset_children(self): """Reset children state to ready.""" for child in self.children: child.state = 'ready' child.reset_children()