src/pyams_zodbbrowser/btreesupport.py
changeset 0 a02202f95e2c
equal deleted inserted replaced
-1:000000000000 0:a02202f95e2c
       
     1 #
       
     2 # Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
       
     3 # All Rights Reserved.
       
     4 #
       
     5 # This software is subject to the provisions of the Zope Public License,
       
     6 # Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
       
     7 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
       
     8 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
       
     9 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
       
    10 # FOR A PARTICULAR PURPOSE.
       
    11 #
       
    12 """
       
    13 BTrees are commonly used in the Zope world.  This modules exposes the
       
    14 contents of BTrees nicely, abstracting away the implementation details.
       
    15 
       
    16 In the DB, every BTree can be represented by more than one persistent object,
       
    17 every one of those versioned separately.  This is part of what makes BTrees
       
    18 efficient.
       
    19 
       
    20 The format of the picked BTree state is nicely documented in ZODB's source
       
    21 code, specifically, BTreeTemplate.c and BucketTemplate.c.
       
    22 """
       
    23 from pyams_utils.request import check_request
       
    24 
       
    25 __docformat__ = 'restructuredtext'
       
    26 
       
    27 
       
    28 # import standard library
       
    29 
       
    30 # import interfaces
       
    31 from pyams_zodbbrowser.interfaces import IStateInterpreter, IObjectHistory
       
    32 
       
    33 # import packages
       
    34 
       
    35 # be compatible with Zope 3.4, but prefer the modern package structure
       
    36 from pyams_utils.adapter import adapter_config
       
    37 from pyams_zodbbrowser.history import ZodbObjectHistory
       
    38 from pyams_zodbbrowser.state import GenericState
       
    39 from BTrees.OOBTree import OOBTree, OOBucket
       
    40 from zope.container.folder import Folder
       
    41 from zope.container.btree import BTreeContainer
       
    42 from zope.interface import implementer
       
    43 
       
    44 
       
    45 @adapter_config(context=OOBTree, provides=IObjectHistory)
       
    46 @implementer(IObjectHistory)
       
    47 class OOBTreeHistory(ZodbObjectHistory):
       
    48 
       
    49     def _load(self):
       
    50         # find all objects (tree and buckets) that have ever participated in
       
    51         # this OOBTree
       
    52         queue = [self._obj]
       
    53         seen = set(self._oid)
       
    54         history_of = {}
       
    55         while queue:
       
    56             obj = queue.pop(0)
       
    57             history = history_of[obj._p_oid] = ZodbObjectHistory(obj)
       
    58             for d in history:
       
    59                 state = history.loadState(d['tid'])
       
    60                 if state and len(state) > 1:
       
    61                     bucket = state[1]
       
    62                     if bucket._p_oid not in seen:
       
    63                         queue.append(bucket)
       
    64                         seen.add(bucket._p_oid)
       
    65         # merge the histories of all objects
       
    66         by_tid = {}
       
    67         for h in list(history_of.values()):
       
    68             for d in h:
       
    69                 by_tid.setdefault(d['tid'], d)
       
    70         self._history = list(by_tid.values())
       
    71         self._history.sort(key=lambda d: d['tid'], reverse=True)
       
    72         self._index_by_tid()
       
    73 
       
    74     def _lastRealChange(self, tid=None):
       
    75         return ZodbObjectHistory(self._obj).lastChange(tid)
       
    76 
       
    77     def loadStatePickle(self, tid=None):
       
    78         # lastChange would return the tid that modified self._obj or any
       
    79         # of its subobjects, thanks to the history merging done by _load.
       
    80         # We need the real last change value.
       
    81         # XXX: this is used to show the pickled size of an object.  It
       
    82         # will be misleading for BTrees if we show just the size for the
       
    83         # main BTree object while we're hiding all the individual buckets.
       
    84         return self._connection._storage.loadSerial(self._obj._p_oid,
       
    85                                                     self._lastRealChange(tid))
       
    86 
       
    87     def loadState(self, tid=None):
       
    88         # lastChange would return the tid that modified self._obj or any
       
    89         # of its subobjects, thanks to the history merging done by _load.
       
    90         # We need the real last change value.
       
    91         return self._connection.oldstate(self._obj, self._lastRealChange(tid))
       
    92 
       
    93     def rollback(self, tid):
       
    94         state = self.loadState(tid)
       
    95         if state != self.loadState():
       
    96             self._obj.__setstate__(state)
       
    97             self._obj._p_changed = True
       
    98 
       
    99         while state and len(state) > 1:
       
   100             bucket = state[1]
       
   101             bucket_history = IObjectHistory(bucket)
       
   102             state = bucket_history.loadState(tid)
       
   103             if state != bucket_history.loadState():
       
   104                 bucket.__setstate__(state)
       
   105                 bucket._p_changed = True
       
   106 
       
   107 
       
   108 @adapter_config(context=(OOBTree, tuple, None), provides=IStateInterpreter)
       
   109 @implementer(IStateInterpreter)
       
   110 class OOBTreeState(object):
       
   111     """Non-empty OOBTrees have a complicated tuple structure."""
       
   112 
       
   113     def __init__(self, type, state, tid):
       
   114         self.btree = OOBTree()
       
   115         self.btree.__setstate__(state)
       
   116         self.state = state
       
   117         # Large btrees have more than one bucket; we have to load old states
       
   118         # to all of them.  See BTreeTemplate.c and BucketTemplate.c for
       
   119         # docs of the pickled state format.
       
   120         while state and len(state) > 1:
       
   121             bucket = state[1]
       
   122             state = IObjectHistory(bucket).loadState(tid)
       
   123             # XXX this is dangerous!
       
   124             bucket.__setstate__(state)
       
   125 
       
   126         self._items = list(self.btree.items())
       
   127         self._dict = dict(self.btree)
       
   128 
       
   129         # now UNDO to avoid dangerous side effects,
       
   130         # see https://bugs.launchpad.net/zodbbrowser/+bug/487243
       
   131         state = self.state
       
   132         while state and len(state) > 1:
       
   133             bucket = state[1]
       
   134             state = IObjectHistory(bucket).loadState()
       
   135             bucket.__setstate__(state)
       
   136 
       
   137     def getError(self):
       
   138         return None
       
   139 
       
   140     def getName(self):
       
   141         return None
       
   142 
       
   143     def getParent(self):
       
   144         return None
       
   145 
       
   146     def listAttributes(self):
       
   147         return None
       
   148 
       
   149     def listItems(self):
       
   150         return self._items
       
   151 
       
   152     def asDict(self):
       
   153         return self._dict
       
   154 
       
   155 
       
   156 @adapter_config(context=(OOBTree, type(None), None), provides=IStateInterpreter)
       
   157 class EmptyOOBTreeState(OOBTreeState):
       
   158     """Empty OOBTrees pickle to None."""
       
   159 
       
   160 
       
   161 @adapter_config(context=(Folder, dict, None), provides=IStateInterpreter)
       
   162 class FolderState(GenericState):
       
   163     """Convenient access to a Folder's items"""
       
   164 
       
   165     def listItems(self):
       
   166         data = self.state.get('data')
       
   167         if not data:
       
   168             return []
       
   169         # data will be an OOBTree
       
   170         loadedstate = IObjectHistory(data).loadState(self.tid)
       
   171         registry = check_request().registry
       
   172         return registry.getMultiAdapter((data, loadedstate, self.tid),
       
   173                                         IStateInterpreter).listItems()
       
   174 
       
   175 
       
   176 @adapter_config(context=(BTreeContainer, dict, None), provides=IStateInterpreter)
       
   177 class BTreeContainerState(GenericState):
       
   178     """Convenient access to a BTreeContainer's items"""
       
   179 
       
   180     def listItems(self):
       
   181         # This is not a typo; BTreeContainer really uses
       
   182         # _SampleContainer__data, for BBB
       
   183         data = self.state.get('_SampleContainer__data')
       
   184         if not data:
       
   185             return []
       
   186         # data will be an OOBTree
       
   187         loadedstate = IObjectHistory(data).loadState(self.tid)
       
   188         registry = check_request().registry
       
   189         return registry.getMultiAdapter((data, loadedstate, self.tid),
       
   190                                         IStateInterpreter).listItems()
       
   191 
       
   192 
       
   193 @adapter_config(context=(OOBucket, tuple, None), provides=IStateInterpreter)
       
   194 class OOBucketState(GenericState):
       
   195     """A single OOBTree bucket, should you wish to look at the internals
       
   196 
       
   197     Here's the state description direct from BTrees/BucketTemplate.c::
       
   198 
       
   199      * For a set bucket (self->values is NULL), a one-tuple or two-tuple.  The
       
   200      * first element is a tuple of keys, of length self->len.  The second element
       
   201      * is the next bucket, present if and only if next is non-NULL:
       
   202      *
       
   203      *     (
       
   204      *          (keys[0], keys[1], ..., keys[len-1]),
       
   205      *          <self->next iff non-NULL>
       
   206      *     )
       
   207      *
       
   208      * For a mapping bucket (self->values is not NULL), a one-tuple or two-tuple.
       
   209      * The first element is a tuple interleaving keys and values, of length
       
   210      * 2 * self->len.  The second element is the next bucket, present iff next is
       
   211      * non-NULL:
       
   212      *
       
   213      *     (
       
   214      *          (keys[0], values[0], keys[1], values[1], ...,
       
   215      *                               keys[len-1], values[len-1]),
       
   216      *          <self->next iff non-NULL>
       
   217      *     )
       
   218 
       
   219     OOBucket is a mapping bucket; OOSet is a set bucket.
       
   220     """
       
   221 
       
   222     def getError(self):
       
   223         return None
       
   224 
       
   225     def getName(self):
       
   226         return None
       
   227 
       
   228     def getParent(self):
       
   229         return None
       
   230 
       
   231     def listAttributes(self):
       
   232         return [('_next', self.state[1] if len(self.state) > 1 else None)]
       
   233 
       
   234     def listItems(self):
       
   235         return list(zip(self.state[0][::2], self.state[0][1::2]))
       
   236 
       
   237     def asDict(self):
       
   238         return dict(self.listAttributes(), _items=dict(self.listItems()))