""" $RCSfile: XSLTMethod.py,v $ ZopeXMLMethods provides methods to apply to Zope objects for XML/XSLT processing. XSLTMethod associates XSLT transformers with XML documents. XSLTMethod automatically transforms an XML document via XSLT, where the XML document is obtained from another Zope object (the 'source' object) via acquisition. Author: Craeg Strong Modified by Philipp von Weitershausen Release: 1.0 $Id: XSLTMethod.py,v 1.55 2003/03/30 03:45:47 cstrong Exp $ """ __cvstag__ = '$Name: $'[6:-2] __date__ = '$Date: 2003/03/30 03:45:47 $'[6:-2] __version__ = '$Revision: 1.55 $'[10:-2] # python import mimetypes # base classes from OFS.SimpleItem import SimpleItem from Products.ZCatalog.CatalogAwareness import CatalogAware from OFS.PropertyManager import PropertyManager # peer classes/modules from IXSLTMethod import IXSLTMethod from ProcessorChooser import ProcessorChooser from GeneratorRegistry import GeneratorRegistry # Zope builtins import OFS import Globals from Globals import MessageDialog from Acquisition import aq_base, aq_parent from AccessControl import ClassSecurityInfo from ZPublisher.mapply import mapply from Products.PageTemplates.PageTemplateFile import PageTemplateFile ################################################################ # Permissions ################################################################ # By defining these constants, no new permissions will be created when # misspelling them in the declareProtected() call PERM_VIEW = "View" PERM_EDIT = "Edit" PERM_FTP = "FTP access" PERM_CONTENT = "Access contents information" PERM_MANAGE = "Manage XML Methods" ################################################################ # Utilties ################################################################ def getPublishedResult(name, obj, REQUEST): """ Get the result of an object as if it were published """ # # index_html is generally used for human-readable HTML, whereas # __call__ would normally be used to obtain access to the # underlying XML source. However, some Zope products do not adhere # to this convention, so we call index_html if __call__ is not # supported. # if hasattr(aq_base(obj), "__call__") and obj.__call__ is not None: return obj(obj, REQUEST) elif hasattr(obj, "index_html") and obj.index_html is not None: return mapply(obj.index_html, (), REQUEST) elif callable(obj): return apply(obj) else: message = "Error, unable to obtain source from object %s" % (name) raise Exception(message) def findCacheManager(self): """ Find an instance of CacheManager to use (optionally, provided the caching property is set to 'on') CacheManager is purely optional. If it is present, caching may be done. If applicable, the XSLTMethod will use the CacheManager it finds via this method The current policy is as follows: use the first cache manager found by acquisition. Alternatively, a more complex policy could be used such as a) looking in some predefined place, where the place could be specified in some property (a la Zope3 explicit acquisition), or b) finding all cache managers in the ZODB and presenting a list for selection by the user. For right now, we don't want to over-engineer the damn thing. CKS 8/4/2002. """ metaType = 'XML Method Cache Manager' cm = None folder = self root = self.getPhysicalRoot() while cm is None: if folder.isPrincipiaFolderish: cacheManagers = folder.objectValues(metaType) if cacheManagers: cm = cacheManagers[0] if cm is None: if folder is root: return None else: folder = aq_parent(folder) return cm # Below gets confused by context aquisition. Bad idea. # #cacheManagers = self.superValues(metaType) #print "got cache managers", cacheManagers #if cacheManagers: # return cacheManagers[0] #else: # return None ################################################################ # Module Scoped Convenience Methods ################################################################ def addInstance(folder, id, title='', description='', selected_processor='', xslTransformerId='', content_type="text/html", behave_like='', caching='on', debugLevel=0): """ This is a convenience factory method for creating an instance of XSLTMethod. It returns the object created, and may therefore be more convenient than the addXSLTMethod() method it calls. It is used by the unit testing programs. """ folder.manage_addProduct['ZopeXMLMethods'].addXSLTMethod( id, title, description, selected_processor, xslTransformerId, content_type, behave_like, caching, debugLevel) return folder[id] ################################################################ # Contructors ################################################################ manage_addXSLTMethodForm = PageTemplateFile('www/create.pt', globals()) def manage_addXSLTMethod(self, id, title='', description='', selected_processor='', xslTransformerId='', content_type="text/html", behave_like='', caching='on', debugLevel=0, REQUEST=None, RESPONSE=None): """ Factory method to create an instance of XSLTMethod, called from a GUI in the Zope Management Interface. It calls addXSLTMethod to actually do the work. """ try: self.addXSLTMethod(id, title, description, selected_processor, xslTransformerId, content_type, behave_like, caching, debugLevel) message = 'Successfully created ' + id + ' XSLTMethod object.' if REQUEST is None: return # support Add and Edit try: url = self.DestinationURL() except: url = REQUEST['URL1'] if REQUEST['submit'] == " Add and Edit ": url = "%s/%s" % (url, id) REQUEST.RESPONSE.redirect(url + '/manage_editForm') else: REQUEST.RESPONSE.redirect(url + "/manage_main") except Exception, e: message = str(e) message.replace('\n','
') return MessageDialog(title = 'Error', message = message, action = 'manage_main') def addXSLTMethod(self, id, title='', description='', selected_processor='', xslTransformerId='', content_type="text/html", behave_like='', caching='on',debugLevel=0): """ Factory method to actually create an instance of XSLTMethod and return it. You should call this method directly if you are creating an instance of XSLTMethod programatically. """ if not id or not xslTransformerId: raise Exception('Required fields must not be blank') tran = self.restrictedTraverse(xslTransformerId, None) if tran is None: message = "Invalid transformer name. %s not found" % (xslTransformerId) raise Exception(message) self._setObject(id, XSLTMethod(id, title, description, selected_processor, xslTransformerId, content_type, behave_like, caching, debugLevel)) self._getOb(id).reindex_object() ################################################################ # Main class ################################################################ class XSLTMethod(CatalogAware, # ZCatalog support PropertyManager, # Property support SimpleItem): """ Automatically transforms an XML document via XSLT, where both the XML document and XSLT are obtained from other Zope objects via acquisition. """ meta_type = 'XSLT Method' __implements__ = IXSLTMethod _security = ClassSecurityInfo() _properties = ( {'id':'title', 'type':'string', 'mode': 'w' }, {'id':'description', 'type':'text', 'mode': 'w' }, {'id':'content_type', 'type':'string', 'mode': 'w' }, {'id':'selected_processor', 'type':'selection', 'mode': 'w', 'select_variable':'availableProcessors' }, {'id':'debugLevel', 'type':'int', 'mode': 'w' }, {'id':'caching', 'type':'selection', 'mode': 'w', 'select_variable':"onOff" }, ) manage_options = ( {'label': 'Edit', 'action': 'manage_editForm', 'help': ('ZopeXMLMethods','edit.stx') },) + \ OFS.PropertyManager.PropertyManager.manage_options + \ OFS.SimpleItem.SimpleItem.manage_options def __init__(self, id, title, description, selected_processor, xslTransformerId, content_type="text/html", behave_like="", caching='on', debugLevel=0): # string attributes self.id = id self.title = title self.description = description self.selected_processor = selected_processor self.xslTransformerId = xslTransformerId self.content_type = content_type self.behave_like = behave_like self.caching = caching self.debugLevel = debugLevel # processor chooser self._processorChooser = ProcessorChooser(preferred = selected_processor) self.selected_processor = self._processorChooser.defaultProcessor() if self.selected_processor is None: raise Exception('No supported XSLT processors available') ################################################################ # ZMI methods ################################################################ _security.declareProtected(PERM_MANAGE, 'manage_editForm') manage_editForm = PageTemplateFile('www/edit.pt',globals()) _security.declareProtected(PERM_EDIT, 'manage_edit') def manage_edit(self, xslTransformerId, behave_like, REQUEST=None, RESPONSE=None): """ Edit XSLTMethod settings """ try: self.editTransform(xslTransformerId) except Exception, e: return MessageDialog(title = 'Error', message = str(e), action = 'manage_editForm') self.behave_like = behave_like message = 'Changes saved.' return self.manage_editForm(manage_tabs_message=message) _security.declareProtected(PERM_EDIT, 'manage_editProperties') def manage_editProperties(self, REQUEST): """ Cover for PropertyManager.manage_editProperties() method. set debugLevel of underlying XSLTProcessor to our debugLevel, then pass it on to the inherited method for further processing """ for prop in self._propertyMap(): name = prop['id'] if name == 'debugLevel': value = REQUEST.get(name, '') self.setDebugLevel(value) return XSLTMethod.inheritedAttribute("manage_editProperties")(self, REQUEST) ################################################################ # Methods implementing the IXSLTMethod interface below ################################################################ _security.declareProtected(PERM_VIEW,'isCacheFileUpToDate') def isCacheFileUpToDate(self, REQUEST): """ Return 1 if and only if self should use the cached value rather than regenerating the transformed value, and the cache manager exists, and the cache file exists. Note: this algorithm is rather simple and limited right now, but we intend to improve it over time. Many items are not taken into account today: a) XML fragments that are included into the main document via the XSLT document() function b) XSLT transformer parameters c) XSLT transformers themselves (as well as all included and imported transformers). FIXME cks 11/26/2001 """ manager = self.findCacheManager() if manager is None: return 0 srcObject = self.getXmlSourceObject() xformObject = self.getXslTransformer() # ZODB mod time. This accounts for properties of the # XSLTMethod Zope object itself, and its content, unless # its content is stored in an external file. # NOTE: this value is returned as number of seconds # since the epoch in UTC (see python time module) # # Unfortunately, the Python time module returns number of # seconds, truncated. bobobase returns microseconds. # Therefore, we must truncate by subtracting 0.5 and rounding. sourceTime = round(srcObject.bobobase_modification_time().timeTime() - 0.5) xformTime = round(xformObject.bobobase_modification_time().timeTime() - 0.5) cacheTime = manager.cacheFileTimeStamp(REQUEST.get("URL")) return ((cacheTime >= sourceTime) and (cacheTime >= xformTime)) _security.declareProtected(PERM_VIEW, 'findCacheManager') findCacheManager = findCacheManager _security.declarePublic('availableProcessors') def availableProcessors(self): """ Return names of currently available XSLT processor libraries """ proc_chooser = ProcessorChooser() return proc_chooser.processors() _security.declarePublic('processor') def processor(self): """ Obtain the object encapsulating the selected XSLT processor. """ return self._processorChooser.processorObject(self.selected_processor) _security.declarePublic('xslTransformer') def getXslTransformer(self): """ Obtain the Zope object holding the XSLT, or None if the name does not point to a valid object. """ return self.restrictedTraverse(self.xslTransformerId, None) _security.declareProtected(PERM_VIEW, 'getXmlSourceObject') def getXmlSourceObject(self): """ Retrieve the source object by using acquisition on the ID """ # Our immediate parent might be a folderish object. Keep # going up until we get to the first non folderish object ob = aq_parent(self) while ob.isPrincipiaFolderish: ob = aq_parent(ob) if self.isDebugging(): print "Requesting contents of", ob.getId() return ob _security.declareProtected(PERM_VIEW, 'transform') def transform(self, REQUEST): """ Generate result using transformer and return it as a string """ processor = self.processor() xslObject = self.getXslTransformer() xslContents = getPublishedResult("XSL transformer", xslObject, REQUEST) xslURL = xslObject.absolute_url() xmlObject = self.getXmlSourceObject() xmlContents = getPublishedResult("XML source", xmlObject, REQUEST) xmlURL = xmlObject.absolute_url() processor.setDebugLevel(self.debugLevel) return processor.transform(xmlContents, xmlURL, xslContents, xslURL, self, REQUEST) _security.declareProtected(PERM_VIEW, 'testTransform') def testTransform(self, REQUEST): """ This version is used purely for testing. It is the same as transform() but it throws exceptions. This is used for negative tests. See tests/TestXSLTMethod() """ processor = self.processor() xslObject = self.getXslTransformer() xslContents = getPublishedResult("XSL transformer", xslObject, REQUEST) xslURL = xslObject.absolute_url() xmlObject = self.getXmlSourceObject() xmlContents = getPublishedResult("XML source", xmlObject, REQUEST) xmlURL = xmlObject.absolute_url() topLevelParams = processor.getXSLParameters(self) processor.setDebugLevel(self.debugLevel) return processor.transformGuts(xmlContents, xmlURL, xslContents, xslURL, self, topLevelParams, REQUEST) _security.declareProtected(PERM_EDIT, 'setCachingOn') def setCachingOn(self, REQUEST=None): """ same as changing the property. For use in scripts or by the cache Manager """ self.caching = 'on' _security.declareProtected(PERM_EDIT, 'setCachingOff') def setCachingOff(self, REQUEST=None): """ same as changing the property. For use in scripts or by the cache Manager """ self.caching = 'off' # innocuous methods should be declared public _security.declarePublic('isCachingOn') def isCachingOn(self): """ Return true if caching is turned on """ return self.caching == 'on' # innocuous methods should be declared public _security.declarePublic('setDebugLevel') def setDebugLevel(self, value): """ Set debug level for ourselves and our underlying processor """ self.debugLevel = value # innocuous methods should be declared public _security.declarePublic('isDebugging') def isDebugging(self): """ Return true if and only if debugging is on. """ return self.debugLevel > 0 ################################################################ # Methods called from ZMI ################################################################ _security.declareProtected(PERM_EDIT, 'editTransform') def editTransform(self, xslTransformerId): """ Change transformer to be used or ID of source object """ # does the xslTransformerId point to a valid transformer? tran = self.restrictedTraverse(xslTransformerId, None) if tran is None: message = "Invalid xslTransformerId %s" % (xslTransformerId) raise Exception(message) self.xslTransformerId = xslTransformerId ################################################################ # Utilities ################################################################ _security.declarePublic('getSelf') def getSelf(self): """ Return this object. For use in DTML scripts """ return self.aq_chain[0] _security.declarePublic('onOff') def onOff(self): return ["on", "off"] _security.declarePublic('behaveLikeList') def behaveLikeList(self): """ Return list of standard zope objects this XSLTMethod can behave like """ return GeneratorRegistry.supportedMetaTypes() ################################################################ # Standard Zope stuff ################################################################ # next line is not strictly necessary, Access contents info is Anonymous by default # as opposed to all other protections that get Manager Role by default _security.setPermissionDefault(PERM_CONTENT, ('Anonymous')) _security.declareProtected(PERM_VIEW, 'index_html') # next line is not strictly necessary, Access contents info is Anonymous by default # as opposed to all other protections that get Manager Role by default _security.setPermissionDefault(PERM_VIEW, ('Anonymous')) def index_html(self, REQUEST = None, RESPONSE = None): """ Default view of rendered version of XML Document. This is called when some one types the transformation path (e.g. "aSource/aXSLTMethod") directly into the browser, rather than via DTML or a page template. We *must* use this method to pass the REQUEST parameter on, otherwise we wouldn't get it because _render_with_namespace_ is *not* called in this case. """ return self(self, REQUEST, RESPONSE) _security.declareProtected(PERM_VIEW, '__call__') def __call__(self, client=None, REQUEST=None, RESPONSE=None): """ Render self by processing its content with the named XSLT stylesheet """ rawResult = None # Check for caching if self.isCachingOn(): if self.isCacheFileUpToDate(REQUEST): manager = self.findCacheManager() if manager is not None: rawResult = manager.valueFromCache(REQUEST.get("URL")) if rawResult is None: rawResult = self.transform(REQUEST) manager = self.findCacheManager() if self.isCachingOn() and manager is not None: manager.saveToCache(REQUEST.get("URL"), rawResult) if self.behave_like == "": behave_like = self.getXmlSourceObject().meta_type else: behave_like = self.behave_like gen = GeneratorRegistry.getGenerator(behave_like) if gen is None: gen = GeneratorRegistry.getDefaultGenerator() # explicitly set the Content-Type here because calling the XML # source object or the XSL transformer object might have # changed it and the call of gen.getResult() below doesn't # guarantee that it will be changed back. if RESPONSE is not None: RESPONSE.setHeader("Content-Type", self.content_type) obj = gen.createObject(self.id, self.title, rawResult, content_type=self.content_type) if client is None: client = self return gen.getResult(obj, client, REQUEST, RESPONSE) # # isDocTemp tells Zope that we are like a Document Template, which # means that __call__ will get called with a REQUEST parameter # (which is what we want, since we grab the URL from REQUEST) # # CKS 3/22/2003 isDocTemp is no longer needed as of Zope 2.6.1, # but it might be needed by older releases, so might as well # keep it around... # isDocTemp = 1 _security.declareProtected(PERM_VIEW, '__render_with_namespace__') def __render_with_namespace__(self, namespace): """ Render with namespace namespace will be given to us by the ZPT that calls us, for example if a ZPT were to include something like the below:
replaceme
""" REQUEST = namespace["REQUEST"] RESPONSE = namespace["RESPONSE"] return self.__call__(REQUEST=REQUEST, RESPONSE=RESPONSE) ################################################################ # Support for Schema Migration ################################################################ def repair(self): """ Repair this object. This method is used for schema migration, when the class definition changes and existing instances of previous versions must be updated. """ # Nothing to repair at the moment # register security information Globals.InitializeClass(XSLTMethod)