########################################################################
#
# File Name:            XUpdate.py
#
# Documentation:        http://docs.4suite.org/4uite/Lib/XUpdate.py.html
#
"""
Handles XUpdate requests (see http://xmldb.org/xupdate/xupdate-wd.html)
WWW: http://4suite.org         e-mail: support@4suite.org

Copyright (c) 2001 Fourthought, Inc. USA.   All Rights Reserved.
See  http://4suite.org/COPYRIGHT  for license and copyright information
"""

import string, os, urllib, urlparse, cStringIO
from Ft.Lib import pDomlette
from xml.xslt import XsltElement
from xml.xslt.AttributeValueTemplate import AttributeValueTemplate
import xml.xslt.Processor
from xml.xpath.Context import Context
from xml.xslt.DomWriter import DomWriter
from xml.xslt.NullWriter import NullWriter
from xml.xpath import Compile
from xml.dom.ext import SplitQName
from xml.dom.NodeFilter import NodeFilter
from xml.dom import Node
from xml.xpath import Conversions

XUPDATE_NS = 'http://www.xmldb.org/xupdate'
XU_MODIFICATION_EXPR = Compile('.//xupdate:modifications')


class Processor(xml.xslt.Processor.Processor):
    def __init__(self, reader=None):
        xml.xslt.Processor.Processor.__init__(self, reader)
        self.writers.append(NullWriter())
        return

    def execute(self, node, xupdate):
        con = Context(xupdate, 1, 1, processorNss={'xupdate': XUPDATE_NS})
        xu_nodes = XU_MODIFICATION_EXPR.evaluate(con)
        root = xupdate.ownerDocument or xupdate
        snit = root.createNodeIterator(
            xupdate, NodeFilter.SHOW_ELEMENT, None ,0
            )
        #curr_node = snit.nextNode()
        curr_node = snit.nextNode()
        while curr_node:
            curr_node.setup()
            curr_node = snit.nextNode()
        for xu_mod in xu_nodes:
            context = Context(node)
            context = xu_mod.instantiate(context, self)
        return



class XUpdateHandler(pDomlette.Handler):

    def __init__(self, resolveEntity=None, processIncludes=1,
                 visitedHrefs=None, force8Bit=0):
        pDomlette.Handler.__init__(self,
                                   resolveEntity=resolveEntity,
                                   processIncludes=processIncludes,
                                   visitedHrefs=visitedHrefs,
                                   force8Bit=force8Bit)

    def startElement(self, name, attribs):
        self._completeTextNode()
        (name, qname, nsattribs) = self._handleStartElementNss(name, attribs)
        namespace = name[0]
        local = name[1]
        prefix = SplitQName(qname)[0]
        if namespace == XUPDATE_NS:
            elem_class = XUPDATE_ELEMENT_MAPPING[local]
            new_element = elem_class(self._ownerDoc, namespace, local, prefix)
        else:
            new_element = LiteralElement(self._ownerDoc, namespace, local,
                                         prefix, '')
        for attr_qname in nsattribs.getQNames():
            (attr_ns, attr_local) = nsattribs.getNameByQName(attr_qname)
            attr_prefix = SplitQName(attr_qname)[0]
            attr = pDomlette.Attribute(self._ownerDoc, attr_ns, attr_local,
                                       attr_prefix)
            attr.value = nsattribs.getValueByQName(attr_qname)
            new_element.attributes[(attr_ns, attr_local)] = attr
        self._nodeStack.append(new_element)
        return

    def endElement(self, name):
        self._completeTextNode()
        new_element = self._nodeStack[-1]
        del self._nodeStack[-1]
        del self._namespaces[-1]
        self._nodeStack[-1].appendChild(new_element)
        return

    def _completeTextNode(self):
        #Note some parsers don't report ignorable white space properly
        if self._currText and len(self._nodeStack) and self._nodeStack[-1].nodeType != Node.DOCUMENT_NODE:
            if self._preserveStateStack[-1] or string.strip(self._currText):
                new_text = LiteralText(self._ownerDoc)
                new_text.data = self._currText
                top_node = self._nodeStack[-1]
                top_node.appendChild(new_text)
        self._currText = ''
        return


class Reader(pDomlette.PyExpatReader):
    HandlerClass = XUpdateHandler


class ModificationsElement(XsltElement):
    legalAttrs = ('version',)

    def __init__(self, doc, uri=XUPDATE_NS, localName='modifications',
                 prefix='xupdate', baseUri=''):
        XsltElement.__init__(self, doc, uri, localName, prefix, baseUri)
        return

    def setup(self):
        self._version = self.getAttributeNS('', 'version')
        if not self._version:
            raise XUpdateException(Error.NO_VERSION)
        XsltElement.setup(self)
        return

    def instantiate(self, context, processor):
        origState = context.copy()
        context.setNamespaces(self._nss)
        for child in self.childNodes:
            context = child.instantiate(context, processor)[0]
        context.set(origState)
        return (context,)


