"""
$RCSfile: FourSuiteProcessor.py,v $

This class encapsulates an XSLT Processor for use by ZopeXMLMethods.
This is the 4Suite 1.0 alpha version, including support for XSLT
parameters, URL/URN resolution, and OASIS Catalogs.

Author: <a href="mailto:cstrong@arielpartners.com">Craeg Strong</a>
Release: 1.0
"""

__cvstag__  = '$Name:  $'[6:-2]
__date__    = '$Date: 2005/09/06 02:08:59 $'[6:-2]
__version__ = '$Revision: 1.4 $'[10:-2]

# python
from cStringIO import StringIO
import os.path, re, sys, types, traceback

# Zope builtins
from Acquisition import aq_get, aq_chain
from zope.interface import implements

# 4Suite

from Ft.Xml.Domlette import NonvalidatingReader, \
     ValidatingReader, Print, PrettyPrint
from Ft.Xml.InputSource import InputSource, InputSourceFactory
from Ft.Xml.XPath import Evaluate
from Ft.Xml import XPath, ReaderException
from Ft.Lib import UriException
from Ft.Lib.Resolvers import SchemeRegistryResolver
from Ft.Xml.Xslt import Processor, XsltException
from Ft.Xml.Catalog import Catalog

# local peer classes
from interfaces import IXSLTProcessor, IXPathProcessor, \
     IDTDValidator

################################################################
# Defaults
################################################################

namespacesPropertyName        = 'URNnamespaces'
parametersPropertyName        = 'XSLparameters'
catalogPropertyName           = 'XMLcatalog'

################################################################
# FourSuiteProcessor class
################################################################

