Back to index

plone3  3.1.7
CopyModifyMergeRepositoryTool.py
Go to the documentation of this file.
00001 #########################################################################
00002 # Copyright (c) 2004, 2005 Alberto Berti, Gregoire Weber,
00003 # Reflab (Vincenzo Di Somma, Francesco Ciriaci, Riccardo Lemmi)
00004 # All Rights Reserved.
00005 #
00006 # This file is part of CMFEditions.
00007 #
00008 # CMFEditions is free software; you can redistribute it and/or modify
00009 # it under the terms of the GNU General Public License as published by
00010 # the Free Software Foundation; either version 2 of the License, or
00011 # (at your option) any later version.
00012 #
00013 # CMFEditions is distributed in the hope that it will be useful,
00014 # but WITHOUT ANY WARRANTY; without even the implied warranty of
00015 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00016 # GNU General Public License for more details.
00017 #
00018 # You should have received a copy of the GNU General Public License
00019 # along with CMFEditions; if not, write to the Free Software
00020 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
00021 #########################################################################
00022 """Copy Modify Merge Repository implementation.
00023 
00024 $Id: CopyModifyMergeRepositoryTool.py,v 1.20 2005/06/24 11:42:01 gregweb Exp $
00025 """
00026 
00027 import time
00028 import transaction
00029 from zope.interface import implements
00030 
00031 from Globals import InitializeClass
00032 from Acquisition import aq_base, aq_parent, aq_inner
00033 from AccessControl import ClassSecurityInfo, Unauthorized
00034 from OFS.SimpleItem import SimpleItem
00035 from BTrees.OOBTree import OOBTree
00036 
00037 from Products.CMFCore.utils import UniqueObject, getToolByName
00038 from Products.CMFCore.utils import _checkPermission
00039 
00040 from Products.CMFEditions.utilities import dereference, wrap
00041 from Products.CMFEditions.interfaces.IArchivist import ArchivistRetrieveError
00042 
00043 from Products.CMFEditions.interfaces.IRepository import ICopyModifyMergeRepository
00044 from Products.CMFEditions.interfaces.IRepository import IPurgeSupport
00045 from Products.CMFEditions.interfaces.IRepository import IRepositoryTool
00046 from Products.CMFEditions.interfaces.IRepository import RepositoryPurgeError
00047 from Products.CMFEditions.interfaces.IRepository import IContentTypeVersionPolicySupport
00048 from Products.CMFEditions.interfaces.IRepository import IVersionData
00049 from Products.CMFEditions.interfaces.IRepository import IHistory
00050 
00051 from Products.CMFEditions.interfaces.IModifier import ModifierException
00052 
00053 from Products.CMFEditions.Permissions import ApplyVersionControl
00054 from Products.CMFEditions.Permissions import SaveNewVersion
00055 from Products.CMFEditions.Permissions import PurgeVersion
00056 from Products.CMFEditions.Permissions import AccessPreviousVersions
00057 from Products.CMFEditions.Permissions import RevertToPreviousVersions
00058 from Products.CMFEditions.Permissions import ManageVersioningPolicies
00059 from Products.CMFEditions.VersionPolicies import VersionPolicy
00060 from Products.CMFEditions.utilities import STUB_OBJECT_PREFIX
00061 try:
00062     from Products.Archetypes.interfaces.referenceable import IReferenceable
00063     from Products.Archetypes.config import REFERENCE_ANNOTATION as REFERENCES_CONTAINER_NAME
00064     from Products.Archetypes.exceptions import ReferenceException
00065     WRONG_AT=False
00066 except ImportError:
00067     WRONG_AT=True
00068 
00069 VERSIONABLE_CONTENT_TYPES = []
00070 VERSION_POLICY_MAPPING = {}
00071 VERSION_POLICY_DEFS = {}
00072 HOOKS = {'add': 'setupPolicyHook',
00073          'remove': 'removePolicyHook',
00074          'enable': 'enablePolicyOnTypeHook',
00075          'disable': 'disablePolicyOnTypeHook'}
00076 
00077 class CopyModifyMergeRepositoryTool(UniqueObject,
00078                                     SimpleItem):
00079 
00080     """See ICopyModifyMergeRepository
00081     """
00082 
00083     __implements__ = (
00084         SimpleItem.__implements__,
00085         IPurgeSupport, 
00086         ICopyModifyMergeRepository,
00087         IContentTypeVersionPolicySupport,
00088         )
00089 
00090     id = 'portal_repository'
00091     alternative_id = 'portal_copymergerepository'
00092 
00093     meta_type = 'CMFEditions Standard Copy Modify Merge Repository'
00094 
00095     autoapply = True
00096 
00097     security = ClassSecurityInfo()
00098 
00099     manage_options = SimpleItem.manage_options[1:]
00100 
00101     _versionable_content_types = VERSIONABLE_CONTENT_TYPES
00102     _version_policy_mapping = VERSION_POLICY_MAPPING
00103     _policy_defs = VERSION_POLICY_DEFS
00104 
00105     # Method for migrating the default dict to a per instance OOBTree,
00106     # performed on product install.
00107     def _migrateVersionPolicies(self):
00108         if not isinstance(self._policy_defs, OOBTree):
00109             btree_defs = OOBTree()
00110             for obj_id, title in self._policy_defs.items():
00111                 btree_defs[obj_id]=VersionPolicy(obj_id, title)
00112             self._policy_defs = btree_defs
00113 
00114     # -------------------------------------------------------------------
00115     # methods implementing IContentTypeVersionPolicySupport
00116     # -------------------------------------------------------------------
00117 
00118 
00119     security.declarePublic('isVersionable')
00120     def isVersionable(self, obj):
00121         """See interface.
00122         """
00123         return obj.portal_type in self.getVersionableContentTypes()
00124 
00125     security.declarePublic('getVersionableContentTypes')
00126     def getVersionableContentTypes(self):
00127         return self._versionable_content_types
00128 
00129     security.declareProtected(ManageVersioningPolicies, 'setVersionableContentTypes')
00130     def setVersionableContentTypes(self, new_content_types):
00131         self._versionable_content_types = new_content_types
00132 
00133     # XXX: There was a typo which mismatched the interface def, preserve it
00134     # for backwards compatibility
00135     security.declareProtected(ManageVersioningPolicies, 'setVersionableContentType')
00136     setVersionableContentType = setVersionableContentTypes
00137 
00138     security.declareProtected(ManageVersioningPolicies, 'addPolicyForContentType')
00139     def addPolicyForContentType(self, content_type, policy_id, **kw):
00140         assert self._policy_defs.has_key(policy_id), "Unknown policy %s"%policy_id
00141         policies = self._version_policy_mapping.copy()
00142         cur_policy = policies.setdefault(content_type, [])
00143         if policy_id not in cur_policy:
00144             cur_policy.append(policy_id)
00145             self._callPolicyHook('enable', policy_id, content_type, **kw)
00146         self._version_policy_mapping = policies
00147 
00148     security.declareProtected(ManageVersioningPolicies, 'removePolicyFromContentType')
00149     def removePolicyFromContentType(self, content_type, policy_id, **kw):
00150         policies = self._version_policy_mapping.copy()
00151         cur_policy = policies.setdefault(content_type, [])
00152         if policy_id in cur_policy:
00153             cur_policy.remove(policy_id)
00154             self._callPolicyHook('disable', policy_id, content_type, **kw)
00155         self._version_policy_mapping = policies
00156 
00157     security.declarePublic('supportsPolicy')
00158     def supportsPolicy(self, obj, policy):
00159         content_type = obj.portal_type
00160         # in 1.0alpha3 and earlier ``version_on_revert`` was 
00161         # ``version_on_rollback``. Convert to new name.
00162         config = self._version_policy_mapping.get(content_type, [])
00163         if "version_on_rollback" in config:
00164             config[config.index("version_on_rollback")] = "version_on_revert"
00165         
00166         return policy in self._version_policy_mapping.get(content_type, [])
00167 
00168     security.declarePublic('hasPolicy')
00169     def hasPolicy(self, obj):
00170         content_type = obj.portal_type
00171         return bool(self._version_policy_mapping.get(content_type, None))
00172 
00173     security.declareProtected(ManageVersioningPolicies, 'manage_setTypePolicies')
00174     def manage_setTypePolicies(self, policy_map, **kw):
00175         assert isinstance(policy_map, dict)
00176         for p_type, policies in self._version_policy_mapping.items():
00177             for policy_id in policies:
00178                 self.removePolicyFromContentType(p_type, policy_id, **kw)
00179         for p_type, policies in policy_map.items():
00180             assert isinstance(policies, list), \
00181                             "Policy list for %s must be a list"%str(p_type)
00182             for policy_id in policies:
00183                 assert self._policy_defs.has_key(policy_id), \
00184                                   "Policy %s is unknown"%policy_id
00185                 self.addPolicyForContentType(p_type, policy_id, **kw)
00186 
00187     security.declarePublic('listPolicies')
00188     def listPolicies(self):
00189         # convert the internal dict into a sequence of tuples
00190         # sort on title
00191         policy_list = [(p.Title(), p) for p in self._policy_defs.values()]
00192         policy_list.sort()
00193         policy_list = [p for (title, p) in policy_list]
00194         return policy_list
00195 
00196     security.declareProtected(ManageVersioningPolicies, 'addPolicy')
00197     def addPolicy(self, policy_id, policy_title, policy_class=VersionPolicy,
00198                                                                         **kw):
00199         self._policy_defs[policy_id]=policy_class(policy_id,policy_title)
00200         self._callPolicyHook('add', policy_id, **kw)
00201 
00202     security.declareProtected(ManageVersioningPolicies, 'removePolicy')
00203     def removePolicy(self, policy_id, **kw):
00204         for p_type in self._version_policy_mapping.keys():
00205             self.removePolicyFromContentType(p_type, policy_id, **kw)
00206         self._callPolicyHook('remove', policy_id, **kw)
00207         del self._policy_defs[policy_id]
00208 
00209     security.declareProtected(ManageVersioningPolicies, 'manage_changePolicyDefs')
00210     def manage_changePolicyDefs(self, policy_list, **kwargs):
00211         # Call remove hooks for existing policies
00212         p_defs = self._policy_defs
00213         for policy_id in list(p_defs.keys()):
00214             self.removePolicy(policy_id, **kwargs)
00215         # Verify proper input formatting
00216         assert isinstance(policy_list, list) or isinstance(policy_list, tuple)
00217         for item in policy_list:
00218             assert isinstance(item, tuple), \
00219                         "List items must be tuples: %s"%str(item)
00220             assert len(item) in (2,3,4), \
00221             "Each policy definition must contain a title and id: %s"%str(item)
00222             assert isinstance(item[0], basestring), \
00223                         "Policy id must be a string: %s"%str(item[0])
00224             assert isinstance(item[1], basestring), \
00225                         "Policy title must be a string: %s"%str(item[1])
00226             # Get optional Policy class and kwargs.
00227             if len(item)>=3:
00228                 policy_class = item[2]
00229             else:
00230                 policy_class = VersionPolicy
00231             if len(item)==4:
00232                 assert isinstance(item[3], dict), \
00233                                 "Extra args for %s must be a dict"%item[0]
00234                 kw = item[3]
00235             else:
00236                 kw = kwargs
00237             # Add new policy
00238             self.addPolicy(item[0],item[1],policy_class,**kw)
00239 
00240     security.declareProtected(ManageVersioningPolicies, 'getPolicyMap')
00241     def getPolicyMap(self):
00242         return dict(self._version_policy_mapping)
00243 
00244     def _callPolicyHook(self, action, policy_id, *args, **kw):
00245         # in 1.0alpha3 and earlier ``version_on_revert`` was 
00246         # ``version_on_rollback``. Convert to new name.
00247         if policy_id == "version_on_revert":
00248             if "version_on_rollback" in self._policy_defs:
00249                 value = self._policy_defs["version_on_rollback"]
00250                 self._policy_defs["version_on_revert"] = value
00251                 del self._policy_defs["version_on_rollback"]
00252         
00253         hook = getattr(self._policy_defs[policy_id], HOOKS[action], None)
00254         if hook is not None and callable(hook):
00255             portal = getToolByName(self, 'portal_url').getPortalObject()
00256             hook(portal, *args, **kw)
00257 
00258     # -------------------------------------------------------------------
00259     # methods implementing ICopyModifyMergeRepository
00260     # -------------------------------------------------------------------
00261 
00262     security.declareProtected(ApplyVersionControl, 'setAutoApplyMode')
00263     def setAutoApplyMode(self, autoapply):
00264         """See ICopyModifyMergeRepository.
00265         """
00266         self.autoapply = autoapply
00267 
00268     security.declarePublic('ApplyVersionControl')
00269     def applyVersionControl(self, obj, comment='', metadata={}):
00270         """See ICopyModifyMergeRepository.
00271         """
00272         self._assertAuthorized(obj, ApplyVersionControl, 'applyVersionControl')
00273         sp = transaction.savepoint(optimistic=True)
00274         try:
00275             self._recursiveSave(obj, metadata,
00276                                 self._prepareSysMetadata(comment),
00277                                 autoapply=True)
00278         except ModifierException:
00279             # modifiers can abort save operations under certain conditions
00280             sp.rollback()
00281             raise
00282 
00283     security.declarePublic('save')
00284     def save(self, obj, comment='', metadata={}):
00285         """See ICopyModifyMergeRepository.
00286         """
00287         self._assertAuthorized(obj, SaveNewVersion, 'save')
00288         sp = transaction.savepoint(optimistic=True)
00289         try:
00290             self._recursiveSave(obj, metadata,
00291                                 self._prepareSysMetadata(comment),
00292                                 autoapply=self.autoapply)
00293         except ModifierException:
00294             # modifiers can abort save operations under certain conditions
00295             sp.rollback()
00296             raise
00297 
00298     # -------------------------------------------------------------------
00299     # methods implementing IPurgeSupport
00300     # -------------------------------------------------------------------
00301 
00302     security.declarePublic('purge')
00303     def purge(self, obj, selector, comment="", metadata={}, countPurged=True):
00304         """See IPurgeSupport.
00305         """
00306         self._assertAuthorized(obj, PurgeVersion, 'purge')
00307         
00308         # Trying to avoid mess with purged versions which we don't offer
00309         # support yet when passed to the repository layer due to a missing
00310         # purge policy. The problem would occure on revert and retrieve.
00311         pp = getToolByName(self, 'portal_purgepolicy', None)
00312         if pp is None:
00313             raise RepositoryPurgeError("Purging a version is not possible. "
00314                                        "Purge is only possible with a purge "
00315                                        "policy installed.")
00316 
00317         portal_archivist = getToolByName(self, 'portal_archivist')
00318         # just hand over to the archivist for the moment (recursive purging
00319         # may be implemented in a future release)
00320         metadata = {
00321             "app_metadata": metadata,
00322             "sys_metadata": self._prepareSysMetadata(comment),
00323         }
00324         portal_archivist.purge(obj=obj, selector=selector,
00325                                metadata=metadata, countPurged=countPurged)
00326 
00327     security.declarePublic('revert')
00328     def revert(self, obj, selector=None, countPurged=True):
00329         """See IPurgeSupport.
00330         """
00331         # XXX this should go away if _recursiveRetrieve is correctly implemented
00332         original_id = obj.getId()
00333 
00334         self._assertAuthorized(obj, RevertToPreviousVersions, 'revert')
00335         fixup_queue = []
00336         self._recursiveRetrieve(obj=obj, selector=selector, inplace=True, 
00337                                 fixup_queue=fixup_queue, 
00338                                 countPurged=countPurged)
00339         # XXX this should go away if _recursiveRetrieve is correctly implemented
00340         if obj.getId() != original_id:
00341             obj._setId(original_id)
00342             #parent.manage_renameObject(obj.getId(), original_id)
00343             #parent._setObject(original_id, obj, set_owner=0)
00344 
00345         # run fixups
00346         self._doInplaceFixups(fixup_queue, True)
00347 
00348     security.declarePublic('retrieve')
00349     def retrieve(self, obj, selector=None, preserve=(), countPurged=True):
00350         """See IPurgeSupport.
00351         """
00352         self._assertAuthorized(obj, AccessPreviousVersions, 'retrieve')
00353         return self._retrieve(obj, selector, preserve, countPurged)
00354 
00355     security.declarePublic('restore')
00356     def restore(self, history_id, selector, container, new_id=None, 
00357                 countPurged=True):
00358         """See IPurgeSupport.
00359         """
00360 
00361         self._assertAuthorized(container, RevertToPreviousVersions, 'revert')
00362         fixup_queue = []
00363         vdata = self._recursiveRetrieve(history_id=history_id,
00364                                         selector=selector, inplace=True,
00365                                         source=container,
00366                                         fixup_queue=fixup_queue,
00367                                         ignore_existing=True,
00368                                         countPurged=countPurged)
00369 
00370         # Set the id to the desired value
00371         orig_id = vdata.data.object.getId()
00372         if new_id and orig_id != new_id:
00373             # Make sure we have a _p_jar
00374             transaction.savepoint()
00375             container.manage_renameObject(orig_id, new_id)
00376             #parent._setObject(original_id, obj, set_owner=0)
00377 
00378         # run fixups
00379         self._doInplaceFixups(fixup_queue, True)
00380 
00381     security.declarePublic('getHistory')
00382     def getHistory(self, obj, oldestFirst=False, preserve=(), 
00383                    countPurged=True):
00384         """See IPurgeSupport.
00385         """
00386         self._assertAuthorized(obj, AccessPreviousVersions, 'getHistory')
00387         return LazyHistory(self, obj, oldestFirst, preserve, countPurged)
00388 
00389     security.declarePublic('isUpToDate')
00390     def isUpToDate(self, obj, selector=None, countPurged=True):
00391         """See IPurgeSupport.
00392         """
00393         portal_archivist = getToolByName(self, 'portal_archivist')
00394         return portal_archivist.isUpToDate(obj=obj, selector=selector,
00395                                            countPurged=countPurged)
00396 
00397 
00398     # -------------------------------------------------------------------
00399     # private helper methods
00400     # -------------------------------------------------------------------
00401 
00402     def _assertAuthorized(self, obj, permission, name=None):
00403         # We need to provide access to the repository upon the object
00404         # permissions istead of repositories method permissions.
00405         # So the repository method access is set to public and the
00406         # access is check on the object when needed.
00407         if not _checkPermission(permission, obj):
00408             raise Unauthorized(name)
00409 
00410     def _prepareSysMetadata(self, comment):
00411         return {
00412             # comment is system metadata
00413             'comment': comment,
00414             # setting a timestamp here set the same timestamp at all
00415             # recursively saved objects
00416             'timestamp': time.time(),
00417             # None means the current object is the originator of the
00418             # save or purge operation
00419             'originator': None,
00420         }
00421 
00422     def _recursiveSave(self, obj, app_metadata, sys_metadata, autoapply):
00423         # prepare the save of the originating working copy
00424         portal_archivist = getToolByName(self, 'portal_archivist')
00425         prep = portal_archivist.prepare(obj, app_metadata, sys_metadata)
00426 
00427         # set the originator of the save operation for the referenced
00428         # objects
00429         if sys_metadata['originator'] is None:
00430             clone = prep.clone.object
00431             sys_metadata['originator'] = "%s.%s.%s" % (prep.history_id,
00432                                                        clone.version_id,
00433                                                        clone.location_id, )
00434 
00435         # What comes now is the current hardcoded policy:
00436         #
00437         # - recursively save inside references, then set a version aware
00438         #   reference
00439         # - on outside references only set a version aware reference
00440         #   (if under version control)
00441         inside_refs = map(lambda original_refs, clone_refs:
00442                           (original_refs, clone_refs.getAttribute()),
00443                           prep.original.inside_refs, prep.clone.inside_refs)
00444         for orig_ref, clone_ref in inside_refs:
00445             self._recursiveSave(orig_ref, app_metadata, sys_metadata,
00446                                 autoapply)
00447             clone_ref.setReference(orig_ref, remove_info=True)
00448 
00449         outside_refs = map(lambda oref, cref: (oref, cref.getAttribute()),
00450                            prep.original.outside_refs, prep.clone.outside_refs)
00451         for orig_ref, clone_ref in outside_refs:
00452             clone_ref.setReference(orig_ref, remove_info=True)
00453 
00454         portal_archivist.save(prep, autoregister=autoapply)
00455 
00456         # just to ensure that the working copy has the correct
00457         # ``verision_id``
00458         prep.copyVersionIdFromClone()
00459 
00460     def _retrieve(self, obj, selector, preserve, countPurged):
00461         """Retrieve a former state.
00462 
00463         Puts the returned version into same context as the working copy is
00464         (to make attribute access acquirable).
00465         """
00466         # We make a savepoint so that we can undo anything that happened
00467         # during the transaction.  There may be resource managers which do not
00468         # support savepoints, those will raise errors here, which means that
00469         # retrieve and getHistory should not be used as a part of more
00470         # complex transactions.
00471         saved = transaction.savepoint()
00472         vd = self._recursiveRetrieve(obj=obj, selector=selector,
00473                                      preserve=preserve, inplace=False,
00474                                      countPurged=countPurged)
00475         saved.rollback()
00476         wrapped = wrap(vd.data.object, aq_parent(aq_inner(obj)))
00477         return VersionData(wrapped, vd.preserved_data,
00478                            vd.sys_metadata, vd.app_metadata)
00479 
00480     def _recursiveRetrieve(self, obj=None, history_id=None, selector=None, preserve=(),
00481                            inplace=False, source=None, fixup_queue=None,
00482                            ignore_existing=False, countPurged=True):
00483         """This is the real workhorse pulling objects out recursively.
00484         """
00485         portal_archivist = getToolByName(self, 'portal_archivist')
00486         portal_reffactories = getToolByName(self, 'portal_referencefactories')
00487         if ignore_existing:
00488             obj = None
00489         else:
00490             obj, history_id = dereference(obj, history_id, self)
00491 
00492         hasBeenDeleted = obj is None
00493 
00494         # CMF's invokeFactory needs the added object be traversable from
00495         # itself to the root and from the root to the itself. This is the
00496         # reason why it is necessary to replace the working copies current
00497         # state with the one of the versions state retrieved. If the
00498         # operation is not an inplace operation (retrieve instead of
00499         # revert) this has to be reversed after having recursed into the
00500         # tree.
00501         if hasBeenDeleted:
00502             # if the object to retreive doesn't have a counterpart in the tree
00503             # build a new one before retrieving an old state
00504             vdata = portal_archivist.retrieve(obj, history_id, selector, 
00505                                               preserve, countPurged)
00506             repo_clone = vdata.data.object
00507             obj = portal_reffactories.invokeFactory(repo_clone, source)
00508             hasBeenMoved = False
00509         else:
00510             if source is None:
00511                 ##### the source has to be stored with the object at save time
00512                 # I(gregweb)'m pretty sure the whole source stuff here gets
00513                 # obsolete as soon as a va_ref to the source is stored also
00514                 # XXX for now let's stick with this:
00515                 source = aq_parent(aq_inner(obj))
00516 
00517             # in the special case the object has been moved the retrieved
00518             # object has to get a new history (it's like copying back back
00519             # the object and then retrieve an old state)
00520             hasBeenMoved = portal_reffactories.hasBeenMoved(obj, source)
00521 
00522         if hasBeenMoved:
00523             if getattr(aq_base(source), obj.getId(), None) is None:
00524                 vdata = portal_archivist.retrieve(obj, history_id, selector, 
00525                                                   preserve, countPurged)
00526                 repo_clone = vdata.data.object
00527                 obj = portal_reffactories.invokeFactory(repo_clone, source)
00528             else:
00529                 # What is the desired behavior
00530                 pass
00531         
00532         vdata = portal_archivist.retrieve(obj, history_id, selector, 
00533                                           preserve, countPurged)
00534         
00535         # Replace the objects attributes retaining identity.
00536         _missing = object()
00537         attrs_to_leave = vdata.attr_handling_references
00538         for key, val in vdata.data.object.__dict__.items():
00539             if key in attrs_to_leave:
00540                 continue
00541             obj_val = getattr(aq_base(obj), key, _missing)
00542             setattr(obj, key, val)
00543 
00544         # Delete reference attributes.
00545         for ref in vdata.refs_to_be_deleted:
00546             ref.remove(permanent=inplace)
00547 
00548         # retrieve all inside refs
00549         for attr_ref in vdata.data.inside_refs:
00550             # get the referenced working copy
00551             # XXX if the working copy we're searching for was moved to
00552             # somewhere else *outside* we generate an another object with
00553             # the same history_id. Unfortunately we're not able to handle
00554             # this correctly before multi location stuff is implemented.
00555             # XXX Perhaps there is a need for a workaround!
00556             va_ref = attr_ref.getAttribute()
00557             if va_ref is None:
00558                 # a missing reference, the policy has changed,
00559                 # don't try to replace it
00560                 continue
00561             history_id = va_ref.history_id
00562 
00563             # retrieve the referenced version (always count purged versions
00564             # also!)
00565             ref_vdata= self._recursiveRetrieve(history_id=history_id,
00566                                                selector=va_ref.version_id,
00567                                                preserve=(),
00568                                                inplace=inplace,
00569                                                source=obj,
00570                                                fixup_queue=fixup_queue,
00571                                                ignore_existing=ignore_existing,
00572                                                countPurged=True)
00573 
00574             # reattach the python reference
00575             attr_ref.setAttribute(ref_vdata.data.object)
00576 
00577         # reattach all outside refs to the current working copy
00578         # XXX this is an implicit policy we can live with for now
00579         for attr_ref in vdata.data.outside_refs:
00580             va_ref = attr_ref.getAttribute()
00581             # If the attribute has been removed by a modifier, then we get
00582             # None, move on to the next ref.
00583             if va_ref is None:
00584                 continue
00585             try:
00586                 ref = dereference(history_id=va_ref.history_id, zodb_hook=self)[0]
00587             except TypeError:
00588                 ref = getattr(aq_base(obj), attr_ref.getAttributeName(), None)
00589             # If the object is not under version control just
00590             # attach the current working copy if it exists
00591             if ref is not None:
00592                 attr_ref.setAttribute(ref)
00593 
00594         # feed the fixup queue defined in revert() and retrieve() to
00595         # perform post-retrieve fixups on the object
00596         if fixup_queue is not None:
00597            fixup_queue.append(obj)
00598         return vdata
00599 
00600     def _doInplaceFixups(self, queue, inplace):
00601         """ Perform fixups to deal with implementation details
00602         (especially zodb and cmf details) which need to be done in
00603         each retrieved object."""
00604         for obj in queue:
00605             if inplace:
00606                 if not WRONG_AT:
00607                     self._fixupATReferences(obj)
00608                 self._fixIds(obj)
00609                 self._fixupCatalogData(obj)
00610 
00611     def _fixupCatalogData(self, obj):
00612         """ Reindex the object, otherwise the catalog will certainly
00613         be out of sync."""
00614         portal_catalog = getToolByName(self, 'portal_catalog')
00615         portal_catalog.reindexObject(obj)
00616         # XXX: In theory we should probably be emitting IObjectModified and
00617         # IObjectMoved events here as those are the possible consequences of a
00618         # revert. Perhaps in out current meager z2 existence we should do
00619         # obj.manage_afterRename()?  Also, should we be doing obj.reindexObject()
00620         # instead to make sure we maximally cover specialized classes which want
00621         # to handle their cataloging in special ways.
00622 
00623     def _fixupATReferences(self, obj):
00624         """Reindex AT reference data, and delete reference
00625         implementations when the target
00626         doesn't exist anymore.
00627 
00628         Deletion of references is done at the end of the
00629         recursiveRetrieve operation to avoid deleting refs to targets
00630         that will be retrieved later in the recursiveRetrive. It
00631         doesn't call refcatalog.deleteReference as that method uses
00632         brains to retrieve reference implementations. If the
00633         target doesn't exist, brains for references pointing to it
00634         do not exist either.
00635 
00636         This manually calls reference.delHook to let it finalize
00637         correctly but traps ReferenceException eventually emitted in
00638         the process and forces the deletion, because leaving the
00639         reference impl. there will leave refcatalog in an
00640         incosistent state.
00641         """
00642 
00643         if IReferenceable.isImplementedBy(obj) and hasattr(obj, REFERENCES_CONTAINER_NAME):
00644             # Delete refs if their target doesn't exists anymore
00645             ref_folder = getattr(obj, REFERENCES_CONTAINER_NAME)
00646             uid_catalog = getToolByName(self, 'uid_catalog')
00647             ref_catalog = getToolByName(self, 'reference_catalog')
00648             ref_objs = ref_folder.objectValues()
00649             for ref in ref_objs:
00650                 if not uid_catalog(UID=ref.targetUID):
00651                     try:
00652                         # at's _deleteReference passes the catalog
00653                         # itself, the source and target obj... i'm
00654                         # going to emulate it as much as i can
00655                         ref.delHook(ref_catalog, obj, None)
00656                     except ReferenceException:
00657                         pass
00658                     ref_folder.manage_delObjects(ref.getId())
00659             # then reindex references
00660             container = aq_parent(aq_inner(obj))
00661             obj._updateCatalog(container)
00662 
00663     def _fixIds(self, obj):
00664         items = getattr(obj ,'objectItems', None)
00665         if callable(items):
00666             temp_ids = []
00667             # find sub-objects whose id doesn't match the name in the container
00668             # remove them from the folder temporarily. This could probably be made
00669             # more efficient.  We assume that any id inconsistencies were created by
00670             # us, and fix accordingly.
00671             for orig_id, child in items():
00672                 real_id = child.getId()
00673                 if orig_id != real_id:
00674                     obj._delOb(orig_id)
00675                     object_list = getattr(obj, '_objects', None)
00676                     if object_list is not None:
00677                         obj._objects = tuple([o for o in object_list if o['id'] != orig_id])
00678                     temp_ids.append((real_id, child))
00679             # Make a second pass to move the objects into place if possible
00680             all_ids = list(obj.objectIds())
00681             for new_id, child in temp_ids:
00682                 if new_id not in all_ids:
00683                     # XXX: This calls child.manage_afterAdd, and it's not clear that we
00684                     # should do so, perhaps manually manipulating the _objects is the
00685                     # better way to go.
00686                     obj._setObject(new_id, child)
00687                     all_ids.append(new_id)
00688                 else:
00689                     # If we really can't add the object make the temp_id permanent
00690                     temp_id = new_id + STUB_OBJECT_PREFIX
00691                     child.id = temp_id
00692                     obj._setObject(temp_id, child)
00693                     all_ids.append(temp_id)
00694 
00695     # -------------------------------------------------------------------
00696     # diagnostics support
00697     # -------------------------------------------------------------------
00698     
00699     def createTestHierarchy(self, context):
00700         """Create a Content Test Hierarchy
00701         """
00702         # XXX to be allowed in test mode only
00703         from StorageMigrationSupport import createTestHierarchy
00704         createTestHierarchy(context)
00705 
00706 
00707 class VersionData:
00708     """
00709     """
00710     __implements__ = (IVersionData, )
00711 
00712     security = ClassSecurityInfo()
00713     security.declareObjectPublic()
00714 
00715     __allow_access_to_unprotected_subobjects__ = 1
00716 
00717     def __init__(self, object, preserved_data, sys_metadata, app_metadata):
00718         self.object = object
00719         self.preserved_data = preserved_data
00720         self.comment = sys_metadata.get('comment', '')
00721         self.metadata = app_metadata
00722         self.sys_metadata = sys_metadata
00723         # If access contents information is disabled for anonymous on the object,
00724         # then a problem arises when trying to access its attributes.  So we
00725         # need to make version_id available (if only this were Zope 3) ;)
00726         self.version_id = object.version_id
00727 
00728 
00729 class LazyHistory:
00730     """Lazy history.
00731     """
00732     __implements__ = (IHistory, )
00733 
00734     __allow_access_to_unprotected_subobjects__ = 1
00735 
00736     def __init__(self, repository, obj, oldestFirst, preserve, countPurged):
00737         archivist = getToolByName(repository, 'portal_archivist')
00738         self._repo = repository
00739         self._obj = obj
00740         self._oldestFirst = oldestFirst
00741         self._preserve = preserve
00742         self._countPurged = countPurged
00743         self._retrieve = repository._retrieve
00744         self._length = len(archivist.queryHistory(obj=obj, preserve=preserve,
00745                                                   countPurged=countPurged))
00746 
00747     def __len__(self):
00748         """See IHistory
00749         """
00750         return self._length
00751 
00752     def __getitem__(self, selector):
00753         """See IHistory
00754         """
00755         if not self._oldestFirst and selector < self._length:
00756             if selector >= 0:
00757                 selector = self._length - 1 - selector
00758             else:
00759                 selector = - (selector + 1)
00760             
00761         return self._retrieve(self._obj, selector, self._preserve, 
00762                               self._countPurged)
00763 
00764     def __iter__(self):
00765         """See IHistory.
00766         """
00767         return GetItemIterator(self.__getitem__,
00768                                stopExceptions=(ArchivistRetrieveError,))
00769 
00770 
00771 class GetItemIterator:
00772     """Iterator object using a getitem implementation to iterate over.
00773     """
00774     def __init__(self, getItem, stopExceptions):
00775         self._getItem = getItem
00776         self._stopExceptions = stopExceptions
00777         self._pos = -1
00778 
00779     def __iter__(self):
00780         return self
00781         
00782     def next(self):
00783         self._pos += 1
00784         try:
00785             return self._getItem(self._pos)
00786         except self._stopExceptions:
00787             raise StopIteration()
00788 
00789 
00790 InitializeClass(CopyModifyMergeRepositoryTool)