class InsertElement(XsltElement):
    legalAttrs = ('select', )

    def __init__(self, doc, uri=XUPDATE_NS, localName='insert',
                 prefix='xupdate', baseUri=''):
        XsltElement.__init__(self, doc, uri, localName, prefix, baseUri)
        self._direction = localName[7:]
        return

    def setup(self):
        self._select = Compile(self.getAttributeNS('', 'select'))
        if not self._select:
            raise XUpdateException(Error.NO_SELECT)
        XsltElement.setup(self)
        return

    def instantiate(self, context, processor):
        origState = context.copy()
        context.setNamespaces(self._nss)
        selection = self._select.evaluate(context)
        refnode = selection[0]
        if refnode:
            processor.pushResult(handler=DomWriter(refnode.ownerDocument))
            try:
                for child in self.childNodes:
                    context = child.instantiate(context, processor)[0]
            finally:
                result = processor.popResult()
            if self._direction == 'before':
                refnode.parentNode.insertBefore(result, refnode)
            elif self._direction == 'after':
                if refnode.nextSibling:
                    refnode.parentNode.insertBefore(result,
                                                    refnode.nextSibling)
                else:
                    refnode.parentNode.appendChild(result)
        context.set(origState)
        return (context,)


class AppendElement(XsltElement):
    legalAttrs = ('select', 'child', )

    def __init__(self, doc, uri=XUPDATE_NS, localName='append',
                 prefix='xupdate', baseUri=''):
        XsltElement.__init__(self, doc, uri, localName, prefix, baseUri)
        return

    def setup(self):
        self._child = Compile(self.getAttributeNS('', 'child') or 'last()')
        self._select = Compile(self.getAttributeNS('', 'select'))
        if not self._select:
            raise XUpdateException(Error.NO_SELECT)
        XsltElement.setup(self)
        return

    def instantiate(self, context, processor):
        origState = context.copy()
        context.setNamespaces(self._nss)
        selection = self._select.evaluate(context)
        refnode = selection[0]
        if refnode:
            processor.pushResult(handler=DomWriter(refnode.ownerDocument))
            try:
                for child in self.childNodes:
                    context = child.instantiate(context, processor)[0]
            finally:
                result = processor.popResult()
            lrc = len(refnode.childNodes)
            con = Context(refnode, 1, lrc,
                          processorNss={'xupdate': XUPDATE_NS})
            #Python lists is 0-indexed counting, node-sets 1-indexed
            position = Conversions.NumberValue(self._child.evaluate(con))
            if position >= lrc:
                refnode.appendChild(result)
            else:
                refnode.insertBefore(result, refnode.childNodes[position])
        context.set(origState)
        return (context,)


class UpdateElement(XsltElement):
    legalAttrs = ('select', )

    def __init__(self, doc, uri=XUPDATE_NS, localName='update',
                 prefix='xupdate', baseUri=''):
        XsltElement.__init__(self, doc, uri, localName, prefix, baseUri)
        return

    def setup(self):
        self._select = Compile(self.getAttributeNS('', 'select'))
        if not self._select:
            raise XUpdateException(Error.NO_SELECT)
        XsltElement.setup(self)
        return

    def instantiate(self, context, processor):
        origState = context.copy()
        context.setNamespaces(self._nss)
        selection = self._select.evaluate(context)
        refnode = selection[0]
        if refnode:
            processor.pushResult(handler=DomWriter(refnode.ownerDocument))
            try:
                for child in self.childNodes:
                    context = child.instantiate(context, processor)[0]
            finally:
                result = processor.popResult()
            while refnode.firstChild:
                refnode.removeChild(refnode.childNodes[0])
            refnode.appendChild(result)
        context.set(origState)
        return (context,)


class RemoveElement(XsltElement):
    legalAttrs = ('select', )

    def __init__(self, doc, uri=XUPDATE_NS, localName='remove',
                 prefix='xupdate', baseUri=''):
        XsltElement.__init__(self, doc, uri, localName, prefix, baseUri)
        return

    def setup(self):
        self._select = Compile(self.getAttributeNS('', 'select'))
        if not self._select:
            raise XUpdateException(Error.NO_SELECT)
        XsltElement.setup(self)
        return

    def instantiate(self, context, processor):
        origState = context.copy()
        context.setNamespaces(self._nss)
        selection = self._select.evaluate(context)
        refnode = selection[0]
        if refnode:
            refnode.parentNode.removeChild(refnode)
        context.set(origState)
        return (context,)


class VariableElement(XsltElement):
    legalAttrs = ('select', )

    def __init__(self, doc, uri=XUPDATE_NS, localName='variable',
                 prefix='xupdate', baseUri=''):
        XsltElement.__init__(self, doc, uri, localName, prefix, baseUri)
        return

    def setup(self):
        name_attr = self.getAttributeNS('', 'name')
        self._name = Util.ExpandQName(name_attr, namespaces=self._nss)
        self._select = Compile(self.getAttributeNS('', 'select'))
        if not self._select:
            raise XUpdateException(Error.NO_SELECT)
        XsltElement.setup(self)
        return

    def instantiate(self, context, processor):
        context.setNamespaces(self._nss)
        selection = self._select.evaluate(context)
        context.varBindings[self._name] = selection
        return (context,)


