"""
$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/03/30 03:45:47 $'[6:-2]
__version__ = '$Revision: 1.27 $'[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

# python
from cStringIO import StringIO
import os.path, sys, types

# local peer classes
from IXSLTProcessor 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 IXSLTProcessor 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
        
    def transform(self, xmlContents, xmlURL, xsltContents, xsltURL,
                  transformObject = None, 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 exceptions thrown by transformGuts and
        sends the error output to stderr, returns empty string to the
        caller.  The idea is that web site users will at worst see an
        empty page.

        """
        topLevelParams = None        
        if transformObject is not None:
            topLevelParams = self.getXSLParameters(transformObject)            
        if self.debugLevel > 1:
            print "params:", topLevelParams

        if self.debugLevel > 1:
            print "xsltContents:"
            print xsltContents
            print "xmlContents:"
            print xmlContents

        try:
            result = self.transformGuts(xmlContents, xmlURL, xsltContents, xsltURL,
                                        transformObject, topLevelParams, REQUEST)

        except Exception, e:
            sys.stderr.write(str(e) + '\n')
            return ""
            
        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

    def transformGuts(self, xmlContents, xmlURL, xsltContents, xsltURL,
                      transformObject = None, params = None,
                      REQUEST = None):
        """

        Actually performs the transformation.  Throws an Exception if
        there are any errors.
        
        """

        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 = params,
                                          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

    ################################################################
    # 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

    def getXSLParameters(self, transformObject):
        """

        Return XSL Transformer parameters as a dictionary of strings
        in the form 'name:value' as would be passed to an XSLT engine
        like Saxon, 4suite, etc. The values are obtained by looking
        for a property in the current context called 'XSLparameters',
        which should be a list of strings. Each name on the list is
        looked up in the current context. If its value is a scalar,
        then the pair 'name:value' is returned. If the value is an
        object, then the pair 'name:url' is returned where url is the
        absolute URL of the object.  The key (name) is actually a
        tuple of two strings, the first of which is an optional
        namespace (we don't use this today).

        """
        parms  = aq_get(transformObject,parametersPropertyName,None)
        result = {}
        if parms is not None:
            for p in parms:
                value = aq_get(transformObject,p,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):
                    self.addParam(result, p, value.absolute_url())
                else:
                    self.addParam(result, p, 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)
        
