########################################################################
# $Header: /var/local/cvsroot/4Suite/Ft/Xml/XPath/Util.py,v 1.22 2005/03/18 23:47:17 jkloth Exp $
"""
General utilities for XPath applications

Copyright 2005 Fourthought, Inc. (USA).
Detailed license and copyright information: http://4suite.org/COPYRIGHT
Project home, documentation, distributions: http://4suite.org/
"""

import os
from xml.dom import Node

from Ft.Xml import EMPTY_NAMESPACE
from Ft.Xml.Domlette import GetAllNs
from Ft.Xml.Lib.XmlString import SplitQName

# NOTE: XPathParser and Context are added to this module by __init__.py

__all__ = [# XPath expression parser:
           #'XPathParser', #is this necessary to expose?
           # XPath expression processing:
           'Compile', 'Evaluate', 'SimpleEvaluate',
           # DOM preparation for XPath processing:
           'NormalizeNode',
           # misc XPath related utilities:
           'GetElementById', 'ElementsById', 'g_documentIdIndex',
           'ExpandQName',
           ]


g_documentIdIndex = {}

def GetElementById(document, idstr):
    """
    Given an ID string, returns the node with that ID from the given
    Domlette document. Raises a ValueError if there is more than one
    element with that ID.
    """
    if id(document) not in g_documentIdIndex:
        # First time through for this document, build ID index
        g_documentIdIndex[id(document)] = mapping = {}
        ElementsById(document.documentElement, mapping)

    elements = g_documentIdIndex[id(document)].get(idstr, [None])
    if len(elements) > 1:
        raise ValueError("ID not unique")
    return elements[0]


def ElementsById(element, idmap):
    """
    Given an element node and a cache of ID value to element node
    mappings already known, supplements the cache with mappings for
    all descendant elements that have ID attributes. The cache is
    modified in place.

    Only works for elements named 'id' or 'ID' in no namespace;
    the attribute type as defined by a DTD does not matter.
    """
    idattr = element.getAttributeNodeNS(EMPTY_NAMESPACE, 'ID') or \
             element.getAttributeNodeNS(EMPTY_NAMESPACE, 'id')
    if idattr:
        idmap.setdefault(idattr.value, []).append(idattr.ownerElement)

    for element in filter(lambda node: node.nodeType == Node.ELEMENT_NODE,
                          element.childNodes):
        ElementsById(element, idmap)
    return


def ExpandQName(qname, refNode=None, namespaces=None):
    """
    Expand the given QName in the context of the given node,
    or in the given namespace dictionary.

    Returns a 2-tuple consisting of the namespace URI and local name.
    """
    nss = {}
    if refNode:
        nss = GetAllNs(refNode)
    elif namespaces:
        nss = namespaces
    (prefix, local) = SplitQName(qname)
    #We're not to use the default namespace
    if prefix:
        try:
            split_name = (nss[prefix], local)
        except KeyError:
            from Ft.Xml.XPath import RuntimeException
            raise RuntimeException(RuntimeException.UNDEFINED_PREFIX,
                                   prefix)
    else:
        split_name = (EMPTY_NAMESPACE, local)
    return split_name


def NormalizeNode(node):
    """
    NormalizeNode is used to prepare a DOM for XPath evaluation.

    1.  Convert CDATA Sections to Text Nodes.
    2.  Normalize all text nodes (adjacent nodes are merged into the first one).
    """
    node = node.firstChild
    while node:
        if node.nodeType == Node.CDATA_SECTION_NODE:
            # If followed by a text node, add this data to it
            if node.nextSibling and node.nextSibling.nodeType == Node.TEXT_NODE:
                node.nextSibling.insertData(0, node.data)
            elif node.data:
                # Replace this node with a new text node
                text = node.ownerDocument.createTextNode(node.data)
                node.parentNode.replaceChild(text, node)
                node = text
            else:
                # It is empty, get rid of it
                next = node.nextSibling
                node.parentNode.removeChild(node)
                node = next
                # Just in case it is None
                continue
        elif node.nodeType == Node.TEXT_NODE:
            next = node.nextSibling
            while next and next.nodeType in [Node.TEXT_NODE,
                                             Node.CDATA_SECTION_NODE]:
                node.appendData(next.data)
                node.parentNode.removeChild(next)
                next = node.nextSibling
            if not node.data:
                # Remove any empty text nodes
                next = node.nextSibling
                node.parentNode.removeChild(node)
                node = next
                # Just in case it is None
                continue
        elif node.nodeType == Node.ELEMENT_NODE:
            for attr in node.attributes.values():
                if len(attr.childNodes) > 1:
                    NormalizeNode(attr)
            NormalizeNode(node)
        node = node.nextSibling
    return

