"""
$RCSfile: FourSuite11Processor.py,v $

This class encapsulates an XSLT Processor for use by ZopeXMLMethods.
This is the 4Suite 0.11 version, including support for XSLT parameters
and URL/URN resolution.

Author: <a href="mailto:cstrong@arielpartners.com">Craeg Strong</a>
Release: 1.0
"""

__cvstag__  = '$Name:  $'[6:-2]
__date__    = '$Date: 2003/06/16 05:47:53 $'[6:-2]
__version__ = '$Revision: 1.3 $'[10:-2]

# 4Suite
from xml import xpath
from xml.xslt.Processor import Processor
from xml.xslt import XsltException
from xml.xslt.StylesheetReader import StylesheetReader
from Ft.Lib.pDomletteReader import PyExpatReader

# Zope
from Acquisition import aq_get
from zope.interface import implements

# python
from cStringIO import StringIO
import os.path, sys, types

# local peer classes
from interfaces import IXSLTProcessor

################################################################
# Defaults
################################################################

namespacesPropertyName        = 'URNnamespaces'
parametersPropertyName        = 'XSLparameters'

################################################################
# FourSuite11Processor class
################################################################

class FourSuite11Processor:
    """
    This class encapsulates an XSLT Processor for use by
    ZopeXMLMethods This is the 4Suite 0.11 version, including support
    for XSLT parameters and URN resolution.
    """

    implements(IXSLTProcessor)
    name           = '4Suite-0.11.1'
    
    def __init__(self):
        "Initialize a new instance of FourSuite11Processor"
        self.debugLevel = 0

    ################################################################
    # Methods implementing the IProcessor interface
    ################################################################

    def setDebugLevel(self, level):
        """

        Set debug level from 0 to 3.
        0 = silent
        3 = extra verbose
        Debug messages go to Zope server log.

        """
        self.debugLevel   = level
        
    ################################################################
    # Methods implementing the IXSLTProcessor interface
    ################################################################

    def transform(self, xmlContents, xmlURL, xsltContents, xsltURL,
                  transformObject = None, params = {}, REQUEST = None):
        """

        Transforms the passed in XML into the required output (usually
        HTML) using the passed in XSLT.  Both the XML and XSLT strings
        should be well-formed.  Returns the output as a string.
        transformObject and REQUEST params may be used to acquire Zope
        content such as XSLT parameters and URN namespaces, if
        required.

        Catches any 4Suite specific exceptions and raises an Exception.

        """

        if self.debugLevel > 1:
            print "params:", params

        if self.debugLevel > 1:
            print "xsltContents:"
            print xsltContents
            print "xmlContents:"
            print xmlContents

        # 4Suite 0.11.0 does not work with unicode; rather it requires an encoding.
        # Unfortunately, ParsedXML gives us unicode.  We coerce it to UTF-8
        # @@ FIXME we need a better way... CKS 3/2/2003
        if type(xmlContents) is type(u''):
            xmlContents = xmlContents.encode('utf8')
        if type(xsltContents) is type(u''):
            xsltContents = xsltContents.encode('utf8')

        # Convert the parameters to the form expected by this processor
        xsltParams = {}
        for key, value in params.items():
            self.addParam(xsltParams, key, value)

        namespaceMap   = {}
        if transformObject is not None:
            namespaceMap   = self.retrieveNamespaces(transformObject)
        if self.debugLevel > 1:
            print "namespaces:", namespaceMap

        if self.debugLevel > 1:
            from xml.xslt import ExtendedProcessingElements, StylesheetReader
            processor = ExtendedProcessingElements.ExtendedProcessor()
            if self.debugLevel > 2:
                processor._4xslt_debug = 1
                processor._4xslt_trace = 1
        else:
            processor = Processor()

        try:
            processor.setStylesheetReader(StylesheetURIResolver(namespaceMap, REQUEST, [ xsltURL ]))
            processor.setDocumentReader(DocumentURIResolver(namespaceMap, REQUEST))
            processor.appendStylesheetString(xsltContents, xsltURL)
            result =  processor.runString(xmlContents,
                                          topLevelParams = xsltParams,
                                          baseUri = xsltURL)

            if self.debugLevel > 1:
                print "===Result==="
                print result
                print "============"

        except XsltException, e:
            #(ty, val, tb) = sys.exc_info()
            #traceback.print_tb(tb)
            raise Exception(str(e))
            
        except (xpath.RuntimeException, xpath.CompiletimeException), e:
            if hasattr(e, 'stylesheetUri'):
                message = "While processing %s\n%s" % e.stylesheetUri, str(e)
                raise Exception(message)
            else:
                raise Exception(str(e))

        return result

    def addParam(self, paramMap, name, value):
        """

        This is a convenience function for adding parameters in the
        correct format to the parameter map to be used for the
        'params' parameter in transformGuts.
        
        """
        paramMap[ ('', name) ] = value
        return paramMap

    ################################################################
    # Utility methods
    ################################################################

    def retrieveNamespaces(self, transformObject):
        """

        retrieves Namespaces defined for URI Resolution

        """
        NIDs   = aq_get(transformObject,namespacesPropertyName,None)
        result = {}
        if NIDs is not None:
            for n in NIDs:
                value = aq_get(transformObject,n,None)
                # I use callable() to determine if it is not a scalar.
                # If not, it must be a Zope object (I think) - WGM
                if callable(value):
                    result[n] = value
                else:
                    result[n] = str(value)
        return result


