""" $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: Craeg Strong 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)