""" $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: Craeg Strong Release: 1.0 """ __cvstag__ = '$Name: $'[6:-2] __date__ = '$Date: 2003/03/30 22:14:57 $'[6:-2] __version__ = '$Revision: 1.5 $'[10:-2] # python from cStringIO import StringIO import os.path, re, sys, types, traceback # Zope builtins from Acquisition import aq_get, aq_chain # 4Suite # # WORKAROUND: PyXML 0.8.2 has a bug regarding parsing DOCTYPEs. # see: http://mail.python.org/pipermail/xml-sig/2003-March/009288.html # and: http://lists.fourthought.com/pipermail/4suite-dev/2003-March/001284.html # This hack workaround will serve until PyXML 0.8.3 is released # @@FIXME CKS 3/30/2003 # if not os.environ.has_key('XML_CATALOGS'): os.environ['XML_CATALOGS'] = '/please/ignore/this/workaround' from Ft.Xml.InputSource import InputSource, InputSourceFactory from Ft.Xml import XPath from Ft.Lib import UriException from Ft.Lib.Uri import SchemeRegistryResolver from Ft.Xml.Xslt import Processor, XsltException try: from Ft.Xml.Catalog import Catalog, CatalogInputSource except: pass # local peer classes from IXSLTProcessor import IXSLTProcessor ################################################################ # 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 name = '4Suite 1.0alpha' def __init__(self): "Initialize a new instance of FourSuiteProcessor" 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 "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') 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[ (None, 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. """ catalog = None if transformObject is not None: catalog = self.getXMLCatalog(transformObject) if self.debugLevel > 1: if catalog is None: print "no XML catalog registered" else: print "catalog:", catalog.uri namespaceMap = {} if transformObject is not None: namespaceMap = self.retrieveNamespaces(transformObject) 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) if catalog is None: docSrc = InputSource(StringIO(xmlContents), xmlURL, resolver = myResolver) styleSrc = InputSource(StringIO(xsltContents), xsltURL, resolver = myResolver) else: docSrc = CatalogInputSource(catalog, StringIO(xmlContents), xmlURL, resolver = myResolver) styleSrc = CatalogInputSource(catalog, StringIO(xsltContents), xsltURL, resolver = myResolver) processor.appendStylesheet(styleSrc) result = processor.run(docSrc, topLevelParams = params) 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 ################################################################ # Internal 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 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 """ catalogFileName = aq_get(transformObject, catalogPropertyName,None) if catalogFileName is not None: catalogObject = aq_get(transformObject, catalogFileName) if catalogObject is not None: catalog = Catalog(catalogObject.absolute_url(), quiet=0) return catalog return None ################################################################ # ResolvingInputSource 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