Back to index

plone3  3.1.7
ArchivistTool.py
Go to the documentation of this file.
00001 #########################################################################
00002 # Copyright (c) 2004, 2005 Alberto Berti, Gregoire Weber. 
00003 # All Rights Reserved.
00004 # 
00005 # This file is part of CMFEditions.
00006 # 
00007 # CMFEditions is free software; you can redistribute it and/or modify
00008 # it under the terms of the GNU General Public License as published by
00009 # the Free Software Foundation; either version 2 of the License, or
00010 # (at your option) any later version.
00011 # 
00012 # CMFEditions is distributed in the hope that it will be useful,
00013 # but WITHOUT ANY WARRANTY; without even the implied warranty of
00014 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00015 # GNU General Public License for more details.
00016 # 
00017 # You should have received a copy of the GNU General Public License
00018 # along with CMFEditions; if not, write to the Free Software
00019 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
00020 #########################################################################
00021 """Archivist implementation
00022 
00023 $Id: ArchivistTool.py,v 1.15 2005/06/24 11:34:08 gregweb Exp $
00024 """
00025 
00026 import time
00027 from StringIO import StringIO
00028 from cPickle import Pickler, Unpickler
00029 from zope.interface import implements, alsoProvides
00030 
00031 from Globals import InitializeClass
00032 from Persistence import Persistent
00033 from Acquisition import aq_base, aq_parent, aq_inner
00034 from AccessControl import ClassSecurityInfo, getSecurityManager
00035 from OFS.SimpleItem import SimpleItem
00036 
00037 from Products.CMFCore.utils import UniqueObject, getToolByName
00038 
00039 from Products.CMFEditions.utilities import KwAsAttributes
00040 from Products.CMFEditions.utilities import dereference
00041 from Products.CMFEditions.interfaces.IStorage import StorageRetrieveError
00042 from Products.CMFEditions.interfaces.IStorage import StorageUnregisteredError
00043 
00044 from Products.CMFEditions.interfaces import IArchivistTool
00045 from Products.CMFEditions.interfaces.IArchivist import IArchivist
00046 from Products.CMFEditions.interfaces.IArchivist import IPurgeSupport
00047 from Products.CMFEditions.interfaces.IArchivist import IHistory
00048 from Products.CMFEditions.interfaces.IArchivist import IVersionData
00049 from Products.CMFEditions.interfaces.IArchivist import IPreparedObject
00050 from Products.CMFEditions.interfaces.IArchivist import IAttributeAdapter
00051 from Products.CMFEditions.interfaces.IArchivist import IVersionAwareReference
00052 from Products.CMFEditions.interfaces.IArchivist import IObjectData
00053 
00054 from Products.CMFEditions.interfaces.IArchivist import ArchivistError
00055 from Products.CMFEditions.interfaces.IArchivist import ArchivistSaveError
00056 from Products.CMFEditions.interfaces.IArchivist import ArchivistRetrieveError
00057 from Products.CMFEditions.interfaces.IArchivist import ArchivistUnregisteredError
00058 from Products.CMFEditions.interfaces import IVersioned
00059 
00060 RETRIEVING_UNREGISTERED_FAILED = \
00061     "Retrieving a version of an unregistered object is not possible. " \
00062     "Register the object '%s' first. "
00063 
00064 def deepcopy(obj):
00065     """Makes a deep copy of the object using the pickle mechanism.
00066     """
00067     stream = StringIO()
00068     p = Pickler(stream, 1)
00069     p.dump(aq_base(obj))
00070     stream.seek(0)
00071     u = Unpickler(stream)
00072     return u.load()
00073 
00074 class VersionData:
00075     """
00076     """
00077     __implements__ = (IVersionData, )
00078     
00079     def __init__(self, data, refs_to_be_deleted, attr_handling_references, 
00080                  preserved_data, metadata):
00081         self.data = data
00082         self.refs_to_be_deleted = refs_to_be_deleted
00083         self.attr_handling_references = attr_handling_references
00084         self.preserved_data = preserved_data
00085         self.sys_metadata = metadata['sys_metadata']
00086         self.app_metadata = metadata['app_metadata']
00087 
00088 
00089 class AttributeAdapter(Persistent):
00090     __implements__ = (IAttributeAdapter, )
00091 
00092     def __init__(self, parent, attr_name, type=None):
00093         self._parent = aq_base(parent)
00094         self._name = attr_name
00095         self._type = type
00096 
00097     def setAttribute(self, obj):
00098         setattr(self._parent, self._name, obj)
00099 
00100     def getAttribute(self):
00101         # The attribute may have been removed by a modifier
00102         return getattr(self._parent, self._name, None)
00103 
00104     def getAttributeName(self):
00105         return self._name
00106 
00107     def getType(self):
00108         return self._type
00109 
00110 
00111 class VersionAwareReference(Persistent):
00112     """A Reference that is version aware (and in future also location aware).
00113     """
00114     __implements__ = (IVersionAwareReference, )
00115 
00116     def __init__(self, **info):
00117         self.history_id = None
00118         self.version_id = None
00119         self.location_id = None
00120         self.info = info
00121         
00122     def setReference(self, target_obj, remove_info=True):
00123         """See IVersionAwareReference
00124         """
00125         storage = getToolByName(target_obj, 'portal_historiesstorage')
00126         
00127         # save as much information as possible
00128         # it may be that the target object is not yet registered with the 
00129         # storage (aka not under version control)
00130         target_obj, self.history_id = dereference(target_obj)
00131         if storage.isRegistered(self.history_id):
00132             self.version_id = target_obj.version_id
00133             # XXX the location id has to be gotten from the object directly
00134             self.location_id = 0 # XXX only one location possible currently
00135             # XXX store the information if the referenced working copy
00136             # was unchanged since the last checkin. In this case the 
00137             # the exact state of the referenced object may be retrieved also.
00138             # XXX we really need a isUpToDate/isChanged methods!
00139             
00140         if remove_info and hasattr(self, 'info'):
00141             del self.info
00142 
00143 
00144 class ArchivistTool(UniqueObject, SimpleItem):
00145     """
00146     """
00147 
00148     __implements__ = (
00149         IPurgeSupport, 
00150         IArchivist,
00151         SimpleItem.__implements__,
00152     )
00153     implements(IArchivistTool)
00154 
00155     id = 'portal_archivist'
00156     alternative_id = 'portal_standard_archivist'
00157     
00158     meta_type = 'CMFEditions Portal Archivist Tool'
00159     
00160     # make interfaces, exceptions and classes available through the tool
00161     interfaces = KwAsAttributes(
00162         IVersionData=IVersionData,
00163         IVersionAwareReference=IVersionAwareReference,
00164         IAttributeAdapter=IAttributeAdapter,
00165     )
00166     exceptions = KwAsAttributes(
00167         ArchivistError=ArchivistError,
00168     )
00169     classes = KwAsAttributes(
00170         VersionData=VersionData,
00171         VersionAwareReference=VersionAwareReference,
00172         AttributeAdapter=AttributeAdapter,
00173     )
00174     
00175     security = ClassSecurityInfo()
00176     
00177     
00178     # -------------------------------------------------------------------
00179     # private helper methods
00180     # -------------------------------------------------------------------
00181     def _cloneByPickle(self, obj):
00182         """Returns a deep copy of a ZODB object, loading ghosts as needed.
00183         """
00184         modifier = getToolByName(self, 'portal_modifier')
00185         callbacks = modifier.getOnCloneModifiers(obj)
00186         if callbacks is not None:
00187             pers_id, pers_load, inside_orefs, outside_orefs = callbacks[0:4]
00188         else:
00189             inside_orefs, outside_orefs = (), ()
00190     
00191         stream = StringIO()
00192         p = Pickler(stream, 1)
00193         if callbacks is not None:
00194             p.persistent_id = pers_id
00195         p.dump(aq_base(obj))
00196         approxSize = stream.tell()
00197         stream.seek(0)
00198         u = Unpickler(stream)
00199         if callbacks is not None:
00200             u.persistent_load = pers_load
00201         return approxSize, u.load(), inside_orefs, outside_orefs
00202 
00203 
00204     # -------------------------------------------------------------------
00205     # methods implementing IArchivist
00206     # -------------------------------------------------------------------
00207 
00208     security.declarePrivate('prepare')
00209     def prepare(self, obj, app_metadata=None, sys_metadata={}):
00210         """See IArchivist.
00211         """
00212         storage = getToolByName(self, 'portal_historiesstorage')
00213         modifier = getToolByName(self, 'portal_modifier')
00214         
00215         obj, history_id = dereference(obj, zodb_hook=self)
00216         if storage.isRegistered(history_id):
00217             # already registered
00218             version_id = len(self.queryHistory(obj))
00219             is_registered = True
00220         else:
00221             # object isn't under version control yet
00222             # A working copy being under version control needs to have
00223             # a history_id, version_id (starts with 0) and a location_id
00224             # (the current implementation isn't able yet to handle multiple
00225             # locations. Nevertheless lets set the location id to a well
00226             # known default value)
00227             uidhandler = getToolByName(self, 'portal_historyidhandler')
00228             history_id = uidhandler.register(obj)
00229             version_id = obj.version_id = 0
00230             alsoProvides(obj, IVersioned)
00231             obj.location_id = 0
00232             is_registered = False
00233         
00234         # the hard work done here is:
00235         # 1. ask for all attributes that have to be passed to the 
00236         #    history storage by reference
00237         # 2. clone the object with some modifications
00238         # 3. modify the clone further
00239         referenced_data = modifier.getReferencedAttributes(obj)
00240         approxSize, clone, inside_orefs, outside_orefs = \
00241             self._cloneByPickle(obj)
00242         metadata, inside_crefs, outside_crefs = \
00243             modifier.beforeSaveModifier(obj, clone)
00244         
00245         # extend the ``sys_metadata`` by the metadata returned by the
00246         # ``beforeSaveModifier`` modifier
00247         sys_metadata.update(metadata)
00248         
00249         # set the version id of the clone to be saved to the repository
00250         # location_id and history_id are the same as on the working copy
00251         # and remain unchanged
00252         clone.version_id = version_id
00253         
00254         # return the prepared infos (clone, refs, etc.)
00255         clone_info = ObjectData(clone, inside_crefs, outside_crefs)
00256         obj_info = ObjectData(obj, inside_orefs, outside_orefs)
00257         return PreparedObject(history_id, obj_info, clone_info, 
00258                               referenced_data, app_metadata, 
00259                               sys_metadata, is_registered, approxSize)
00260         
00261     security.declarePrivate('register')
00262     def register(self, prepared_obj):
00263         """See IArchivist.
00264         """
00265         # only register at the storage layer if not yet registered
00266         if not prepared_obj.is_registered:
00267             storage = getToolByName(self, 'portal_historiesstorage')
00268             return storage.register(prepared_obj.history_id, 
00269                                     prepared_obj.clone, 
00270                                     prepared_obj.referenced_data,
00271                                     prepared_obj.metadata)
00272 
00273     security.declarePrivate('save')
00274     def save(self, prepared_obj, autoregister=None):
00275         """See IArchivist.
00276         """
00277         if not prepared_obj.is_registered:
00278             if autoregister:
00279                 return self.register(prepared_obj)
00280             raise ArchivistSaveError(
00281                 "Saving an unregistered object is not possible. Register "
00282                 "the object '%s' first. "% prepared_obj.original.object)
00283         
00284         storage = getToolByName(self, 'portal_historiesstorage')
00285         return storage.save(prepared_obj.history_id, 
00286                             prepared_obj.clone, 
00287                             prepared_obj.referenced_data, 
00288                             prepared_obj.metadata)
00289 
00290     # -------------------------------------------------------------------
00291     # methods implementing IPurgeSupport
00292     # -------------------------------------------------------------------
00293 
00294     security.declarePrivate('purge')
00295     def purge(self, obj=None, history_id=None, selector=None, metadata={}, 
00296               countPurged=True):
00297         """See IPurgeSupport.
00298         """
00299         storage = getToolByName(self, 'portal_historiesstorage')
00300         obj, history_id = dereference(obj, history_id, self)
00301         storage.purge(history_id, selector, metadata, countPurged)
00302 
00303     security.declarePrivate('retrieve')
00304     def retrieve(self, obj=None, history_id=None, selector=None, preserve=(),
00305                  countPurged=True):
00306         """See IPurgeSupport.
00307         """
00308         # retrieve the object by accessing the right history entry 
00309         # (counting from the oldest version)
00310         # the histories storage called by LazyHistory knows what to do
00311         # with a None selector
00312         history = self.getHistory(obj, history_id, preserve, countPurged)
00313         try:
00314             return history[selector]
00315         except StorageRetrieveError:
00316             raise ArchivistRetrieveError(
00317                 "Retrieving of '%s' failed. Version '%s' does not exist. "
00318                 % (obj, selector))
00319     
00320     security.declarePrivate('getHistory')
00321     def getHistory(self, obj=None, history_id=None, preserve=(), 
00322                    countPurged=True):
00323         """See IPurgeSupport.
00324         """
00325         try:
00326             return LazyHistory(self, obj, history_id, preserve, countPurged)
00327         except StorageUnregisteredError:
00328             raise ArchivistUnregisteredError(
00329                 "Retrieving a version of an unregistered object is not "
00330                 "possible. Register the object '%s' first. " % obj)
00331         
00332     security.declarePrivate('queryHistory')
00333     def queryHistory(self, obj=None, history_id=None, preserve=(), default=[],
00334                      countPurged=True):
00335         """See IPurgeSupport.
00336         """
00337         try:
00338             return LazyHistory(self, obj, history_id, preserve, countPurged)
00339         except StorageUnregisteredError:
00340             return default
00341 
00342     security.declarePrivate('isUpToDate')
00343     def isUpToDate(self, obj=None, history_id=None, selector=None, 
00344                    countPurged=True):
00345         """See IPurgeSupport.
00346         """
00347         storage = getToolByName(self, 'portal_historiesstorage')
00348         obj, history_id = dereference(obj, history_id, self)
00349         if not storage.isRegistered(history_id):
00350             raise ArchivistUnregisteredError(
00351                 "The object %s is not registered" % obj)
00352         
00353         modified = storage.getModificationDate(history_id, selector,
00354                                                countPurged)
00355         return modified == obj.modified()
00356 
00357 InitializeClass(ArchivistTool)
00358 
00359 
00360 def getUserId():
00361     return getSecurityManager().getUser().getUserName()
00362 
00363 
00364 class ObjectData(Persistent):
00365     """
00366     """
00367     __implements__ = (IObjectData, )
00368     
00369     def __init__(self, obj, inside_refs=(), outside_refs=()):
00370         self.object = obj
00371         self.inside_refs = inside_refs
00372         self.outside_refs = outside_refs
00373 
00374 
00375 class PreparedObject:
00376     """
00377     """
00378     __implements__ = (IPreparedObject, )
00379     
00380     def __init__(self, history_id, original, clone, referenced_data, 
00381                  app_metadata, sys_metadata, is_registered, approxSize):
00382         
00383         # parent reference (register the parent with the unique id handler)
00384         # register with sys_metadata as there is no other possibility
00385         obj = original.object
00386         parent = aq_parent(aq_inner(obj))
00387         portal_uidhandler = getToolByName(obj, 'portal_historyidhandler')
00388         
00389         # set defaults if missing
00390         sys_metadata['comment'] = sys_metadata.get('comment', '')
00391         sys_metadata['timestamp'] = sys_metadata.get('timestamp', 
00392                                                      int(time.time()))
00393         sys_metadata['originator'] = sys_metadata.get('originator', None)
00394         sys_metadata['principal'] = getUserId()
00395         sys_metadata['approxSize'] = approxSize
00396         sys_metadata['parent'] = {
00397             'history_id': portal_uidhandler.register(parent),
00398             'version_id': getattr(parent, "version_id", None),
00399             'location_id': getattr(parent, "location_id", None),
00400         }
00401         
00402         # bundle application and system metadata in different namespaces
00403         metadata = {
00404             'sys_metadata': sys_metadata,
00405             'app_metadata': app_metadata,
00406         }
00407         
00408         self.history_id = history_id
00409         self.original = original
00410         self.clone = clone
00411         self.referenced_data = referenced_data
00412         self.metadata = metadata
00413         self.is_registered = is_registered
00414 
00415     def copyVersionIdFromClone(self):
00416         self.original.object.version_id = self.clone.object.version_id
00417 
00418 
00419 class LazyHistory:
00420     """Lazy history.
00421     """
00422     __implements__ = (IHistory, )
00423     
00424     def __init__(self, archivist, obj, history_id, preserve, countPurged):
00425         """Sets up a lazy history. 
00426         
00427         Takes an object which should be the original object in the portal, 
00428         and a history_id for the storage lookup. If the history id is 
00429         omitted then the history_id will be determined by dereferencing 
00430         the obj. If the obj is omitted, then the obj will be obtained by 
00431         dereferencing the history_id.
00432         """
00433         self._modifier = getToolByName(archivist, 'portal_modifier')
00434         storage = getToolByName(archivist, 'portal_historiesstorage')
00435         self._obj, history_id = dereference(obj, history_id, archivist)
00436         self._preserve = preserve
00437         self._history = storage.getHistory(history_id, countPurged)
00438         
00439     def __len__(self):
00440         """See IHistory
00441         """
00442         return len(self._history)
00443     
00444     def __getitem__(self, selector):
00445         """See IHistory
00446         """
00447         # To retrieve an object from the storage the following
00448         # steps have to be carried out:
00449         #
00450         # 1. get the appropriate data from the storage
00451         vdata = self._history[selector]
00452         
00453         # 2. clone the data and add the version id
00454         data = deepcopy(vdata.object)
00455         repo_clone = aq_base(data.object)
00456         
00457         # 3. the separately saved attributes need not be cloned
00458         referenced_data = vdata.referenced_data
00459         
00460         # 4. clone the metadata
00461         metadata = deepcopy(vdata.metadata)
00462         
00463         # 5. reattach the separately saved attributes
00464         self._modifier.reattachReferencedAttributes(repo_clone, 
00465                                                     referenced_data)
00466         
00467         # 6. call the after retrieve modifier
00468         refs_to_be_deleted, attr_handling_references, preserved_data = \
00469             self._modifier.afterRetrieveModifier(self._obj, repo_clone, 
00470                                                  self._preserve)
00471         
00472         return VersionData(data, refs_to_be_deleted, 
00473                            attr_handling_references, preserved_data, 
00474                            metadata)
00475 
00476     def __iter__(self):
00477         """See IHistory.
00478         """
00479         return GetItemIterator(self.__getitem__,
00480                                stopExceptions=(StorageRetrieveError,))
00481 
00482 
00483 class GetItemIterator:
00484     """Iterator object using a getitem implementation to iterate over.
00485     """
00486     def __init__(self, getItem, stopExceptions):
00487         self._getItem = getItem
00488         self._stopExceptions = stopExceptions
00489         self._pos = -1
00490 
00491     def __iter__(self):
00492         return self
00493         
00494     def next(self):
00495         self._pos += 1
00496         try:
00497             return self._getItem(self._pos)
00498         except self._stopExceptions:
00499             raise StopIteration()
00500 
00501 
00502 def object_copied(obj, event):
00503     if getattr(aq_base(obj), 'version_id', None) is not None:
00504         delattr(obj, 'version_id')