class FourSuiteProcessor:
    """
    This class encapsulates an XSLT Processor for use by
    ZopeXMLMethods.  This is the 4Suite 1.0 alpha version, including
    support for XSLT parameters and URL/URN resolution.
    """

    implements(IXSLTProcessor,
                      IXPathProcessor,
                      IDTDValidator)

    name           = '4Suite 1.0beta'
        
    def __init__(self):
        "Initialize a new instance of FourSuiteProcessor"
        self.debugLevel = 0

    ################################################################
    # XPathExpression class
    ################################################################

    class XPathExpression:
        """
        This class encapsulates a compiled xpath expression
        """

        def __init__(self, expr, debugLevel):
            self.expr       = expr
            self.debugLevel = debugLevel

        ############################################################
        # Methods implementing the IXPathExpression interface
        ############################################################

        def evaluateString(self, xmlContents, xmlURL, prettyPrint = 0):
            """
            Evaluates the XPath expression against the passed in
            xmlContents, returns the resulting XML as a string.
            """

            if self.debugLevel > 1:
                print "xmlContents:"
                print xmlContents
                print "xmlURL"
                print xmlURL

            # 4Suite 1.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')

            try:
                docSrc  = InputSource(StringIO(xmlContents), xmlURL)
                dom     = NonvalidatingReader.parse(docSrc)

                context = XPath.Context.Context(dom)
                result  = self.expr.evaluate(context)
                if len(result) == 0:
                    return ''
                
                retval  = StringIO()
                if prettyPrint:
                    PrettyPrint(result[0], retval, asHtml = 0)
                else:
                    Print(result[0], retval, asHtml = 0)

                retval.seek(0)
                return retval.read()

            except (XPath.RuntimeException, XPath.CompiletimeException), e:
                raise Exception(str(e))

        def evaluateDOM(self, xmlContents, xmlURL):
            """
            Evaluates the XPath expression against the passed in
            xmlContents, returns the resulting XML as a list of DOM
            nodes
            """

            if self.debugLevel > 1:
                print "xmlContents:"
                print xmlContents
                print "xmlURL"
                print xmlURL

            # 4Suite 1.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')

            try:
                docSrc  = InputSource(StringIO(xmlContents), xmlURL)
                dom     = NonvalidatingReader.parse(docSrc)

                context = XPath.Context.Context(dom)
                result  = self.expr.evaluate(context)
                return result

            except (XPath.RuntimeException, XPath.CompiletimeException), e:
                raise Exception(str(e))

    ################################################################
    # 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 IDTDValidator interface
    ################################################################

    def dtdValidationResultsString(self, xmlContents, xmlURL,
                                   methodObject=None, REQUEST=None):
        """
        Validate the passed in XML document against the DTD that it
        refers to in a DOCTYPE.
        """

        if self.debugLevel > 1:
            print "xmlContents:"
            print xmlContents
            print "xmlURL"
            print xmlURL

        # 4Suite 1.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')

        catalog = None
        if methodObject is not None:
            catalog = self.getXMLCatalog(methodObject)
        if self.debugLevel > 1:
            if catalog is None:
                print "no XML catalog registered"
            else:
                print "catalog:", catalog.uri

        namespaceMap   = {}
        if methodObject is not None:
            namespaceMap   = self.retrieveNamespaces(methodObject)
        if self.debugLevel > 1:
            print "namespaces:", namespaceMap
        
        try:
            myResolver = URNResolver(namespaceMap, REQUEST)            
            docSrc  = InputSource(StringIO(xmlContents), xmlURL,
                                  resolver = myResolver,
                                  catalog = catalog)
            dom     = ValidatingReader.parse(docSrc)
            return 'Document is valid with respect to its DTD'
        except (XPath.RuntimeException, ReaderException), e:
            return str(e)

    def isValidForDTD(self, xmlContents, xmlURL,
                      methodObject=None, REQUEST=None):
        """
        Validate the passed in XML document against the
        DTD that it internally refers to in a DOCTYPE.
        """

        if self.debugLevel > 1:
            print "xmlContents:"
            print xmlContents
            print "xmlURL"
            print xmlURL

        # 4Suite 1.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')

        catalog = None
        if methodObject is not None:
            catalog = self.getXMLCatalog(methodObject)
        if self.debugLevel > 1:
            if catalog is None:
                print "no XML catalog registered"
            else:
                print "catalog:", catalog.uri

        namespaceMap   = {}
        if methodObject is not None:
            namespaceMap   = self.retrieveNamespaces(methodObject)
        if self.debugLevel > 1:
            print "namespaces:", namespaceMap

        try:
            myResolver = URNResolver(namespaceMap, REQUEST)            
            docSrc  = InputSource(StringIO(xmlContents), xmlURL,
                                  resolver = myResolver,
                                  catalog =  catalog)
            dom     = ValidatingReader.parse(docSrc)
            return 1
        except (XPath.RuntimeException, ReaderException), e:
            return 0

    ################################################################
    # Methods implementing the IXSLTProcessor interface
    ################################################################

    def transform(self, xmlContents, xmlURL, xsltContents, xsltURL,
                  methodObject = 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.
        methodObject and REQUEST params may be used to acquire Zope
        content such as XSLT parameters and URN namespaces, if
        required.

        Catches any 4Suite specific exceptions thrown and raises an
        Exception.

        """

        if self.debugLevel > 1:
            print "params:", params

        if self.debugLevel > 1:
            print "xsltContents:"
            print xsltContents
            print "xsltURL"
            print xsltURL
            print "xmlContents:"
            print xmlContents
            print "xmlURL"
            print xmlURL

        # 4Suite 1.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)

        catalog = None
        if methodObject is not None:
            catalog = self.getXMLCatalog(methodObject)
        if self.debugLevel > 1:
            if catalog is None:
                print "no XML catalog registered"
            else:
                print "catalog:", catalog.uri
        
        namespaceMap   = {}
        if methodObject is not None:
            namespaceMap   = self.retrieveNamespaces(methodObject)
        if self.debugLevel > 1:
            print "namespaces:", namespaceMap

        if self.debugLevel > 1:
            from Ft.Xml.Xslt import ExtendedProcessingElements
            processor = ExtendedProcessingElements.ExtendedProcessor()
            if self.debugLevel > 2:
                processor._4xslt_debug = 1
                processor._4xslt_trace = 1
        else:
            processor = Processor.Processor()

        try:
            myResolver = URNResolver(namespaceMap, REQUEST)
            processor.inputSourceFactory = InputSourceFactory(resolver = myResolver)
            docSrc   = InputSource(StringIO(xmlContents), xmlURL,
                                   resolver = myResolver,
                                   catalog = catalog)
            styleSrc = InputSource(StringIO(xsltContents), xsltURL,
                                   resolver = myResolver,
                                   catalog =  catalog)
            processor.appendStylesheet(styleSrc)
            result = processor.run(docSrc, topLevelParams = xsltParams)

            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[ (None, name) ] = value
        return paramMap
    
    ################################################################
    # Methods implementing the IXPathProcessor interface
    ################################################################

    def compile(self, xpathExpression):
        """
        Compiles the passed in XPath expression.  Throws an Exception
        if there are any errors, otherwise returns the compiled
        expression as an instance of XPathExpression.
        """

        try:
            expr    = XPath.Compile(xpathExpression)
            return self.XPathExpression(expr, self.debugLevel)

        except (XPath.RuntimeException, XPath.CompiletimeException), e:
            raise Exception(str(e))

    ################################################################
    # Internal methods
    ################################################################

    def retrieveNamespaces(self, methodObject):
        """

        retrieves Namespaces defined for URI Resolution

        """
        NIDs   = aq_get(methodObject,namespacesPropertyName,None)
        result = {}
        if NIDs is not None:
            for n in NIDs:
                value = aq_get(methodObject,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 getXMLCatalog(self, methodObject):
        """

        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

        """
        catalogFileName = aq_get(methodObject,
                                 catalogPropertyName,None)
        if catalogFileName is not None:
            catalogObject = aq_get(methodObject, catalogFileName)
            if catalogObject is not None:
                catalog = Catalog(catalogObject.absolute_url(),
                                  quiet = (self.debugLevel == 0) )
                return catalog
        return None

################################################################
# URNResolver class
################################################################

class URNResolver(SchemeRegistryResolver):
    "A resolver that resolves URNs to Zope objects"

    def __init__(self, namespaceMap, REQUEST):
        """
        Remember the URN namespaces corresponding to Zope folders and
        the REQUEST context with which we want to load the resources
        """
        SchemeRegistryResolver.__init__(self)
        self.handlers['urn'] = self.resolveURN
        self.supportedSchemes.append('urn')
        self.namespaceMap    = namespaceMap
        self.req             = REQUEST
      
    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 resolveURN(self, uri, base=None):
        "Resolve the URN to a Zope object, or die"

        if not self.isRecognizedURN(uri):
            raise UriException('Unable to resolve URI %s' % 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:
            raise UriException('Unable to resolve URI %s' % uri)
        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 self.resolve(resolvedURL, base)
        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
                raise UriException('Unable to resolve URI %s' % uri)
            else:
                # load the resource from the Zope object
                stream = StringIO(st)
                return stream

    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

################################################################
# Register ourselves with the Processor Registry
################################################################

from ProcessorRegistry import ProcessorRegistry
klass = FourSuiteProcessor
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"