# -- Core XPath API ---------------------------------------------------------


def SimpleEvaluate(expr, node, explicitNss=None):
    """
    Designed to be the most simple/brain-dead interface to using XPath
    Usually invoked through Node objects using:
      node.xpath(expr[, explicitNss])

    expr - XPath expression in string or compiled form
    node - the node to be used as core of the context for evaluating the XPath
    explicitNss - (optional) any additional or overriding namespace mappings
                  in the form of a dictionary of prefix: namespace
                  the base namespace mappings are taken from in-scope
                  declarations on the given node.  This explicit dictionary
                  is suprimposed on the base mappings
    """
    if 'EXTMODULES' in os.environ:
        ext_modules = os.environ["EXTMODULES"].split(':')
    else:
        ext_modules = []
    explicitNss = explicitNss or {}

    nss = GetAllNs(node)
    nss.update(explicitNss)
    context = Context.Context(node, 0, 0, processorNss=nss,
                              extModuleList=ext_modules)

    if hasattr(expr, "evaluate"):
        retval = expr.evaluate(context)
    else:
        retval = XPathParser.new().parse(expr).evaluate(context)
    return retval


def Evaluate(expr, contextNode=None, context=None):
    """
    Evaluates the given XPath expression.

    Two arguments are required: the expression (as a string or compiled
    expression object), and a context. The context can be given as a
    Domlette node via the 'contextNode' named argument, or can be given as
    an Ft.Xml.XPath.Context.Context object via the 'context' named
    argument.

    If namespace bindings or variable bindings are needed, use a
    Context object. If extension functions are needed, either use a
    Context object, or set the EXTMODULES environment variable to be a
    ':'-separated list of names of Python modules that implement
    extension functions.

    The return value will be one of the following:
    node-set: list of Domlette node objects (xml.dom.Node based);
    string: Unicode string type;
    number: float type;
    boolean: Ft.Lib.boolean C extension object;
    or a non-XPath object (i.e. as returned by an extension function).
    """
    if 'EXTMODULES' in os.environ:
        ext_modules = os.environ["EXTMODULES"].split(':')
    else:
        ext_modules = []

    if contextNode and context:
        con = context.clone()
        con.node = contextNode
    elif context:
        con = context
    elif contextNode:
        #contextNode should be a node, not a context obj,
        #but this is a common error.  Be forgiving?
        if isinstance(contextNode, Context.Context):
            con = contextNode
        else:
            con = Context.Context(contextNode, 0, 0, extModuleList=ext_modules)
    else:
        # import here to avoid circularity
        from Ft.Xml.XPath import RuntimeException
        raise RuntimeException(RuntimeException.NO_CONTEXT)

    if hasattr(expr, "evaluate"):
        retval = expr.evaluate(con)
    else:
        retval = XPathParser.new().parse(expr).evaluate(con)
    return retval


def Compile(expr):
    """
    Given an XPath expression as a string, returns an object that allows
    an evaluation engine to operate on the expression efficiently.
    This "compiled" expression object can be passed to the Evaluate
    function instead of a string, in order to shorten the amount of time
    needed to evaluate the expression.
    """
    if not isinstance(expr, (str, unicode)):
        raise TypeError("Expected string, found %s" % type(expr))
    try:
        return XPathParser.new().parse(expr)
    except SyntaxError, error:
        # import here to avoid circularity
        from Ft.Xml.XPath import CompiletimeException
        raise CompiletimeException(CompiletimeException.SYNTAX, 0, 0, str(error))
    except:
        import traceback, cStringIO
        stream = cStringIO.StringIO()
        traceback.print_exc(None, stream)
        # import here to avoid circularity
        from Ft.Xml.XPath import CompiletimeException
        raise CompiletimeException(CompiletimeException.INTERNAL, stream.getvalue())

