Back to index

plone3  3.1.7
TransformEngine.py
Go to the documentation of this file.
00001 from logging import DEBUG
00002 from zope.interface import implements
00003 
00004 from AccessControl import ClassSecurityInfo
00005 from Acquisition import aq_base
00006 from Globals import InitializeClass
00007 from Globals import PersistentMapping
00008 try:
00009     from ZODB.PersistentList import PersistentList
00010 except ImportError:
00011     from persistent.list import PersistentList
00012 from OFS.Folder import Folder
00013 
00014 from Products.PageTemplates.PageTemplateFile import PageTemplateFile
00015 
00016 from Products.CMFCore.ActionProviderBase import ActionProviderBase
00017 from Products.CMFCore.permissions import ManagePortal, View
00018 from Products.CMFCore.utils import registerToolInterface
00019 from Products.CMFCore.utils import UniqueObject
00020 from Products.CMFCore.utils import getToolByName
00021 
00022 from Products.PortalTransforms.libtransforms.utils import MissingBinary
00023 from Products.PortalTransforms import transforms
00024 from Products.PortalTransforms.interfaces import iengine
00025 from Products.PortalTransforms.interfaces import idatastream
00026 from Products.PortalTransforms.interfaces import itransform
00027 from Products.PortalTransforms.interfaces import IPortalTransformsTool
00028 from Products.PortalTransforms.data import datastream
00029 from Products.PortalTransforms.chain import TransformsChain
00030 from Products.PortalTransforms.chain import chain
00031 from Products.PortalTransforms.cache import Cache
00032 from Products.PortalTransforms.Transform import Transform
00033 from Products.PortalTransforms.utils import log
00034 from Products.PortalTransforms.utils import TransformException
00035 from Products.PortalTransforms.utils import _www
00036 
00037 class TransformTool(UniqueObject, ActionProviderBase, Folder):
00038 
00039     id = 'portal_transforms'
00040     meta_type = id.title().replace('_', ' ')
00041     isPrincipiaFolderish = 1 # Show up in the ZMI
00042 
00043     __implements__ = iengine
00044     implements(IPortalTransformsTool)
00045 
00046     meta_types = all_meta_types = (
00047         { 'name'   : 'Transform',
00048           'action' : 'manage_addTransformForm'},
00049         { 'name'   : 'TransformsChain',
00050           'action' : 'manage_addTransformsChainForm'},
00051         )
00052 
00053     manage_addTransformForm = PageTemplateFile('addTransform', _www)
00054     manage_addTransformsChainForm = PageTemplateFile('addTransformsChain', _www)
00055     manage_cacheForm = PageTemplateFile('setCacheTime', _www)
00056     manage_editTransformationPolicyForm = PageTemplateFile('editTransformationPolicy', _www)
00057     manage_reloadAllTransforms = PageTemplateFile('reloadAllTransforms', _www)
00058 
00059     manage_options = ((Folder.manage_options[0],) + Folder.manage_options[2:] +
00060                       (
00061         { 'label'   : 'Caches',
00062           'action' : 'manage_cacheForm'},
00063         { 'label'   : 'Policy',
00064           'action' : 'manage_editTransformationPolicyForm'},
00065         { 'label'   : 'Reload transforms',
00066           'action' : 'manage_reloadAllTransforms'},
00067         )
00068                       )
00069 
00070     security = ClassSecurityInfo()
00071 
00072     def __init__(self, policies=None, max_sec_in_cache=3600):
00073         self._mtmap = PersistentMapping()
00074         self._policies = policies or PersistentMapping()
00075         self.max_sec_in_cache = max_sec_in_cache
00076         self._new_style_pt = 1
00077 
00078     # mimetype oriented conversions (iengine interface) ########################
00079 
00080     def unregisterTransform(self, name):
00081         """ unregister a transform
00082         name is the name of a registered transform
00083         """
00084         self._unmapTransform(getattr(self, name))
00085         if name in self.objectIds():
00086             self._delObject(name)
00087 
00088 
00089     def convertTo(self, target_mimetype, orig, data=None, object=None,
00090                   usedby=None, context=None, **kwargs):
00091         """Convert orig to a given mimetype
00092 
00093         * orig is an encoded string
00094 
00095         * data an optional idatastream object. If None a new datastream will be
00096         created and returned
00097 
00098         * optional object argument is the object on which is bound the data.
00099         If present that object will be used by the engine to bound cached data.
00100 
00101         * additional arguments (kwargs) will be passed to the transformations.
00102         Some usual arguments are : filename, mimetype, encoding
00103 
00104         return an object implementing idatastream or None if no path has been
00105         found.
00106         """
00107         target_mimetype = str(target_mimetype)
00108 
00109         if object is not None:
00110             cache = Cache(object)
00111             data = cache.getCache(target_mimetype)
00112             if data is not None:
00113                 time, data = data
00114                 if self.max_sec_in_cache == 0 or time < self.max_sec_in_cache:
00115                     return data
00116 
00117         if data is None:
00118             data = self._wrap(target_mimetype)
00119 
00120         registry = getToolByName(self, 'mimetypes_registry')
00121 
00122         if not getattr(aq_base(registry), 'classify', None):
00123             # avoid problems when importing a site with an old mimetype registry
00124             # XXX return None or orig?
00125             return None
00126 
00127         orig_mt = registry.classify(orig,
00128                                     mimetype=kwargs.get('mimetype'),
00129                                     filename=kwargs.get('filename'))
00130         orig_mt = str(orig_mt)
00131         if not orig_mt:
00132             log('Unable to guess input mime type (filename=%s, mimetype=%s)' %(
00133                 kwargs.get('mimetype'), kwargs.get('filename')), severity=DEBUG)
00134             return None
00135 
00136         target_mt = registry.lookup(target_mimetype)
00137         if target_mt:
00138             target_mt = target_mt[0]
00139         else:
00140             log('Unable to match target mime type %s'% str(target_mimetype),
00141                 severity=DEBUG)
00142             return None
00143 
00144         ## fastpath
00145         # If orig_mt and target_mt are the same, we only allow
00146         # a one-hop transform, a.k.a. filter.
00147         # XXX disabled filtering for now
00148         filter_only = False
00149         if orig_mt == str(target_mt):
00150             filter_only = True
00151             data.setData(orig)
00152             md = data.getMetadata()
00153             md['mimetype'] = str(orig_mt)
00154             if object is not None:
00155                 cache.setCache(str(target_mimetype), data)
00156             return data
00157 
00158         ## get a path to output mime type
00159         requirements = self._policies.get(str(target_mt), [])
00160         path = self._findPath(orig_mt, target_mt, list(requirements))
00161         if not path and requirements:
00162             log('Unable to satisfy requirements %s' % ', '.join(requirements),
00163                 severity=DEBUG)
00164             path = self._findPath(orig_mt, target_mt)
00165 
00166         if not path:
00167             log('NO PATH FROM %s TO %s : %s' % (orig_mt, target_mimetype, path),
00168                 severity=DEBUG)
00169             return None #XXX raise TransformError
00170 
00171         if len(path) > 1:
00172             ## create a chain on the fly (sly)
00173             transform = chain()
00174             for t in path:
00175                 transform.registerTransform(t)
00176         else:
00177             transform = path[0]
00178 
00179         result = transform.convert(orig, data, context=context, usedby=usedby, **kwargs)
00180         assert(idatastream.isImplementedBy(result),
00181                'result doesn\'t implemented idatastream')
00182         self._setMetaData(result, transform)
00183 
00184         # set cache if possible
00185         if object is not None and result.isCacheable():
00186             cache.setCache(str(target_mimetype), result)
00187 
00188         # return idatastream object
00189         return result
00190 
00191     security.declarePublic('convertToData')
00192     def convertToData(self, target_mimetype, orig, data=None, object=None,
00193                       usedby=None, context=None, **kwargs):
00194         """Convert to a given mimetype and return the raw data
00195         ignoring subobjects. see convertTo for more information
00196         """
00197         data =self.convertTo(target_mimetype, orig, data, object, usedby,
00198                        context, **kwargs)
00199         if data:
00200             return data.getData()
00201         return None
00202 
00203     security.declarePublic('convert')
00204     def convert(self, name, orig, data=None, context=None, **kwargs):
00205         """run a tranform of a given name on data
00206 
00207         * name is the name of a registered transform
00208 
00209         see convertTo docstring for more info
00210         """
00211         if not data:
00212             data = self._wrap(name)
00213         try:
00214             transform = getattr(self, name)
00215         except AttributeError:
00216             raise Exception('No such transform "%s"' % name)
00217         data = transform.convert(orig, data, context=context, **kwargs)
00218         self._setMetaData(data, transform)
00219         return data
00220 
00221 
00222     def __call__(self, name, orig, data=None, context=None, **kwargs):
00223         """run a transform by its name, returning the raw data product
00224 
00225         * name is the name of a registered transform.
00226 
00227         return an encoded string.
00228         see convert docstring for more info on additional arguments.
00229         """
00230         data = self.convert(name, orig, data, context, **kwargs)
00231         return data.getData()
00232 
00233 
00234     # utilities ###############################################################
00235 
00236     def _setMetaData(self, datastream, transform):
00237         """set metadata on datastream according to the given transform
00238         (mime type and optionaly encoding)
00239         """
00240         md = datastream.getMetadata()
00241         if hasattr(transform, 'output_encoding'):
00242             md['encoding'] = transform.output_encoding
00243         md['mimetype'] = transform.output
00244 
00245     def _wrap(self, name):
00246         """wrap a data object in an icache"""
00247         return datastream(name)
00248 
00249     def _unwrap(self, data):
00250         """unwrap data from an icache"""
00251         if idatastream.isImplementedBy(data):
00252             data = data.getData()
00253         return data
00254 
00255     def _mapTransform(self, transform):
00256         """map transform to internal structures"""
00257         registry = getToolByName(self, 'mimetypes_registry')
00258         inputs = getattr(transform, 'inputs', None)
00259         if not inputs:
00260             raise TransformException('Bad transform %s : no input MIME type' %
00261                                      (transform))
00262         for i in inputs:
00263             mts = registry.lookup(i)
00264             if not mts:
00265                 msg = 'Input MIME type %r for transform %s is not registered '\
00266                       'in the MIME types registry' % (i, transform.name())
00267                 raise TransformException(msg)
00268             for mti in mts:
00269                 for mt in mti.mimetypes:
00270                     mt_in = self._mtmap.setdefault(mt, PersistentMapping())
00271                     output = getattr(transform, 'output', None)
00272                     if not output:
00273                         msg = 'Bad transform %s : no output MIME type'
00274                         raise TransformException(msg % transform.name())
00275                     mto = registry.lookup(output)
00276                     if not mto:
00277                         msg = 'Output MIME type %r for transform %s is not '\
00278                               'registered in the MIME types registry' % \
00279                               (output, transform.name())
00280                         raise TransformException(msg)
00281                     if len(mto) > 1:
00282                         msg = 'Wildcarding not allowed in transform\'s output '\
00283                               'MIME type'
00284                         raise TransformException(msg)
00285 
00286                     for mt2 in mto[0].mimetypes:
00287                         try:
00288                             if not transform in mt_in[mt2]:
00289                                 mt_in[mt2].append(transform)
00290                         except KeyError:
00291                             mt_in[mt2] = PersistentList([transform])
00292 
00293     def _unmapTransform(self, transform):
00294         """unmap transform from internal structures"""
00295         registry = getToolByName(self, 'mimetypes_registry')
00296         for i in transform.inputs:
00297             for mti in registry.lookup(i):
00298                 for mt in mti.mimetypes:
00299                     mt_in = self._mtmap.get(mt, {})
00300                     output = transform.output
00301                     mto = registry.lookup(output)
00302                     for mt2 in mto[0].mimetypes:
00303                         l = mt_in[mt2]
00304                         for i in range(len(l)):
00305                             if transform.name() == l[i].name():
00306                                 l.pop(i)
00307                                 break
00308                         else:
00309                             log('Can\'t find transform %s from %s to %s' % (
00310                                 transform.name(), mti, mt),
00311                                 severity=DEBUG)
00312 
00313     def _findPath(self, orig, target, required_transforms=()):
00314         """return the shortest path for transformation from orig mimetype to
00315         target mimetype
00316         """
00317         path = []
00318 
00319         if not self._mtmap:
00320             return None
00321 
00322         # naive algorithm :
00323         #  find all possible paths with required transforms
00324         #  take the shortest
00325         #
00326         # it should be enough since we should not have so much possible paths
00327         shortest, winner = 9999, None
00328         for path in self._getPaths(str(orig), str(target), required_transforms):
00329             if len(path) < shortest:
00330                 winner = path
00331                 shortest = len(path)
00332 
00333         return winner
00334 
00335     def _getPaths(self, orig, target, requirements, path=None, result=None):
00336         """return a all path for transformation from orig mimetype to
00337         target mimetype
00338         """
00339         if path is None:
00340             result = []
00341             path = []
00342             requirements = list(requirements)
00343         outputs = self._mtmap.get(orig)
00344         if outputs is None:
00345             return result
00346         path.append(None)
00347         for o_mt, transforms in outputs.items():
00348             for transform in transforms:
00349                 required = 0
00350                 name = transform.name()
00351                 if name in requirements:
00352                     requirements.remove(name)
00353                     required = 1
00354                 if transform in path:
00355                     # avoid infinite loop...
00356                     continue
00357                 path[-1] = transform
00358                 if o_mt == target:
00359                     if not requirements:
00360                         result.append(path[:])
00361                 else:
00362                     self._getPaths(o_mt, target, requirements, path, result)
00363                 if required:
00364                     requirements.append(name)
00365         path.pop()
00366 
00367         return result
00368 
00369     security.declarePrivate('manage_afterAdd')
00370     def manage_afterAdd(self, item, container):
00371         """ overload manage_afterAdd to finish initialization when the
00372         transform tool is added
00373         """
00374         Folder.manage_afterAdd(self, item, container)
00375         transforms.initialize(self)
00376         # XXX required?
00377         #try:
00378         #    # first initialization
00379         #    transforms.initialize(self)
00380         #except:
00381         #    # may fail on copy
00382         #    pass
00383 
00384     security.declareProtected(ManagePortal, 'manage_addTransform')
00385     def manage_addTransform(self, id, module, REQUEST=None):
00386         """ add a new transform to the tool """
00387         transform = Transform(id, module)
00388         self._setObject(id, transform)
00389         self._mapTransform(transform)
00390         if REQUEST is not None:
00391             REQUEST['RESPONSE'].redirect(self.absolute_url()+'/manage_main')
00392 
00393     security.declareProtected(ManagePortal, 'manage_addTransform')
00394     def manage_addTransformsChain(self, id, description, REQUEST=None):
00395         """ add a new transform to the tool """
00396         transform = TransformsChain(id, description)
00397         self._setObject(id, transform)
00398         self._mapTransform(transform)
00399         if REQUEST is not None:
00400             REQUEST['RESPONSE'].redirect(self.absolute_url()+'/manage_main')
00401 
00402     security.declareProtected(ManagePortal, 'manage_addTransform')
00403     def manage_setCacheValidityTime(self, seconds, REQUEST=None):
00404         """set  the lifetime of cached data in seconds"""
00405         self.max_sec_in_cache = int(seconds)
00406         if REQUEST is not None:
00407             REQUEST['RESPONSE'].redirect(self.absolute_url()+'/manage_main')
00408 
00409     security.declareProtected(ManagePortal, 'reloadTransforms')
00410     def reloadTransforms(self, ids=()):
00411         """ reload transforms with the given ids
00412         if no ids, reload all registered transforms
00413 
00414         return a list of (transform_id, transform_module) describing reloaded
00415         transforms
00416         """
00417         if not ids:
00418             ids = self.objectIds()
00419         reloaded = []
00420         for id in ids:
00421             o = getattr(self, id)
00422             o.reload()
00423             reloaded.append((id, o.module))
00424         return reloaded
00425 
00426     # Policy handling methods #################################################
00427 
00428     def manage_addPolicy(self, output_mimetype, required_transforms, REQUEST=None):
00429         """ add a policy for a given output mime types"""
00430         registry = getToolByName(self, 'mimetypes_registry')
00431         if not registry.lookup(output_mimetype):
00432             raise TransformException('Unknown MIME type')
00433         if self._policies.has_key(output_mimetype):
00434             msg = 'A policy for output %s is yet defined' % output_mimetype
00435             raise TransformException(msg)
00436 
00437         required_transforms = tuple(required_transforms)
00438         self._policies[output_mimetype] = required_transforms
00439         if REQUEST is not None:
00440             REQUEST['RESPONSE'].redirect(self.absolute_url()+'/manage_editTransformationPolicyForm')
00441 
00442     def manage_delPolicies(self, outputs, REQUEST=None):
00443         """ remove policies for given output mime types"""
00444         for mimetype in outputs:
00445             del self._policies[mimetype]
00446         if REQUEST is not None:
00447             REQUEST['RESPONSE'].redirect(self.absolute_url()+'/manage_editTransformationPolicyForm')
00448 
00449     def listPolicies(self):
00450         """ return the list of defined policies
00451 
00452         a policy is a 2-uple (output_mime_type, [list of required transforms])
00453         """
00454         # XXXFIXME: backward compat, should be removed latter
00455         if not hasattr(self, '_policies'):
00456             self._policies = PersistentMapping()
00457         return self._policies.items()
00458 
00459     # mimetype oriented conversions (iengine interface) ########################
00460 
00461     def registerTransform(self, transform):
00462         """register a new transform
00463 
00464         transform isn't a Zope Transform (the wrapper) but the wrapped transform
00465         the persistence wrapper will be created here
00466         """
00467         # needed when call from transform.transforms.initialize which
00468         # register non zope transform
00469         module = str(transform.__module__)
00470         transform = Transform(transform.name(), module, transform)
00471         if not itransform.isImplementedBy(transform):
00472             raise TransformException('%s does not implement itransform' % transform)
00473         name = transform.name()
00474         __traceback_info__ = (name, transform)
00475         if name not in self.objectIds():
00476             self._setObject(name, transform)
00477             self._mapTransform(transform)
00478 
00479     security.declareProtected(ManagePortal, 'ZopeFind')
00480     def ZopeFind(self, *args, **kwargs):
00481         """Don't break ZopeFind feature when a transform can't be loaded
00482         """
00483         try:
00484             return Folder.ZopeFind(self, *args, **kwargs)
00485         except MissingBinary:
00486             log('ZopeFind: catched MissingBinary exception')
00487 
00488     security.declareProtected(View, 'objectItems')
00489     def objectItems(self, *args, **kwargs):
00490         """Don't break ZopeFind feature when a transform can't be loaded
00491         """
00492         try:
00493             return Folder.objectItems(self, *args, **kwargs)
00494         except MissingBinary:
00495             log('objectItems: catched MissingBinary exception')
00496             return []
00497 
00498     # available mimetypes ####################################################
00499     def listAvailableTextInputs(self):
00500         """ Returns a list of mimetypes that can be used as input for textfields
00501             by building a list of the inputs beginning with "text/" of all transforms.
00502         """
00503         available_types = []
00504         candidate_transforms = [object[1] for object in self.objectItems()]
00505         for candidate in candidate_transforms:
00506             for input in candidate.inputs:
00507                 if input.startswith("text/") and input not in available_types:
00508                     available_types.append(input)
00509         return available_types
00510 
00511 InitializeClass(TransformTool)
00512 registerToolInterface('portal_transforms', IPortalTransformsTool)