"""
$RCSfile: LibXsltProcessor.py,v $

This class encapsulates an XSLT Processor for use by ZopeXMLMethods.
This is the GNOME libxslt version, including support for XSLT
parameters.  It does not yet include support for 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.17 $'[10:-2]

# GNOME libxslt
import libxml2
import libxslt

# Zope
from Acquisition import aq_get

# python
import sys

# local peer classes
from IXSLTProcessor import IXSLTProcessor

################################################################
# Defaults
################################################################

namespacesPropertyName        = 'URNnamespaces'
parametersPropertyName        = 'XSLparameters'
catalogPropertyName           = 'XMLcatalog'

################################################################
# LibXsltProcessor class
################################################################

class LibXsltProcessor:
    """
    
    This class encapsulates an XSLT Processor for use by ZopeXMLMethods.
    This is the GNOME libxslt version, including support for XSLT
    parameters.  It does not yet include support for URN resolution.

    """

    __implements__ = IXSLTProcessor
    name           = 'libxslt 1.0.27'
        
    def __init__(self):
        "Initialize a new instance of LibXsltProcessor"
        self.debugLevel = 0

        libxml2.registerErrorHandler(self.errorHandler, "")
        libxslt.registerErrorHandler(self.errorHandler, "")

        libxml2.lineNumbersDefault(1)
        libxml2.substituteEntitiesDefault(1)

    ################################################################
    # 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 ] = "'%s'" % (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.
        
        """
        #
        # URN Resolution not yet supported, so no use looking up namespaces yet
        # @@ CKS 10/14/2002
        #
        #namespaceMap   = {}
        #if transformObject is not None:
        #    namespaceMap   = self.retrieveNamespaces(transformObject)
        #if self.debugLevel > 1:
        #    print "namespaces:", namespaceMap

        #
        # Catalogs not yet adequately supported (see below)
        #
#         catalog = None
#         if transformObject is not None:
#             catalog = self.getXMLCatalog(transformObject)
#         if catalog is None:
#             print "no XML catalog registered"
#         else:
#             print "catalog:", catalog

        try:
            styleDoc  = libxml2.parseDoc(xsltContents)
            styleDoc.setBase(xsltURL)
        except libxml2.parserError, e:
            message = "XML parse error for XSLT file %s: %s" % (xsltURL, str(e))
            raise Exception(message)

        style     = libxslt.parseStylesheetDoc(styleDoc)

        try:
            xmlDoc    = libxml2.parseDoc(xmlContents)
            xmlDoc.setBase(xmlURL)
        except libxml2.parserError, e:
            message = "XML parse error for XML document: %s" % (str(e))
            raise Exception(message)

        resultDoc = style.applyStylesheet(xmlDoc, params)
        result    = style.saveResultToString(resultDoc)
        
        style.freeStylesheet()
        xmlDoc.freeDoc()
        resultDoc.freeDoc()
        
        return result

#     def getXMLCatalog(self, transformObject):
#         """
#         Find the OASIS TR9401 and XML Input Resolver, if any.  They
#         are registered by defining a property called 'XMLcatalog'
#         somewhere in the acquisition path, pointing to a zope object
#         whose contents is the catalog.
#
#         Two libxslt limitations stymied my efforts: (1) no python
#         binding for thread-safe xmlCatalogAddLocal() (2) Can't load
#         catalog from URI instead of filename oh well, no catalogs
#         yet.  CKS 3/18/2003
#         """
#         catalogFileName = aq_get(transformObject,
#                                  catalogPropertyName,None)
#         if catalogFileName is not None:
#             catalogObject = aq_get(transformObject, catalogFileName)
#             if catalogObject is not None:
#                 print "loading catalog", catalogObject.absolute_url(), catalogObject()
#                 #catalogDoc = libxml2.parseDoc(catalogObject())
#                 #catalog = libxml2.newCatalog(catalogDoc)
#                 catalog = libxml2.loadCatalog(catalogObject.absolute_url())
#                 return catalog
#         return None

    ################################################################
    # LibXml API Hooks
    ################################################################
    
    def errorHandler(self, ctx, error):
        """
        
        The default error handler for libxml2 and libxslt prints out
        messages to stderr.  Throw an exception instead.
        
        """
        raise Exception(error)

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