aboutsummaryrefslogtreecommitdiffstats
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.py34
-rw-r--r--sleekxmpp/xmlstream/matcher/id.py32
-rw-r--r--sleekxmpp/xmlstream/matcher/many.py40
-rw-r--r--sleekxmpp/xmlstream/matcher/stanzapath.py38
-rw-r--r--sleekxmpp/xmlstream/matcher/xmlmask.py155
-rw-r--r--sleekxmpp/xmlstream/matcher/xpath.py79
7 files changed, 394 insertions, 0 deletions
diff --git a/sleekxmpp/xmlstream/matcher/__init__.py b/sleekxmpp/xmlstream/matcher/__init__.py
new file mode 100644
index 0000000..1038d1b
--- /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 0000000..701ab32
--- /dev/null
+++ b/sleekxmpp/xmlstream/matcher/base.py
@@ -0,0 +1,34 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+
+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.
+ """
+
+ def __init__(self, criteria):
+ """
+ Create a new stanza matcher.
+
+ Arguments:
+ criteria -- Object to compare some aspect of a stanza
+ against.
+ """
+ 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 0000000..0c8ce2d
--- /dev/null
+++ b/sleekxmpp/xmlstream/matcher/id.py
@@ -0,0 +1,32 @@
+"""
+ 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 MatcherId(MatcherBase):
+
+ """
+ The ID matcher selects stanzas that have the same stanza 'id'
+ interface value as the desired ID.
+
+ Methods:
+ match -- Overrides MatcherBase.match.
+ """
+
+ def match(self, xml):
+ """
+ Compare the given stanza's 'id' attribute to the stored
+ id value.
+
+ Overrides MatcherBase.match.
+
+ Arguments:
+ xml -- The 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 0000000..f470ec9
--- /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 0000000..f8ff283
--- /dev/null
+++ b/sleekxmpp/xmlstream/matcher/stanzapath.py
@@ -0,0 +1,38 @@
+"""
+ 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 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.
+
+ In most cases, the stanza path and XPath should be identical, but be
+ aware that differences may occur.
+
+ Methods:
+ match -- Overrides MatcherBase.match.
+ """
+
+ 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. For most cases, the stanza path and
+ XPath should be identical, but be aware that differences may occur.
+
+ Overrides MatcherBase.match.
+
+ Arguments:
+ stanza -- The stanza object to compare against.
+ """
+ return stanza.match(self._criteria)
diff --git a/sleekxmpp/xmlstream/matcher/xmlmask.py b/sleekxmpp/xmlstream/matcher/xmlmask.py
new file mode 100644
index 0000000..2967a2a
--- /dev/null
+++ b/sleekxmpp/xmlstream/matcher/xmlmask.py
@@ -0,0 +1,155 @@
+"""
+ 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 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
+
+
+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:
+
+ <message xmlns="jabber:client"><body /></message>
+
+ Use of XMLMask is discouraged, and XPath or 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.
+
+ Methods:
+ match -- Overrides MatcherBase.match.
+ setDefaultNS -- Set the default namespace for the mask.
+ """
+
+ def __init__(self, criteria):
+ """
+ Create a new XMLMask matcher.
+
+ Arguments:
+ criteria -- Either an XML object or XML string to use as a mask.
+ """
+ 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.
+
+ Arguments:
+ 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.
+
+ Arguments:
+ 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.
+
+ Arguments:
+ source -- The XML object to compare against the mask.
+ mask -- The XML object serving as the mask.
+ 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:
+ logging.log(logging.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 != mask.text:
+ 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.
+ for subelement in mask:
+ if use_ns:
+ if not self._mask_cmp(source.find(subelement.tag),
+ subelement, use_ns):
+ 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.
+
+ Arguments:
+ xml -- The XML object to search for the given child tag.
+ 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 0000000..669c9f1
--- /dev/null
+++ b/sleekxmpp/xmlstream/matcher/xpath.py
@@ -0,0 +1,79 @@
+"""
+ 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.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.
+
+ Note that using this matcher may not produce expected behavior when using
+ attribute selectors. For Python 2.6 and 3.1, the ElementTree 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 StanzaPath matcher.
+
+ If the value of IGNORE_NS is set to true, then XPath expressions will
+ be matched without using namespaces.
+
+ Methods:
+ match -- Overrides MatcherBase.match.
+ """
+
+ def match(self, xml):
+ """
+ Compare a stanza's XML contents to an XPath expression.
+
+ If the value of IGNORE_NS is set to true, then XPath expressions
+ will be matched without using namespaces.
+
+ Note that in Python 2.6 and 3.1 the ElementTree find method does
+ not support attribute selectors in the XPath expression.
+
+ Arguments:
+ xml -- The stanza object 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