Back to index

plone3  3.1.7
registry.py
Go to the documentation of this file.
00001 ##############################################################################
00002 #
00003 # Copyright (c) 2004 Zope Corporation and Contributors. All Rights Reserved.
00004 #
00005 # This software is subject to the provisions of the Zope Public License,
00006 # Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
00007 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
00008 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
00009 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
00010 # FOR A PARTICULAR PURPOSE.
00011 #
00012 ##############################################################################
00013 """ Classes:  ImportStepRegistry, ExportStepRegistry
00014 
00015 $Id: registry.py 84478 2008-03-05 09:33:15Z witsch $
00016 """
00017 import os
00018 from xml.sax import parseString
00019 from xml.sax.handler import ContentHandler
00020 
00021 from AccessControl import ClassSecurityInfo
00022 from Acquisition import Implicit
00023 from Globals import InitializeClass
00024 import App.Product
00025 from Products.PageTemplates.PageTemplateFile import PageTemplateFile
00026 from zope.interface import implements
00027 from warnings import warn
00028 
00029 from interfaces import BASE
00030 from interfaces import IImportStepRegistry
00031 from interfaces import IExportStepRegistry
00032 from interfaces import IToolsetRegistry
00033 from interfaces import IProfileRegistry
00034 from permissions import ManagePortal
00035 from metadata import ProfileMetadata
00036 from utils import _xmldir
00037 from utils import _getDottedName
00038 from utils import _resolveDottedName
00039 from utils import _extractDocstring
00040 from utils import _computeTopologicalSort
00041 
00042 #
00043 #   XML parser
00044 #
00045 
00046 class _HandlerBase(ContentHandler):
00047 
00048     _MARKER = object()
00049 
00050     def _extract(self, attrs, key):
00051         result = attrs.get(key, self._MARKER)
00052 
00053         if result is self._MARKER:
00054             return None
00055 
00056         return self._encode(result)
00057 
00058     def _encode(self, content):
00059         if self._encoding is None:
00060             return content
00061 
00062         return content.encode(self._encoding)
00063 
00064 
00065 class _ToolsetParser(_HandlerBase):
00066 
00067     security = ClassSecurityInfo()
00068     security.declareObjectPrivate()
00069     security.setDefaultAccess( 'deny' )
00070 
00071     def __init__( self, encoding ):
00072 
00073         self._encoding = encoding
00074         self._required = {}
00075         self._forbidden = []
00076 
00077     def startElement( self, name, attrs ):
00078 
00079         if name == 'tool-setup':
00080             pass
00081 
00082         elif name == 'forbidden':
00083 
00084             tool_id = self._extract( attrs, 'tool_id' )
00085 
00086             if tool_id not in self._forbidden:
00087                 self._forbidden.append( tool_id )
00088 
00089         elif name == 'required':
00090 
00091             tool_id = self._extract( attrs, 'tool_id' )
00092             dotted_name = self._extract( attrs, 'class' )
00093             self._required[ tool_id ] = dotted_name
00094 
00095         else:
00096             raise ValueError, 'Unknown element %s' % name
00097 
00098 InitializeClass( _ToolsetParser )
00099 
00100 class _ImportStepRegistryParser(_HandlerBase):
00101 
00102     security = ClassSecurityInfo()
00103     security.declareObjectPrivate()
00104     security.setDefaultAccess( 'deny' )
00105 
00106     def __init__( self, encoding ):
00107 
00108         self._encoding = encoding
00109         self._started = False
00110         self._pending = None
00111         self._parsed = []
00112 
00113     def startElement( self, name, attrs ):
00114 
00115         if name == 'import-steps':
00116 
00117             if self._started:
00118                 raise ValueError, 'Duplicated setup-steps element: %s' % name
00119 
00120             self._started = True
00121 
00122         elif name == 'import-step':
00123 
00124             if self._pending is not None:
00125                 raise ValueError, 'Cannot nest setup-step elements'
00126 
00127             self._pending = dict( [ ( k, self._extract( attrs, k ) )
00128                                     for k in attrs.keys() ] )
00129 
00130             self._pending[ 'dependencies' ] = []
00131 
00132         elif name == 'dependency':
00133 
00134             if not self._pending:
00135                 raise ValueError, 'Dependency outside of step'
00136 
00137             depended = self._extract( attrs, 'step' )
00138             self._pending[ 'dependencies' ].append( depended )
00139 
00140         else:
00141             raise ValueError, 'Unknown element %s' % name
00142 
00143     def characters( self, content ):
00144 
00145         if self._pending is not None:
00146             content = self._encode( content )
00147             self._pending.setdefault( 'description', [] ).append( content )
00148 
00149     def endElement(self, name):
00150 
00151         if name == 'import-steps':
00152             pass
00153 
00154         elif name == 'import-step':
00155 
00156             if self._pending is None:
00157                 raise ValueError, 'No pending step!'
00158 
00159             deps = tuple( self._pending[ 'dependencies' ] )
00160             self._pending[ 'dependencies' ] = deps
00161 
00162             desc = ''.join( self._pending[ 'description' ] )
00163             self._pending[ 'description' ] = desc
00164 
00165             self._parsed.append( self._pending )
00166             self._pending = None
00167 
00168 InitializeClass( _ImportStepRegistryParser )
00169 
00170 
00171 class _ExportStepRegistryParser(_HandlerBase):
00172 
00173     security = ClassSecurityInfo()
00174     security.declareObjectPrivate()
00175     security.setDefaultAccess( 'deny' )
00176 
00177     def __init__( self, encoding ):
00178 
00179         self._encoding = encoding
00180         self._started = False
00181         self._pending = None
00182         self._parsed = []
00183 
00184     def startElement( self, name, attrs ):
00185 
00186         if name == 'export-steps':
00187 
00188             if self._started:
00189                 raise ValueError, 'Duplicated export-steps element: %s' % name
00190 
00191             self._started = True
00192 
00193         elif name == 'export-step':
00194 
00195             if self._pending is not None:
00196                 raise ValueError, 'Cannot nest export-step elements'
00197 
00198             self._pending = dict( [ ( k, self._extract( attrs, k ) )
00199                                     for k in attrs.keys() ] )
00200 
00201         else:
00202             raise ValueError, 'Unknown element %s' % name
00203 
00204     def characters( self, content ):
00205 
00206         if self._pending is not None:
00207             content = self._encode( content )
00208             self._pending.setdefault( 'description', [] ).append( content )
00209 
00210     def endElement(self, name):
00211 
00212         if name == 'export-steps':
00213             pass
00214 
00215         elif name == 'export-step':
00216 
00217             if self._pending is None:
00218                 raise ValueError, 'No pending step!'
00219 
00220             desc = ''.join( self._pending[ 'description' ] )
00221             self._pending[ 'description' ] = desc
00222 
00223             self._parsed.append( self._pending )
00224             self._pending = None
00225 
00226 InitializeClass( _ExportStepRegistryParser )
00227 
00228 
00229 class BaseStepRegistry( Implicit ):
00230 
00231     security = ClassSecurityInfo()
00232 
00233     def __init__( self ):
00234 
00235         self.clear()
00236 
00237     security.declareProtected( ManagePortal, 'listSteps' )
00238     def listSteps( self ):
00239 
00240         """ Return a list of registered step IDs.
00241         """
00242         return self._registered.keys()
00243 
00244     security.declareProtected( ManagePortal, 'getStepMetadata' )
00245     def getStepMetadata( self, key, default=None ):
00246 
00247         """ Return a mapping of metadata for the step identified by 'key'.
00248 
00249         o Return 'default' if no such step is registered.
00250 
00251         o The 'handler' metadata is available via 'getStep'.
00252         """
00253         result = {}
00254 
00255         info = self._registered.get( key )
00256 
00257         if info is None:
00258             return default
00259 
00260         result = info.copy()
00261         result['invalid'] =  _resolveDottedName( result[ 'handler' ] ) is None
00262 
00263         return result
00264 
00265     security.declareProtected( ManagePortal, 'listStepMetadata' )
00266     def listStepMetadata( self ):
00267 
00268         """ Return a sequence of mappings describing registered steps.
00269 
00270         o Mappings will be ordered alphabetically.
00271         """
00272         step_ids = self.listSteps()
00273         step_ids.sort()
00274         return [ self.getStepMetadata( x ) for x in step_ids ]
00275 
00276     security.declareProtected( ManagePortal, 'generateXML' )
00277     def generateXML( self ):
00278 
00279         """ Return a round-trippable XML representation of the registry.
00280 
00281         o 'handler' values are serialized using their dotted names.
00282         """
00283         return self._exportTemplate()
00284 
00285     security.declarePrivate( 'getStep' )
00286     def getStep( self, key, default=None ):
00287 
00288         """ Return the I(Export|Import)Plugin registered for 'key'.
00289 
00290         o Return 'default' if no such step is registered.
00291         """
00292         marker = object()
00293         info = self._registered.get( key, marker )
00294 
00295         if info is marker:
00296             return default
00297 
00298         return _resolveDottedName( info[ 'handler' ] )
00299 
00300     security.declarePrivate( 'unregisterStep' )
00301     def unregisterStep( self, id ):
00302         del self._registered[id]
00303 
00304     security.declarePrivate( 'clear' )
00305     def clear( self ):
00306 
00307         self._registered = {}
00308 
00309     security.declarePrivate( 'parseXML' )
00310     def parseXML( self, text, encoding=None ):
00311 
00312         """ Parse 'text'.
00313         """
00314         reader = getattr( text, 'read', None )
00315 
00316         if reader is not None:
00317             text = reader()
00318 
00319         parser = self.RegistryParser( encoding )
00320         parseString( text, parser )
00321 
00322         return parser._parsed
00323 InitializeClass( BaseStepRegistry )
00324 
00325 class ImportStepRegistry( BaseStepRegistry ):
00326 
00327     """ Manage knowledge about steps to create / configure site.
00328 
00329     o Steps are composed together to define a site profile.
00330     """
00331     implements(IImportStepRegistry)
00332 
00333     security = ClassSecurityInfo()
00334     RegistryParser = _ImportStepRegistryParser
00335 
00336 
00337     security.declareProtected( ManagePortal, 'sortSteps' )
00338     def sortSteps( self ):
00339 
00340         """ Return a sequence of registered step IDs
00341 
00342         o Sequence is sorted topologically by dependency, with the dependent
00343           steps *after* the steps they depend on.
00344         """
00345         return self._computeTopologicalSort()
00346 
00347     security.declareProtected( ManagePortal, 'checkComplete' )
00348     def checkComplete( self ):
00349 
00350         """ Return a sequence of ( node, edge ) tuples for unsatisifed deps.
00351         """
00352         result = []
00353         seen = {}
00354 
00355         graph = self._computeTopologicalSort()
00356 
00357         for node in graph:
00358 
00359             dependencies = self.getStepMetadata( node )[ 'dependencies' ]
00360 
00361             for dependency in dependencies:
00362 
00363                 if seen.get( dependency ) is None:
00364                     result.append( ( node, dependency ) )
00365 
00366             seen[ node ] = 1
00367 
00368         return result
00369 
00370     security.declarePrivate( 'registerStep' )
00371     def registerStep( self
00372                     , id
00373                     , version=None
00374                     , handler=None
00375                     , dependencies=()
00376                     , title=None
00377                     , description=None
00378                     ):
00379         """ Register a setup step.
00380 
00381         o 'id' is a unique name for this step,
00382 
00383         o 'version' is a string for comparing versions, it is preferred to
00384           be a yyyy/mm/dd-ii formatted string (date plus two-digit
00385           ordinal).  when comparing two version strings, the version with
00386           the lower sort order is considered the older version.
00387 
00388           - Newer versions of a step supplant older ones.
00389 
00390           - Attempting to register an older one after a newer one results
00391             in a KeyError.
00392 
00393         o 'handler' is the dottoed name of a handler which should implement
00394            IImportPlugin.
00395 
00396         o 'dependencies' is a tuple of step ids which have to run before
00397           this step in order to be able to run at all. Registration of
00398           steps that have unmet dependencies are deferred until the
00399           dependencies have been registered.
00400 
00401         o 'title' is a one-line UI description for this step.
00402           If None, the first line of the documentation string of the handler
00403           is used, or the id if no docstring can be found.
00404 
00405         o 'description' is a one-line UI description for this step.
00406           If None, the remaining line of the documentation string of
00407           the handler is used, or default to ''.
00408         """
00409         already = self.getStepMetadata( id )
00410 
00411         if handler is None:
00412             raise ValueError, 'No handler specified'
00413 
00414         if already and already[ 'version' ] > version:
00415             raise KeyError( 'Existing registration for step %s, version %s'
00416                           % ( id, already[ 'version' ] ) )
00417 
00418         if not isinstance(handler, str):
00419             handler = _getDottedName( handler )
00420 
00421         if title is None or description is None:
00422 
00423             method = _resolveDottedName(handler)
00424             if method is None:
00425                 t,d = id, ''
00426             else:
00427                 t, d = _extractDocstring( method, id, '' )
00428 
00429             title = title or t
00430             description = description or d
00431 
00432         info = { 'id'           : id
00433                , 'version'      : version
00434                , 'handler'      : handler
00435                , 'dependencies' : dependencies
00436                , 'title'        : title
00437                , 'description'  : description
00438                }
00439 
00440         self._registered[ id ] = info
00441 
00442 
00443     #
00444     #   Helper methods
00445     #
00446     security.declarePrivate( '_computeTopologicalSort' )
00447     def _computeTopologicalSort( self ):
00448         return _computeTopologicalSort(self._registered.values())
00449 
00450 
00451     security.declarePrivate( '_exportTemplate' )
00452     _exportTemplate = PageTemplateFile( 'isrExport.xml', _xmldir )
00453 
00454 InitializeClass( ImportStepRegistry )
00455 
00456 _import_step_registry = ImportStepRegistry()
00457 
00458 class ExportStepRegistry( BaseStepRegistry ):
00459 
00460     """ Registry of known site-configuration export steps.
00461 
00462     o Each step is registered with a unique id.
00463 
00464     o When called, with the portal object passed in as an argument,
00465       the step must return a sequence of three-tuples,
00466       ( 'data', 'content_type', 'filename' ), one for each file exported
00467       by the step.
00468 
00469       - 'data' is a string containing the file data;
00470 
00471       - 'content_type' is the MIME type of the data;
00472 
00473       - 'filename' is a suggested filename for use when downloading.
00474 
00475     """
00476     implements(IExportStepRegistry)
00477 
00478     security = ClassSecurityInfo()
00479     RegistryParser = _ExportStepRegistryParser
00480 
00481     security.declarePrivate( 'registerStep' )
00482     def registerStep( self, id, handler, title=None, description=None ):
00483 
00484         """ Register an export step.
00485 
00486         o 'id' is the unique identifier for this step
00487 
00488         o 'handler' is the dottoed name of a handler which should implement
00489            IImportPlugin.
00490 
00491         o 'title' is a one-line UI description for this step.
00492           If None, the first line of the documentation string of the step
00493           is used, or the id if no docstring can be found.
00494 
00495         o 'description' is a one-line UI description for this step.
00496           If None, the remaining line of the documentation string of
00497           the step is used, or default to ''.
00498         """
00499         if not isinstance(handler, str):
00500             handler = _getDottedName( handler )
00501 
00502         if title is None or description is None:
00503 
00504             method = _resolveDottedName(handler)
00505             if method is None:
00506                 t,d = id, ''
00507             else:
00508                 t, d = _extractDocstring( method, id, '' )
00509 
00510             title = title or t
00511             description = description or d
00512 
00513         info = { 'id'           : id
00514                , 'handler'      : handler
00515                , 'title'        : title
00516                , 'description'  : description
00517                }
00518 
00519         self._registered[ id ] = info
00520 
00521 
00522     #
00523     #   Helper methods
00524     #
00525     security.declarePrivate( '_exportTemplate' )
00526     _exportTemplate = PageTemplateFile( 'esrExport.xml', _xmldir )
00527 
00528 InitializeClass( ExportStepRegistry )
00529 
00530 _export_step_registry = ExportStepRegistry()
00531 
00532 
00533 class ToolsetRegistry( Implicit ):
00534 
00535     """ Track required / forbidden tools.
00536     """
00537     implements(IToolsetRegistry)
00538 
00539     security = ClassSecurityInfo()
00540     security.setDefaultAccess( 'allow' )
00541 
00542     def __init__( self ):
00543 
00544         self.clear()
00545 
00546     #
00547     #   Toolset API
00548     #
00549     security.declareProtected( ManagePortal, 'listForbiddenTools' )
00550     def listForbiddenTools( self ):
00551 
00552         """ See IToolsetRegistry.
00553         """
00554         result = list( self._forbidden )
00555         result.sort()
00556         return result
00557 
00558     security.declareProtected( ManagePortal, 'addForbiddenTool' )
00559     def addForbiddenTool( self, tool_id ):
00560 
00561         """ See IToolsetRegistry.
00562         """
00563         if tool_id in self._forbidden:
00564             return
00565 
00566         if self._required.get( tool_id ) is not None:
00567             raise ValueError, 'Tool %s is required!' % tool_id
00568 
00569         self._forbidden.append( tool_id )
00570 
00571     security.declareProtected( ManagePortal, 'listRequiredTools' )
00572     def listRequiredTools( self ):
00573 
00574         """ See IToolsetRegistry.
00575         """
00576         result = list( self._required.keys() )
00577         result.sort()
00578         return result
00579 
00580     security.declareProtected( ManagePortal, 'getRequiredToolInfo' )
00581     def getRequiredToolInfo( self, tool_id ):
00582 
00583         """ See IToolsetRegistry.
00584         """
00585         return self._required[ tool_id ]
00586 
00587     security.declareProtected( ManagePortal, 'listRequiredToolInfo' )
00588     def listRequiredToolInfo( self ):
00589 
00590         """ See IToolsetRegistry.
00591         """
00592         return [ self.getRequiredToolInfo( x )
00593                         for x in self.listRequiredTools() ]
00594 
00595     security.declareProtected( ManagePortal, 'addRequiredTool' )
00596     def addRequiredTool( self, tool_id, dotted_name ):
00597 
00598         """ See IToolsetRegistry.
00599         """
00600         if tool_id in self._forbidden:
00601             raise ValueError, "Forbidden tool ID: %s" % tool_id
00602 
00603         self._required[ tool_id ] = { 'id' : tool_id
00604                                     , 'class' : dotted_name
00605                                     }
00606 
00607     security.declareProtected( ManagePortal, 'generateXML' )
00608     def generateXML( self ):
00609 
00610         """ Pseudo API.
00611         """
00612         return self._toolsetConfig()
00613 
00614     security.declareProtected( ManagePortal, 'parseXML' )
00615     def parseXML( self, text, encoding=None ):
00616 
00617         """ Pseudo-API
00618         """
00619         reader = getattr( text, 'read', None )
00620 
00621         if reader is not None:
00622             text = reader()
00623 
00624         parser = _ToolsetParser( encoding )
00625         parseString( text, parser )
00626 
00627         for tool_id in parser._forbidden:
00628             self.addForbiddenTool( tool_id )
00629 
00630         for tool_id, dotted_name in parser._required.items():
00631             self.addRequiredTool( tool_id, dotted_name )
00632 
00633     security.declarePrivate( 'clear' )
00634     def clear( self ):
00635 
00636         self._forbidden = []
00637         self._required = {}
00638 
00639     #
00640     #   Helper methods.
00641     #
00642     security.declarePrivate( '_toolsetConfig' )
00643     _toolsetConfig = PageTemplateFile( 'tscExport.xml'
00644                                      , _xmldir
00645                                      , __name__='toolsetConfig'
00646                                      )
00647 
00648 InitializeClass( ToolsetRegistry )
00649 
00650 
00651 class ProfileRegistry( Implicit ):
00652 
00653     """ Track registered profiles.
00654     """
00655     implements(IProfileRegistry)
00656 
00657     security = ClassSecurityInfo()
00658     security.setDefaultAccess( 'allow' )
00659 
00660     def __init__( self ):
00661 
00662         self.clear()
00663 
00664     security.declareProtected( ManagePortal, 'getProfileInfo' )
00665     def getProfileInfo( self, profile_id, for_=None ):
00666 
00667         """ See IProfileRegistry.
00668         """
00669         result = self._profile_info[ profile_id ]
00670         if for_ is not None:
00671             if not issubclass( for_, result['for'] ):
00672                 raise KeyError, profile_id
00673         return result.copy()
00674 
00675     security.declareProtected( ManagePortal, 'listProfiles' )
00676     def listProfiles( self, for_=None ):
00677 
00678         """ See IProfileRegistry.
00679         """
00680         result = []
00681         for profile_id in self._profile_ids:
00682             info = self.getProfileInfo( profile_id )
00683             if for_ is None or issubclass( for_, info['for'] ):
00684                 result.append( profile_id )
00685         return tuple( result )
00686 
00687     security.declareProtected( ManagePortal, 'listProfileInfo' )
00688     def listProfileInfo( self, for_=None ):
00689 
00690         """ See IProfileRegistry.
00691         """
00692         candidates = [ self.getProfileInfo( id )
00693                         for id in self.listProfiles() ]
00694         return [ x for x in candidates if for_ is None or x['for'] is None or
00695                  issubclass( for_, x['for'] ) ]
00696 
00697     security.declareProtected( ManagePortal, 'registerProfile' )
00698     def registerProfile( self
00699                        , name
00700                        , title
00701                        , description
00702                        , path
00703                        , product=None
00704                        , profile_type=BASE
00705                        , for_=None
00706                        ):
00707         """ See IProfileRegistry.
00708         """
00709         profile_id = '%s:%s' % (product or 'other', name)
00710         if self._profile_info.get( profile_id ) is not None:
00711             raise KeyError, 'Duplicate profile ID: %s' % profile_id
00712 
00713         self._profile_ids.append( profile_id )
00714 
00715         info = { 'id' : profile_id
00716                , 'title' : title
00717                , 'description' : description
00718                , 'path' : path
00719                , 'product' : product
00720                , 'type': profile_type
00721                , 'for': for_
00722                }
00723 
00724         metadata = ProfileMetadata(path, product=product)()
00725 
00726         version = metadata.get( 'version', None )
00727         if version is None and product is not None:
00728             prod_name = product.split('.')[-1]
00729             prod_module = getattr(App.Product.Products, prod_name, None)
00730             if prod_module is not None:
00731                 prod_path = prod_module.__path__[0]
00732 
00733                 # Retrieve version number from any suitable version.txt
00734                 for fname in ('version.txt', 'VERSION.txt', 'VERSION.TXT'):
00735                     try:
00736                         fpath = os.path.join( prod_path, fname )
00737                         fhandle = open( fpath, 'r' )
00738                         version = fhandle.read().strip()
00739                         fhandle.close()
00740                         warn('Version for profile %s taken from version.txt. '
00741                              'This is deprecated behaviour and will be '
00742                              'removed in GenericSetup 1.5: please specify '
00743                              'the version in metadata.xml.' % profile_id,
00744                              DeprecationWarning)
00745                         break
00746                     except IOError:
00747                         continue
00748 
00749             if version is not None:
00750                 metadata[ 'version' ] = version
00751 
00752         # metadata.xml description trumps ZCML description... awkward
00753         info.update( metadata )
00754 
00755         self._profile_info[ profile_id ] = info
00756 
00757     security.declarePrivate( 'clear' )
00758     def clear( self ):
00759 
00760         self._profile_info = {}
00761         self._profile_ids = []
00762 
00763 InitializeClass( ProfileRegistry )
00764 
00765 _profile_registry = ProfileRegistry()
00766 
00767