|
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())) |