################################################################
# URIResolver class
################################################################

class URIResolver:
    "Base class for both document and stylesheet URI resolvers"

    def __init__(self, namespaceMap):
        self.namespaceMap = namespaceMap

    def acquireObjectContents(self, base, contextURL, REQUEST):
        """

        Obtain the contents of the Zope object indicated by the passed
        in context, starting from the passed in base object.

        """
        #print "acquire contents for:",contextURL
        zObject     = base
        #print "base", zObject.getId()
        #
        # why doesn't the below work?  Is this a bug?
        # zObject = base.restrictedTraverse(contextURL)
        
        # sigh.   Do it the hard way.
        contextList = contextURL.split('/')
        for context in contextList:
            zObject = aq_get(zObject,context,None)
            if zObject is None:
                return None
        return zObject(zObject, REQUEST)

    def isRecognizedURN(self, uri):
        "Return true if this uri is of a format we recognize"
        uriParts = uri.split(':')
        return uriParts[0] == 'urn' and len(uriParts) == 3

################################################################
# StylesheetURIResolver class
################################################################

class StylesheetURIResolver(StylesheetReader, URIResolver):
    """
    A wrapper for the 4suite stylesheet reader that understands URNs
    """

    def __init__(self, namespaceMap, REQUEST, stylesheetIncPaths):
        URIResolver.__init__(self, namespaceMap)
        StylesheetReader.__init__(self, stylesheetIncPaths=stylesheetIncPaths)
        self.req                 = REQUEST
        self.stylesheetIncPaths  = stylesheetIncPaths

    def __getinitargs__(self):
        """
        Needed by BaseReader from PyExpat.  They clone the stylesheet
        reader for some reason and need to know how to call __init__
        """
        return (self.namespaceMap, self.req, self.stylesheetIncPaths)

    def fromUri(self, uri, baseUri='', ownerDoc=None, stripElements=None,
                importIndex=0, importDepth=0):
        """ resolve URI for import or include call """
        #print "fromUri:", uri

        if self.isRecognizedURN(uri):
            uriParts = uri.split(':')
            nid      = uriParts[1] # namespace ID
            nss      = uriParts[2] # namespace specific string
            base     = self.namespaceMap.get(nid, None)
            if base is None:
                # revert to normal behavior
                return StylesheetReader.fromUri(self, uri, baseUri,
                                                ownerDoc, stripElements,
                                                importIndex, importDepth)
            elif type(base) == types.StringType:
                # We are mapping one URL to another a la XMLCatalog RewriteURI
                #
                # could use urllib join, but it replaces the last component if no trailing slash. e.g.
                #
                # urllib.join  ("http://www.foo.com/bar", "mumble.xml") ==> http://www.foo.com/mumble.xml
                # os.path.join ("http://www.foo.com/bar", "mumble.xml") ==> http://www.foo.com/bar/mumble.xml
                resolvedURL = os.path.join(base, nss)
                return StylesheetReader.fromUri(self, resolvedURL, baseUri,
                                                ownerDoc, stripElements,
                                                importIndex, importDepth)
            else: # its a Zope object, we must retrieve its contents
                st = self.acquireObjectContents(base, nss, self.req)
                if st is None:
                    # failure, cannot grab object, revert to normal behavior
                    return StylesheetReader.fromUri(self, uri, baseUri,
                                                    ownerDoc, stripElements,
                                                    importIndex, importDepth)
                else:
                    myStream = StringIO(st)
                    #
                    # There is a very important reason we use fromStream
                    # here rather than fromUri.  The reason is that we do
                    # not want to exit the Zope context and come back in
                    # again.  If we were to do that, we would lose our REQUEST
                    # object, and therefore our context.
                    #
                    return StylesheetReader.fromStream(self, myStream, baseUri,
                                                       ownerDoc, stripElements,
                                                       importIndex, importDepth)
        else:
            # revert to normal behavior
            return StylesheetReader.fromUri(self, uri, baseUri,
                                            ownerDoc, stripElements,
                                            importIndex, importDepth)

