summaryrefslogtreecommitdiff
path: root/sleekxmpp/xmlstream/matcher
diff options
context:
space:
mode:
Diffstat (limited to 'sleekxmpp/xmlstream/matcher')
-rw-r--r--sleekxmpp/xmlstream/matcher/__init__.py16
-rw-r--r--sleekxmpp/xmlstream/matcher/base.py31
-rw-r--r--sleekxmpp/xmlstream/matcher/id.py29
-rw-r--r--sleekxmpp/xmlstream/matcher/many.py40
-rw-r--r--sleekxmpp/xmlstream/matcher/stanzapath.py43
-rw-r--r--sleekxmpp/xmlstream/matcher/xmlmask.py158
-rw-r--r--sleekxmpp/xmlstream/matcher/xpath.py86
7 files changed, 403 insertions, 0 deletions
diff --git a/sleekxmpp/xmlstream/matcher/__init__.py b/sleekxmpp/xmlstream/matcher/__init__.py
new file mode 100644
index 00000000..1038d1bd
--- /dev/null
+++ b/sleekxmpp/xmlstream/matcher/__init__.py
@@ -0,0 +1,16 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.xmlstream.matcher.id import MatcherId
+from sleekxmpp.xmlstream.matcher.many import MatchMany
+from sleekxmpp.xmlstream.matcher.stanzapath import StanzaPath
+from sleekxmpp.xmlstream.matcher.xmlmask import MatchXMLMask
+from sleekxmpp.xmlstream.matcher.xpath import MatchXPath
+
+__all__ = ['MatcherId', 'MatchMany', 'StanzaPath',
+ 'MatchXMLMask', 'MatchXPath']
diff --git a/sleekxmpp/xmlstream/matcher/base.py b/sleekxmpp/xmlstream/matcher/base.py
new file mode 100644
index 00000000..83c26688
--- /dev/null
+++ b/sleekxmpp/xmlstream/matcher/base.py
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+"""
+ sleekxmpp.xmlstream.matcher.base
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Part of SleekXMPP: The Sleek XMPP Library
+
+ :copyright: (c) 2011 Nathanael C. Fritz
+ :license: MIT, see LICENSE for more details
+"""
+
+
+class MatcherBase(object):
+
+ """
+ Base class for stanza matchers. Stanza matchers are used to pick
+ stanzas out of the XML stream and pass them to the appropriate
+ stream handlers.
+
+ :param criteria: Object to compare some aspect of a stanza against.
+ """
+
+ def __init__(self, criteria):
+ self._criteria = criteria
+
+ def match(self, xml):
+ """Check if a stanza matches the stored criteria.
+
+ Meant to be overridden.
+ """
+ return False
diff --git a/sleekxmpp/xmlstream/matcher/id.py b/sleekxmpp/xmlstream/matcher/id.py
new file mode 100644
index 00000000..11ab70bb
--- /dev/null
+++ b/sleekxmpp/xmlstream/matcher/id.py
@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+"""
+ sleekxmpp.xmlstream.matcher.id
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Part of SleekXMPP: The Sleek XMPP Library
+
+ :copyright: (c) 2011 Nathanael C. Fritz
+ :license: MIT, see LICENSE for more details
+"""
+
+from sleekxmpp.xmlstream.matcher.base import MatcherBase
+
+
+class MatcherId(MatcherBase):
+
+ """
+ The ID matcher selects stanzas that have the same stanza 'id'
+ interface value as the desired ID.
+ """
+
+ def match(self, xml):
+ """Compare the given stanza's ``'id'`` attribute to the stored
+ ``id`` value.
+
+ :param xml: The :class:`~sleekxmpp.xmlstream.stanzabase.ElementBase`
+ stanza to compare against.
+ """
+ return xml['id'] == self._criteria
diff --git a/sleekxmpp/xmlstream/matcher/many.py b/sleekxmpp/xmlstream/matcher/many.py
new file mode 100644
index 00000000..f470ec9c
--- /dev/null
+++ b/sleekxmpp/xmlstream/matcher/many.py
@@ -0,0 +1,40 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.xmlstream.matcher.base import MatcherBase
+
+
+class MatchMany(MatcherBase):
+
+ """
+ The MatchMany matcher may compare a stanza against multiple
+ criteria. It is essentially an OR relation combining multiple
+ matchers.
+
+ Each of the criteria must implement a match() method.
+
+ Methods:
+ match -- Overrides MatcherBase.match.
+ """
+
+ def match(self, xml):
+ """
+ Match a stanza against multiple criteria. The match is successful
+ if one of the criteria matches.
+
+ Each of the criteria must implement a match() method.
+
+ Overrides MatcherBase.match.
+
+ Arguments:
+ xml -- The stanza object to compare against.
+ """
+ for m in self._criteria:
+ if m.match(xml):
+ return True
+ return False
diff --git a/sleekxmpp/xmlstream/matcher/stanzapath.py b/sleekxmpp/xmlstream/matcher/stanzapath.py
new file mode 100644
index 00000000..a4c0fda0
--- /dev/null
+++ b/sleekxmpp/xmlstream/matcher/stanzapath.py
@@ -0,0 +1,43 @@
+# -*- coding: utf-8 -*-
+"""
+ sleekxmpp.xmlstream.matcher.stanzapath
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Part of SleekXMPP: The Sleek XMPP Library
+
+ :copyright: (c) 2011 Nathanael C. Fritz
+ :license: MIT, see LICENSE for more details
+"""
+
+from sleekxmpp.xmlstream.matcher.base import MatcherBase
+from sleekxmpp.xmlstream.stanzabase import fix_ns
+
+
+class StanzaPath(MatcherBase):
+
+ """
+ The StanzaPath matcher selects stanzas that match a given "stanza path",
+ which is similar to a normal XPath except that it uses the interfaces and
+ plugins of the stanza instead of the actual, underlying XML.
+
+ :param criteria: Object to compare some aspect of a stanza against.
+ """
+
+ def __init__(self, criteria):
+ self._criteria = fix_ns(criteria, split=True,
+ propagate_ns=False,
+ default_ns='jabber:client')
+ self._raw_criteria = criteria
+
+ def match(self, stanza):
+ """
+ Compare a stanza against a "stanza path". A stanza path is similar to
+ an XPath expression, but uses the stanza's interfaces and plugins
+ instead of the underlying XML. See the documentation for the stanza
+ :meth:`~sleekxmpp.xmlstream.stanzabase.ElementBase.match()` method
+ for more information.
+
+ :param stanza: The :class:`~sleekxmpp.xmlstream.stanzabase.ElementBase`
+ stanza to compare against.
+ """
+ return stanza.match(self._criteria) or stanza.match(self._raw_criteria)
diff --git a/sleekxmpp/xmlstream/matcher/xmlmask.py b/sleekxmpp/xmlstream/matcher/xmlmask.py
new file mode 100644
index 00000000..7977e767
--- /dev/null
+++ b/sleekxmpp/xmlstream/matcher/xmlmask.py
@@ -0,0 +1,158 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from xml.parsers.expat import ExpatError
+
+from sleekxmpp.xmlstream.stanzabase import ET
+from sleekxmpp.xmlstream.matcher.base import MatcherBase
+
+
+# Flag indicating if the builtin XPath matcher should be used, which
+# uses namespaces, or a custom matcher that ignores namespaces.
+# Changing this will affect ALL XMLMask matchers.
+IGNORE_NS = False
+
+
+log = logging.getLogger(__name__)
+
+
+class MatchXMLMask(MatcherBase):
+
+ """
+ The XMLMask matcher selects stanzas whose XML matches a given
+ XML pattern, or mask. For example, message stanzas with body elements
+ could be matched using the mask:
+
+ .. code-block:: xml
+
+ <message xmlns="jabber:client"><body /></message>
+
+ Use of XMLMask is discouraged, and
+ :class:`~sleekxmpp.xmlstream.matcher.xpath.MatchXPath` or
+ :class:`~sleekxmpp.xmlstream.matcher.stanzapath.StanzaPath`
+ should be used instead.
+
+ The use of namespaces in the mask comparison is controlled by
+ ``IGNORE_NS``. Setting ``IGNORE_NS`` to ``True`` will disable namespace
+ based matching for ALL XMLMask matchers.
+
+ :param criteria: Either an :class:`~xml.etree.ElementTree.Element` XML
+ object or XML string to use as a mask.
+ """
+
+ def __init__(self, criteria):
+ MatcherBase.__init__(self, criteria)
+ if isinstance(criteria, str):
+ self._criteria = ET.fromstring(self._criteria)
+ self.default_ns = 'jabber:client'
+
+ def setDefaultNS(self, ns):
+ """Set the default namespace to use during comparisons.
+
+ :param ns: The new namespace to use as the default.
+ """
+ self.default_ns = ns
+
+ def match(self, xml):
+ """Compare a stanza object or XML object against the stored XML mask.
+
+ Overrides MatcherBase.match.
+
+ :param xml: The stanza object or XML object to compare against.
+ """
+ if hasattr(xml, 'xml'):
+ xml = xml.xml
+ return self._mask_cmp(xml, self._criteria, True)
+
+ def _mask_cmp(self, source, mask, use_ns=False, default_ns='__no_ns__'):
+ """Compare an XML object against an XML mask.
+
+ :param source: The :class:`~xml.etree.ElementTree.Element` XML object
+ to compare against the mask.
+ :param mask: The :class:`~xml.etree.ElementTree.Element` XML object
+ serving as the mask.
+ :param use_ns: Indicates if namespaces should be respected during
+ the comparison.
+ :default_ns: The default namespace to apply to elements that
+ do not have a specified namespace.
+ Defaults to ``"__no_ns__"``.
+ """
+ use_ns = not IGNORE_NS
+
+ if source is None:
+ # If the element was not found. May happend during recursive calls.
+ return False
+
+ # Convert the mask to an XML object if it is a string.
+ if not hasattr(mask, 'attrib'):
+ try:
+ mask = ET.fromstring(mask)
+ except ExpatError:
+ log.warning("Expat error: %s\nIn parsing: %s", '', mask)
+ if not use_ns:
+ # Compare the element without using namespaces.
+ source_tag = source.tag.split('}', 1)[-1]
+ mask_tag = mask.tag.split('}', 1)[-1]
+ if source_tag != mask_tag:
+ return False
+ else:
+ # Compare the element using namespaces
+ mask_ns_tag = "{%s}%s" % (self.default_ns, mask.tag)
+ if source.tag not in [mask.tag, mask_ns_tag]:
+ return False
+
+ # If the mask includes text, compare it.
+ if mask.text and source.text and \
+ source.text.strip() != mask.text.strip():
+ return False
+
+ # Compare attributes. The stanza must include the attributes
+ # defined by the mask, but may include others.
+ for name, value in mask.attrib.items():
+ if source.attrib.get(name, "__None__") != value:
+ return False
+
+ # Recursively check subelements.
+ matched_elements = {}
+ for subelement in mask:
+ if use_ns:
+ matched = False
+ for other in source.findall(subelement.tag):
+ matched_elements[other] = False
+ if self._mask_cmp(other, subelement, use_ns):
+ if not matched_elements.get(other, False):
+ matched_elements[other] = True
+ matched = True
+ if not matched:
+ return False
+ else:
+ if not self._mask_cmp(self._get_child(source, subelement.tag),
+ subelement, use_ns):
+ return False
+
+ # Everything matches.
+ return True
+
+ def _get_child(self, xml, tag):
+ """Return a child element given its tag, ignoring namespace values.
+
+ Returns ``None`` if the child was not found.
+
+ :param xml: The :class:`~xml.etree.ElementTree.Element` XML object
+ to search for the given child tag.
+ :param tag: The name of the subelement to find.
+ """
+ tag = tag.split('}')[-1]
+ try:
+ children = [c.tag.split('}')[-1] for c in xml.getchildren()]
+ index = children.index(tag)
+ except ValueError:
+ return None
+ return xml.getchildren()[index]
diff --git a/sleekxmpp/xmlstream/matcher/xpath.py b/sleekxmpp/xmlstream/matcher/xpath.py
new file mode 100644
index 00000000..b6af0609
--- /dev/null
+++ b/sleekxmpp/xmlstream/matcher/xpath.py
@@ -0,0 +1,86 @@
+# -*- coding: utf-8 -*-
+"""
+ sleekxmpp.xmlstream.matcher.xpath
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Part of SleekXMPP: The Sleek XMPP Library
+
+ :copyright: (c) 2011 Nathanael C. Fritz
+ :license: MIT, see LICENSE for more details
+"""
+
+from sleekxmpp.xmlstream.stanzabase import ET
+from sleekxmpp.xmlstream.matcher.base import MatcherBase
+
+
+# Flag indicating if the builtin XPath matcher should be used, which
+# uses namespaces, or a custom matcher that ignores namespaces.
+# Changing this will affect ALL XPath matchers.
+IGNORE_NS = False
+
+
+class MatchXPath(MatcherBase):
+
+ """
+ The XPath matcher selects stanzas whose XML contents matches a given
+ XPath expression.
+
+ .. warning::
+
+ Using this matcher may not produce expected behavior when using
+ attribute selectors. For Python 2.6 and 3.1, the ElementTree
+ :meth:`~xml.etree.ElementTree.Element.find()` method does
+ not support the use of attribute selectors. If you need to
+ support Python 2.6 or 3.1, it might be more useful to use a
+ :class:`~sleekxmpp.xmlstream.matcher.stanzapath.StanzaPath` matcher.
+
+ If the value of :data:`IGNORE_NS` is set to ``True``, then XPath
+ expressions will be matched without using namespaces.
+ """
+
+ def match(self, xml):
+ """
+ Compare a stanza's XML contents to an XPath expression.
+
+ If the value of :data:`IGNORE_NS` is set to ``True``, then XPath
+ expressions will be matched without using namespaces.
+
+ .. warning::
+
+ In Python 2.6 and 3.1 the ElementTree
+ :meth:`~xml.etree.ElementTree.Element.find()` method does not
+ support attribute selectors in the XPath expression.
+
+ :param xml: The :class:`~sleekxmpp.xmlstream.stanzabase.ElementBase`
+ stanza to compare against.
+ """
+ if hasattr(xml, 'xml'):
+ xml = xml.xml
+ x = ET.Element('x')
+ x.append(xml)
+
+ if not IGNORE_NS:
+ # Use builtin, namespace respecting, XPath matcher.
+ if x.find(self._criteria) is not None:
+ return True
+ return False
+ else:
+ # Remove namespaces from the XPath expression.
+ criteria = []
+ for ns_block in self._criteria.split('{'):
+ criteria.extend(ns_block.split('}')[-1].split('/'))
+
+ # Walk the XPath expression.
+ xml = x
+ for tag in criteria:
+ if not tag:
+ # Skip empty tag name artifacts from the cleanup phase.
+ continue
+
+ children = [c.tag.split('}')[-1] for c in xml.getchildren()]
+ try:
+ index = children.index(tag)
+ except ValueError:
+ return False
+ xml = xml.getchildren()[index]
+ return True