class RenameElement(XsltElement):
    legalAttrs = ('select', )

    def __init__(self, doc, uri=XUPDATE_NS, localName='rename',
                 prefix='xupdate', baseUri=''):
        XsltElement.__init__(self, doc, uri, localName, prefix, baseUri)
        return

    def setup(self):
        self._select = Compile(self.getAttributeNS('', 'select'))
        if not self._select:
            raise XUpdateException(Error.NO_SELECT)
        XsltElement.setup(self)
        return

    def instantiate(self, context, processor):
        origState = context.copy()
        context.setNamespaces(self._nss)
        selection = self._select.evaluate(context)
        refnode = selection[0]
        if refnode:
            refnode.parentNode.removeChild(refNode)
        context.set(origState)
        return (context,)


class LiteralText(pDomlette.Text):
    def __init__(self, doc):
        pDomlette.Text.__init__(self, doc)
        return
    
    def instantiate(self, context, processor):
        processor.writers[-1].text(self.data)
        return (context,)


class LiteralElement(XsltElement):
    legalAttrs = ()

    def __init__(self, doc, uri, localName, prefix, baseUri):
        XsltElement.__init__(self, doc, uri, localName, prefix, baseUri)
        return

    def setup(self):
        XsltElement.setup(self)
        return

    def instantiate(self, context, processor):
        origState = context.copy()
        context.setNamespaces(self._nss)
        processor.writers[-1].startElement(self.nodeName, self.namespaceURI)
        for child in self.childNodes:
            context = child.instantiate(context, processor)[0]
        processor.writers[-1].endElement(self.nodeName)
        context.set(origState)
        return (context,)


class ElementElement(XsltElement):
    legalAttrs = ('name', 'namespace')

    def __init__(self, doc, uri=XUPDATE_NS, localName='element',
                 prefix='xupdate', baseUri=''):
        XsltElement.__init__(self, doc, uri, localName, prefix, baseUri)
        return

    def setup(self):
        self._name = AttributeValueTemplate(self.getAttributeNS('', 'name'))
        self._namespace = AttributeValueTemplate(self.getAttributeNS('', 'namespace'))
        if not self._name:
            raise XUpdateException(Error.NO_NAME)
        XsltElement.setup(self)
        return

    def instantiate(self, context, processor):
        origState = context.copy()
        context.setNamespaces(self._nss)
        name = self._name.evaluate(context)
        namespace = self._namespace.evaluate(context)
        (prefix, local) = xml.dom.ext.SplitQName(name)
        if not namespace and prefix:
            namespace = context.processorNss[prefix]
        processor.writers[-1].startElement(name, namespace)
        for child in self.childNodes:
            context = child.instantiate(context, processor)[0]
        processor.writers[-1].endElement(name)
        return (context,)


class AttributeElement(XsltElement):
    legalAttrs = ('name', 'namespace')

    def __init__(self, doc, uri=XUPDATE_NS, localName='attribute',
                 prefix='xupdate', baseUri=''):
        XsltElement.__init__(self, doc, uri, localName, prefix, baseUri)
        return

    def setup(self):
        self._name = AttributeValueTemplate(self.getAttributeNS('', 'name'))
        self._namespace = AttributeValueTemplate(self.getAttributeNS('', 'namespace'))
        self._value = ''
        try:
            self._value = self.firstChild.data
        except AttributeError:
            self._value = ''
        if not self._name:
            raise XUpdateException(Error.NO_NAME)
        XsltElement.setup(self)
        return

    def instantiate(self, context, processor):
        origState = context.copy()
        context.setNamespaces(self._nss)
        name = self._name.evaluate(context)
        namespace = self._namespace.evaluate(context)
        processor.writers[-1].attribute(name, self._value, namespace)
        (prefix, local) = xml.dom.ext.SplitQName(name)
        if not namespace and prefix:
            namespace = context.processorNss[prefix]
        return (context,)


class TextElement(XsltElement):
    legalAttrs = ()

    def __init__(self, doc, uri=XUPDATE_NS, localName='text',
                 prefix='xupdate', baseUri=''):
        XsltElement.__init__(self, doc, uri, localName, prefix, baseUri)
        return

    def setup(self):
        try:
            self._data = self.firstChild.data
        except AttributeError:
            self._data = ''
        XsltElement.setup(self)
        return

    def instantiate(self, context, processor):
        origState = context.copy()
        context.setNamespaces(self._nss)
        processor.writers[-1].text(self._data)
        return (context,)


XUPDATE_ELEMENT_MAPPING = {
    'modifications': ModificationsElement,
    'insert-before': InsertElement,
    'insert-after': InsertElement,
    'element': ElementElement,
    'attribute': AttributeElement,
    'text': TextElement,
    'append': AppendElement,
    'update': UpdateElement,
    'remove': RemoveElement,
    'variable': VariableElement,
    'rename': RenameElement,
#    '': Element,
#    '': Element,
    }