################################################################
# DocumentURIResolver class
################################################################

class DocumentURIResolver(PyExpatReader, URIResolver):
    """
    A wrapper for the document reader that understands URNs
    """

    def __init__(self, namespaceMap, REQUEST):
        URIResolver.__init__(self, namespaceMap)        
        PyExpatReader.__init__(self)
        self.req  = REQUEST
    
    def fromUri(self, uri, baseUri='', ownerDoc=None, stripElements=None):
        """ resolve URI for document() call """
        #print "fromUri:", uri

        if self.isRecognizedURN(uri):
            uriParts = uri.split(':')
            nid      = uriParts[1] # namespace ID
            nss      = uriParts[2] # namespace specific string
            base = self.namespaceMap.get(nid, None)
            if base is None:
                # revert to normal behavior
                return PyExpatReader.fromUri(self, uri, baseUri,
                                             ownerDoc, stripElements)
            elif type(base) == types.StringType:
                # We are mapping one URL to another a la XMLCatalog RewriteURI
                #
                # could use urllib join, but it replaces the last component if no trailing slash. e.g.
                #
                # urllib.join  ("http://www.foo.com/bar", "mumble.xml") ==> http://www.foo.com/mumble.xml
                # os.path.join ("http://www.foo.com/bar", "mumble.xml") ==> http://www.foo.com/bar/mumble.xml
                resolvedURL = os.path.join(base, nss)
                return PyExpatReader.fromUri(self, resolvedURL, baseUri,
                                             ownerDoc, stripElements)
            else: # its a Zope object, we must retrieve its contents
                st = self.acquireObjectContents(base, nss, self.req)
                if st:
                    myStream = StringIO(st)
                    #
                    # There is a very important reason we use fromStream
                    # here rather than fromUri.  The reason is that we do
                    # not want to exit the Zope context and come back in
                    # again.  If we were to do that, we would lose our REQUEST
                    # object, and therefore our context.
                    #
                    return PyExpatReader.fromStream(self, myStream, baseUri,
                                                    ownerDoc, stripElements)
                else:
                    # failure, cannot grab object, revert to normal behavior
                    return PyExpatReader.fromUri(self, uri, baseUri,
                                                 ownerDoc, stripElements)
        else:
            # revert to normal behavior
            return PyExpatReader.fromUri(self, uri, baseUri,
                                         ownerDoc, stripElements)
        
################################################################
# Register ourselves with the Processor Registry
################################################################

from ProcessorRegistry import ProcessorRegistry
klass = FourSuite11Processor
try:
    proc = klass()
    ProcessorRegistry.register(proc)
    #print "Registered processor", klass.name, "for use with ZopeXMLMethods"
except:
    print "Processor", klass.name, "not available for use with ZopeXMLMethods"
