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

This class encapsulates an XSLT Processor for use by ZopeXMLMethod.
This is the SabPyth 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.12 $'[10:-2]

# Sablot
import Sablot

# Zope
from Acquisition import aq_get

# python
import os.path, sys, urllib

# local peer classes
from IXSLTProcessor import IXSLTProcessor

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

namespacesPropertyName        = 'URNnamespaces'
parametersPropertyName        = 'XSLparameters'

################################################################
# PyanaProcessor class
################################################################

class SabPythProcessor:
    """
    This class encapsulates an XSLT Processor for use by
    ZopeXMLMethod.  This is the SabPyth version, including support for
    XSLT parameters.  It does not yet include support for URN
    resolution.
    """

    __implements__ = IXSLTProcessor
    name           = 'Sab-Pyth-0.52'
        
    def __init__(self):
        "Initialize a new instance of SabPythProcessor"
        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 = []
        if transformObject is not None:
            topLevelParams = self.getXSLParameters(transformObject)            
        if self.debugLevel > 0:
            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 = [],
                      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

        # create a processor object:
        sp = Sablot.CreateProcessor()

        paramList = []
        if params: paramList = params.items()

        sp.regHandler(Sablot.HLR_SCHEME, UrnSchemeHandler(namespaceMap,
                                                          REQUEST))
        sp.regHandler(Sablot.HLR_MESSAGE, MessageHandler())
        
        # call the run() method:
        sp.run('arg:sheet', 'arg:input', 'arg:output',
               paramList,
               [('input', xmlContents),
                ('sheet', xsltContents)])

        # print the output, assuming it is encoded in utf-8 (the default
        # encoding output by Sablotron):
        text   = unicode(sp.getResultArg('output'), 'utf8')
        result = text.encode('ISO-8859-1')
        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
            
################################################################
# SabPyth API Hooks
################################################################

class MessageHandler:
    """

    MessageHandlers are called by Sablotron to receive error reports,
    display them, keep a log, trace etc.

    """

    def makeCode (self, severity, facility, code):
        """
    
        makes the 'external' error code to report with log() or
        error() call with facility = module id; severity = 1 iff
        critical. 'code' is the error code internal to Sablotron.

        """
        pass
        #print "makeCode", severity, facility, code

    def log (self, code, level, fields):
        """

        pass code created by makeCode, level as necessary. fields is a
        list of strings in form "field:contents". distinguished fields
        include: msg, file, line, token

        """
        pass
        #print "log", code, level, fields

    def error (self, code, level, fields):
        "for reporting errors, meaning as with log()"
        errorMessage = " ".join(fields)
        #print "error", errorMessage
        raise Exception(errorMessage)

class UrnSchemeHandler:
    """
    
    SchemeHandlers are used by Sablotron to resolve URLs that use
    other schemes than the builtin file: and arg: (eg. http: or
    ftp:). An example scheme handler that uses the standard python
    module urllib.py is provided in the file urlhandler.py.
    
    """

    def __init__(self, namespaceMap, REQUEST):
	self.handles      = {}
        self.namespaceMap = namespaceMap
        self.req          = REQUEST

    def getAll (self, scheme, rest, bytecount):
        """

        open the URI and return the whole string
        scheme = URI scheme (e.g. "http")
        rest = the rest of the URI (without colon)

        """
	#print 'getall', scheme, rest, bytecount

        if self.isRecognizedURN(scheme, rest):

            uriParts = rest.split(':')
            nid      = uriParts[0][1:] # for some reason, sablotron pre-pends a slash
            nss      = uriParts[1]
            #print "resolving urn", nid, nss
            
            base = self.namespaceMap.get(nid, None)
            st   = self.acquireObjectContents(base, nss, self.req)            
            return st
        else:
            # Maybe it is a URL.  Give it a try
            url = scheme + ':' + rest
            #print "resolving url", url
            
            return urllib.urlopen(url).read()

    def open (self, scheme, rest):
        """

        open: open the URI and return a handle
        scheme = URI scheme (e.g. "http")
        rest = the rest of the URI (without colon)
        the resulting handle is returned in '*handle'

        """
	#print 'open', scheme, rest

        handle = self.gethandle()

        if self.isRecognizedURN(scheme, rest):
            uriParts = rest.split(':')
            nid      = uriParts[0][1:] # for some reason, sablotron pre-pends a slash
            nss      = uriParts[1]
            #print "resolving urn", nid, nss
            
            base   = self.namespaceMap.get(nid, None)
            self.handles[handle] = self.acquireObjectContents(base, nss, self.req)
        else:
            # Maybe it is a URL.  Give it a try            
            url = scheme + ':' + rest
            #print "resolving url", url
            
            self.handles[handle] = urllib.urlopen(url)            
        return handle
    
    def get (self, handle, bytecount):
        """

        get: retrieve data from the URI
        handle = the handle assigned on open
        buffer = pointer to the data
        *byteCount = number of bytes to read (the number actually read is returned here) 

        """
	#print 'get'
	return self.handles[handle].read(bytecount)
        
    def put (self, handle, bytecount):
        """
        
        put: save data to the URI (if possible)
        handle = the handle assigned on open
        buffer = pointer to the data
        *byteCount = number of bytes to write (the number actually written is returned here)

        """
	#print 'put', handle, len(buffer)
	pass

    def close (self, handle):
        """
        close: close the URI with the given handle
        handle = the handle assigned on open 
        """
	#print 'close', handle
	self.handles[handle] = None

    def gethandle(self):
        "get next available handle"
        next = '%s' % (len(self.handles))
        self.handles[next] = ""
        return next

    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, base
        zObject     = base
        #print "base", zObject.getId()
        #
        # why doesn't the below work?  Is this a bug?
        # (see com/arielpartners/website/scripts/resolver.dtml)
        # 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
        contents = zObject(zObject, REQUEST)
        return contents

    def isRecognizedURN(self, scheme, uri):
        "Return true if this uri is of a format we recognize"
        uriParts = uri.split(':')
        return scheme == 'urn' and len(uriParts) == 2
