Code

- Extended getuid() to replace figure_curuserid().
[roundup.git] / roundup / backends / back_metakit.py
1 # $Id: back_metakit.py,v 1.50 2003-09-08 20:39:18 jlgijsbers Exp $
2 '''
3    Metakit backend for Roundup, originally by Gordon McMillan.
5    Notes by Richard:
7    This backend has some behaviour specific to metakit:
9     - there's no concept of an explicit "unset" in metakit, so all types
10       have some "unset" value:
12       ========= ===== ====================================================
13       Type      Value Action when fetching from mk
14       ========= ===== ====================================================
15       Strings   ''    convert to None
16       Date      0     (seconds since 1970-01-01.00:00:00) convert to None
17       Interval  ''    convert to None
18       Number    0     ambiguious :( - do nothing
19       Boolean   0     ambiguious :( - do nothing
20       Link      0     convert to None
21       Multilink []    actually, mk can handle this one ;)
22       Passowrd  ''    convert to None
23       ========= ===== ====================================================
25       The get/set routines handle these values accordingly by converting
26       to/from None where they can. The Number/Boolean types are not able
27       to handle an "unset" at all, so they default the "unset" to 0.
29     - probably a bunch of stuff that I'm not aware of yet because I haven't
30       fully read through the source. One of these days....
31 '''
32 from roundup import hyperdb, date, password, roundupdb, security
33 import metakit
34 from sessions import Sessions, OneTimeKeys
35 import re, marshal, os, sys, weakref, time, calendar
36 from roundup import indexer
37 import locking
38 from roundup.date import Range
40 _dbs = {}
42 def Database(config, journaltag=None):
43     ''' Only have a single instance of the Database class for each instance
44     '''
45     db = _dbs.get(config.DATABASE, None)
46     if db is None or db._db is None:
47         db = _Database(config, journaltag)
48         _dbs[config.DATABASE] = db
49     else:
50         db.journaltag = journaltag
51     return db
53 class _Database(hyperdb.Database, roundupdb.Database):
54     def __init__(self, config, journaltag=None):
55         self.config = config
56         self.journaltag = journaltag
57         self.classes = {}
58         self.dirty = 0
59         self.lockfile = None
60         self._db = self.__open()
61         self.indexer = Indexer(self.config.DATABASE, self._db)
62         self.sessions = Sessions(self.config)
63         self.otks = OneTimeKeys(self.config)
64         self.security = security.Security(self)
66         os.umask(0002)
68     def post_init(self):
69         if self.indexer.should_reindex():
70             self.reindex()
72     def reindex(self):
73         for klass in self.classes.values():
74             for nodeid in klass.list():
75                 klass.index(nodeid)
76         self.indexer.save_index()
78     # --- defined in ping's spec
79     def __getattr__(self, classname):
80         if classname == 'transactions':
81             return self.dirty
82         # fall back on the classes
83         return self.getclass(classname)
84     def getclass(self, classname):
85         try:
86             return self.classes[classname]
87         except KeyError:
88             raise KeyError, 'There is no class called "%s"'%classname
89     def getclasses(self):
90         return self.classes.keys()
91     # --- end of ping's spec 
93     # --- exposed methods
94     def commit(self):
95         if self.dirty:
96             self._db.commit()
97             for cl in self.classes.values():
98                 cl._commit()
99             self.indexer.save_index()
100         self.dirty = 0
101     def rollback(self):
102         if self.dirty:
103             for cl in self.classes.values():
104                 cl._rollback()
105             self._db.rollback()
106             self._db = None
107             self._db = metakit.storage(self.dbnm, 1)
108             self.hist = self._db.view('history')
109             self.tables = self._db.view('tables')
110             self.indexer.rollback()
111             self.indexer.datadb = self._db
112         self.dirty = 0
113     def clearCache(self):
114         for cl in self.classes.values():
115             cl._commit()
116     def clear(self):
117         for cl in self.classes.values():
118             cl._clear()
119     def hasnode(self, classname, nodeid):
120         return self.getclass(classname).hasnode(nodeid)
121     def pack(self, pack_before):
122         mindate = int(calendar.timegm(pack_before.get_tuple()))
123         i = 0
124         while i < len(self.hist):
125             if self.hist[i].date < mindate and self.hist[i].action != _CREATE:
126                 self.hist.delete(i)
127             else:
128                 i = i + 1
129     def addclass(self, cl):
130         self.classes[cl.classname] = cl
131         if self.tables.find(name=cl.classname) < 0:
132             self.tables.append(name=cl.classname)
133     def addjournal(self, tablenm, nodeid, action, params, creator=None,
134                 creation=None):
135         tblid = self.tables.find(name=tablenm)
136         if tblid == -1:
137             tblid = self.tables.append(name=tablenm)
138         if creator is None:
139             creator = self.getuid()
140         else:
141             try:
142                 creator = int(creator)
143             except TypeError:
144                 creator = int(self.getclass('user').lookup(creator))
145         if creation is None:
146             creation = int(time.time())
147         elif isinstance(creation, date.Date):
148             creation = int(calendar.timegm(creation.get_tuple()))
149         # tableid:I,nodeid:I,date:I,user:I,action:I,params:B
150         self.hist.append(tableid=tblid,
151                          nodeid=int(nodeid),
152                          date=creation,
153                          action=action,
154                          user = creator,
155                          params = marshal.dumps(params))
156     def getjournal(self, tablenm, nodeid):
157         rslt = []
158         tblid = self.tables.find(name=tablenm)
159         if tblid == -1:
160             return rslt
161         q = self.hist.select(tableid=tblid, nodeid=int(nodeid))
162         if len(q) == 0:
163             raise IndexError, "no history for id %s in %s" % (nodeid, tablenm)
164         i = 0
165         #userclass = self.getclass('user')
166         for row in q:
167             try:
168                 params = marshal.loads(row.params)
169             except ValueError:
170                 print "history couldn't unmarshal %r" % row.params
171                 params = {}
172             #usernm = userclass.get(str(row.user), 'username')
173             dt = date.Date(time.gmtime(row.date))
174             #rslt.append((nodeid, dt, usernm, _actionnames[row.action], params))
175             rslt.append((nodeid, dt, str(row.user), _actionnames[row.action],
176                 params))
177         return rslt
179     def destroyjournal(self, tablenm, nodeid):
180         nodeid = int(nodeid)
181         tblid = self.tables.find(name=tablenm)
182         if tblid == -1:
183             return 
184         i = 0
185         hist = self.hist
186         while i < len(hist):
187             if hist[i].tableid == tblid and hist[i].nodeid == nodeid:
188                 hist.delete(i)
189             else:
190                 i = i + 1
191         self.dirty = 1
192         
193     def close(self):
194         for cl in self.classes.values():
195             cl.db = None
196         self._db = None
197         if self.lockfile is not None:
198             locking.release_lock(self.lockfile)
199         if _dbs.has_key(self.config.DATABASE):
200             del _dbs[self.config.DATABASE]
201         if self.lockfile is not None:
202             self.lockfile.close()
203             self.lockfile = None
204         self.classes = {}
205         self.indexer = None
207     # --- internal
208     def __open(self):
209         ''' Open the metakit database
210         '''
211         # make the database dir if it doesn't exist
212         if not os.path.exists(self.config.DATABASE):
213             os.makedirs(self.config.DATABASE)
215         # figure the file names
216         self.dbnm = db = os.path.join(self.config.DATABASE, 'tracker.mk4')
217         lockfilenm = db[:-3]+'lck'
219         # get the database lock
220         self.lockfile = locking.acquire_lock(lockfilenm)
221         self.lockfile.write(str(os.getpid()))
222         self.lockfile.flush()
224         # see if the schema has changed since last db access
225         self.fastopen = 0
226         if os.path.exists(db):
227             dbtm = os.path.getmtime(db)
228             pkgnm = self.config.__name__.split('.')[0]
229             schemamod = sys.modules.get(pkgnm+'.dbinit', None)
230             if schemamod:
231                 if os.path.exists(schemamod.__file__):
232                     schematm = os.path.getmtime(schemamod.__file__)
233                     if schematm < dbtm:
234                         # found schema mod - it's older than the db
235                         self.fastopen = 1
236                 else:
237                      # can't find schemamod - must be frozen
238                     self.fastopen = 1
240         # open the db
241         db = metakit.storage(db, 1)
242         hist = db.view('history')
243         tables = db.view('tables')
244         if not self.fastopen:
245             # create the database if it's brand new
246             if not hist.structure():
247                 hist = db.getas('history[tableid:I,nodeid:I,date:I,user:I,action:I,params:B]')
248             if not tables.structure():
249                 tables = db.getas('tables[name:S]')
250             db.commit()
252         # we now have an open, initialised database
253         self.tables = tables
254         self.hist = hist
255         return db
257     def setid(self, classname, maxid):
258         ''' No-op in metakit
259         '''
260         pass
261         
262 _STRINGTYPE = type('')
263 _LISTTYPE = type([])
264 _CREATE, _SET, _RETIRE, _LINK, _UNLINK, _RESTORE = range(6)
266 _actionnames = {
267     _CREATE : 'create',
268     _SET : 'set',
269     _RETIRE : 'retire',
270     _RESTORE : 'restore',
271     _LINK : 'link',
272     _UNLINK : 'unlink',
275 _marker = []
277 _ALLOWSETTINGPRIVATEPROPS = 0
279 class Class:    
280     privateprops = None
281     def __init__(self, db, classname, **properties):
282         #self.db = weakref.proxy(db)
283         self.db = db
284         self.classname = classname
285         self.keyname = None
286         self.ruprops = properties
287         self.privateprops = { 'id' : hyperdb.String(),
288                               'activity' : hyperdb.Date(),
289                               'creation' : hyperdb.Date(),
290                               'creator'  : hyperdb.Link('user') }
292         # event -> list of callables
293         self.auditors = {'create': [], 'set': [], 'retire': [], 'restore': []}
294         self.reactors = {'create': [], 'set': [], 'retire': [], 'restore': []}
296         view = self.__getview()
297         self.maxid = 1
298         if view:
299             self.maxid = view[-1].id + 1
300         self.uncommitted = {}
301         self.rbactions = []
303         # people reach inside!!
304         self.properties = self.ruprops
305         self.db.addclass(self)
306         self.idcache = {}
308         # default is to journal changes
309         self.do_journal = 1
311     def enableJournalling(self):
312         '''Turn journalling on for this class
313         '''
314         self.do_journal = 1
316     def disableJournalling(self):
317         '''Turn journalling off for this class
318         '''
319         self.do_journal = 0
320         
321     # --- the roundup.Class methods
322     def audit(self, event, detector):
323         l = self.auditors[event]
324         if detector not in l:
325             self.auditors[event].append(detector)
326     def fireAuditors(self, action, nodeid, newvalues):
327         for audit in self.auditors[action]:
328             audit(self.db, self, nodeid, newvalues)
329     def fireReactors(self, action, nodeid, oldvalues):
330         for react in self.reactors[action]:
331             react(self.db, self, nodeid, oldvalues)
332     def react(self, event, detector):
333         l = self.reactors[event]
334         if detector not in l:
335             self.reactors[event].append(detector)
337     # --- the hyperdb.Class methods
338     def create(self, **propvalues):
339         self.fireAuditors('create', None, propvalues)
340         newid = self.create_inner(**propvalues)
341         # self.set() (called in self.create_inner()) does reactors)
342         return newid
344     def create_inner(self, **propvalues):
345         rowdict = {}
346         rowdict['id'] = newid = self.maxid
347         self.maxid += 1
348         ndx = self.getview(1).append(rowdict)
349         propvalues['#ISNEW'] = 1
350         try:
351             self.set(str(newid), **propvalues)
352         except Exception:
353             self.maxid -= 1
354             raise
355         return str(newid)
356     
357     def get(self, nodeid, propname, default=_marker, cache=1):
358         '''
359             'cache' exists for backwards compatibility, and is not used.
360         '''
362         view = self.getview()        
363         id = int(nodeid)
364         if cache == 0:
365             oldnode = self.uncommitted.get(id, None)
366             if oldnode and oldnode.has_key(propname):
367                 return oldnode[propname]
368         ndx = self.idcache.get(id, None)
369         if ndx is None:
370             ndx = view.find(id=id)
371             if ndx < 0:
372                 raise IndexError, "%s has no node %s" % (self.classname, nodeid)
373             self.idcache[id] = ndx
374         try:
375             raw = getattr(view[ndx], propname)
376         except AttributeError:
377             raise KeyError, propname
378         rutyp = self.ruprops.get(propname, None)
379         if rutyp is None:
380             rutyp = self.privateprops[propname]
381         converter = _converters.get(rutyp.__class__, None)
382         if converter:
383             raw = converter(raw)
384         return raw
385         
386     def set(self, nodeid, **propvalues):
387         isnew = 0
388         if propvalues.has_key('#ISNEW'):
389             isnew = 1
390             del propvalues['#ISNEW']
391         if not isnew:
392             self.fireAuditors('set', nodeid, propvalues)
393         if not propvalues:
394             return propvalues
395         if propvalues.has_key('id'):
396             raise KeyError, '"id" is reserved'
397         if self.db.journaltag is None:
398             raise hyperdb.DatabaseError, 'Database open read-only'
399         view = self.getview(1)
401         # node must exist & not be retired
402         id = int(nodeid)
403         ndx = view.find(id=id)
404         if ndx < 0:
405             raise IndexError, "%s has no node %s" % (self.classname, nodeid)
406         row = view[ndx]
407         if row._isdel:
408             raise IndexError, "%s has no node %s" % (self.classname, nodeid)
409         oldnode = self.uncommitted.setdefault(id, {})
410         changes = {}
412         for key, value in propvalues.items():
413             # this will raise the KeyError if the property isn't valid
414             # ... we don't use getprops() here because we only care about
415             # the writeable properties.
416             if _ALLOWSETTINGPRIVATEPROPS:
417                 prop = self.ruprops.get(key, None)
418                 if not prop:
419                     prop = self.privateprops[key]
420             else:
421                 prop = self.ruprops[key]
422             converter = _converters.get(prop.__class__, lambda v: v)
423             # if the value's the same as the existing value, no sense in
424             # doing anything
425             oldvalue = converter(getattr(row, key))
426             if  value == oldvalue:
427                 del propvalues[key]
428                 continue
429             
430             # check to make sure we're not duplicating an existing key
431             if key == self.keyname:
432                 iv = self.getindexview(1)
433                 ndx = iv.find(k=value)
434                 if ndx == -1:
435                     iv.append(k=value, i=row.id)
436                     if not isnew:
437                         ndx = iv.find(k=oldvalue)
438                         if ndx > -1:
439                             iv.delete(ndx)
440                 else:
441                     raise ValueError, 'node with key "%s" exists'%value
443             # do stuff based on the prop type
444             if isinstance(prop, hyperdb.Link):
445                 link_class = prop.classname
446                 # must be a string or None
447                 if value is not None and not isinstance(value, type('')):
448                     raise ValueError, 'property "%s" link value be a string'%(
449                         key)
450                 # Roundup sets to "unselected" by passing None
451                 if value is None:
452                     value = 0   
453                 # if it isn't a number, it's a key
454                 try:
455                     int(value)
456                 except ValueError:
457                     try:
458                         value = self.db.getclass(link_class).lookup(value)
459                     except (TypeError, KeyError):
460                         raise IndexError, 'new property "%s": %s not a %s'%(
461                             key, value, prop.classname)
463                 if (value is not None and
464                         not self.db.getclass(link_class).hasnode(value)):
465                     raise IndexError, '%s has no node %s'%(link_class, value)
467                 setattr(row, key, int(value))
468                 changes[key] = oldvalue
469                 
470                 if self.do_journal and prop.do_journal:
471                     # register the unlink with the old linked node
472                     if oldvalue:
473                         self.db.addjournal(link_class, oldvalue, _UNLINK,
474                             (self.classname, str(row.id), key))
476                     # register the link with the newly linked node
477                     if value:
478                         self.db.addjournal(link_class, value, _LINK,
479                             (self.classname, str(row.id), key))
481             elif isinstance(prop, hyperdb.Multilink):
482                 if value is not None and type(value) != _LISTTYPE:
483                     raise TypeError, 'new property "%s" not a list of ids'%key
484                 link_class = prop.classname
485                 l = []
486                 if value is None:
487                     value = []
488                 for entry in value:
489                     if type(entry) != _STRINGTYPE:
490                         raise ValueError, 'new property "%s" link value ' \
491                             'must be a string'%key
492                     # if it isn't a number, it's a key
493                     try:
494                         int(entry)
495                     except ValueError:
496                         try:
497                             entry = self.db.getclass(link_class).lookup(entry)
498                         except (TypeError, KeyError):
499                             raise IndexError, 'new property "%s": %s not a %s'%(
500                                 key, entry, prop.classname)
501                     l.append(entry)
502                 propvalues[key] = value = l
504                 # handle removals
505                 rmvd = []
506                 for id in oldvalue:
507                     if id not in value:
508                         rmvd.append(id)
509                         # register the unlink with the old linked node
510                         if self.do_journal and prop.do_journal:
511                             self.db.addjournal(link_class, id, _UNLINK,
512                                 (self.classname, str(row.id), key))
514                 # handle additions
515                 adds = []
516                 for id in value:
517                     if id not in oldvalue:
518                         if not self.db.getclass(link_class).hasnode(id):
519                             raise IndexError, '%s has no node %s'%(
520                                 link_class, id)
521                         adds.append(id)
522                         # register the link with the newly linked node
523                         if self.do_journal and prop.do_journal:
524                             self.db.addjournal(link_class, id, _LINK,
525                                 (self.classname, str(row.id), key))
527                 # perform the modifications on the actual property value
528                 sv = getattr(row, key)
529                 i = 0
530                 while i < len(sv):
531                     if str(sv[i].fid) in rmvd:
532                         sv.delete(i)
533                     else:
534                         i += 1
535                 for id in adds:
536                     sv.append(fid=int(id))
538                 # figure the journal entry
539                 l = []
540                 if adds:
541                     l.append(('+', adds))
542                 if rmvd:
543                     l.append(('-', rmvd))
544                 if l:
545                     changes[key] = tuple(l)
546                 #changes[key] = oldvalue
548                 if not rmvd and not adds:
549                     del propvalues[key]
551             elif isinstance(prop, hyperdb.String):
552                 if value is not None and type(value) != _STRINGTYPE:
553                     raise TypeError, 'new property "%s" not a string'%key
554                 if value is None:
555                     value = ''
556                 setattr(row, key, value)
557                 changes[key] = oldvalue
558                 if hasattr(prop, 'isfilename') and prop.isfilename:
559                     propvalues[key] = os.path.basename(value)
560                 if prop.indexme:
561                     self.db.indexer.add_text((self.classname, nodeid, key),
562                         value, 'text/plain')
564             elif isinstance(prop, hyperdb.Password):
565                 if value is not None and not isinstance(value, password.Password):
566                     raise TypeError, 'new property "%s" not a Password'% key
567                 if value is None:
568                     value = ''
569                 setattr(row, key, str(value))
570                 changes[key] = str(oldvalue)
571                 propvalues[key] = str(value)
573             elif isinstance(prop, hyperdb.Date):
574                 if value is not None and not isinstance(value, date.Date):
575                     raise TypeError, 'new property "%s" not a Date'% key
576                 if value is None:
577                     setattr(row, key, 0)
578                 else:
579                     setattr(row, key, int(calendar.timegm(value.get_tuple())))
580                 changes[key] = str(oldvalue)
581                 propvalues[key] = str(value)
583             elif isinstance(prop, hyperdb.Interval):
584                 if value is not None and not isinstance(value, date.Interval):
585                     raise TypeError, 'new property "%s" not an Interval'% key
586                 if value is None:
587                     setattr(row, key, '')
588                 else:
589                     # kedder: we should store interval values serialized
590                     setattr(row, key, value.serialise())
591                 changes[key] = str(oldvalue)
592                 propvalues[key] = str(value)
593  
594             elif isinstance(prop, hyperdb.Number):
595                 if value is None:
596                     value = 0
597                 try:
598                     v = int(value)
599                 except ValueError:
600                     raise TypeError, "%s (%s) is not numeric"%(key, repr(value))
601                 setattr(row, key, v)
602                 changes[key] = oldvalue
603                 propvalues[key] = value
605             elif isinstance(prop, hyperdb.Boolean):
606                 if value is None:
607                     bv = 0
608                 elif value not in (0,1):
609                     raise TypeError, "%s (%s) is not boolean"%(key, repr(value))
610                 else:
611                     bv = value 
612                 setattr(row, key, bv)
613                 changes[key] = oldvalue
614                 propvalues[key] = value
616             oldnode[key] = oldvalue
618         # nothing to do?
619         if not propvalues:
620             return propvalues
621         if not propvalues.has_key('activity'):
622             row.activity = int(time.time())
623         if isnew:
624             if not row.creation:
625                 row.creation = int(time.time())
626             if not row.creator:
627                 row.creator = self.db.getuid()
629         self.db.dirty = 1
630         if self.do_journal:
631             if isnew:
632                 self.db.addjournal(self.classname, nodeid, _CREATE, {})
633                 self.fireReactors('create', nodeid, None)
634             else:
635                 self.db.addjournal(self.classname, nodeid, _SET, changes)
636                 self.fireReactors('set', nodeid, oldnode)
638         return propvalues
639     
640     def retire(self, nodeid):
641         if self.db.journaltag is None:
642             raise hyperdb.DatabaseError, 'Database open read-only'
643         self.fireAuditors('retire', nodeid, None)
644         view = self.getview(1)
645         ndx = view.find(id=int(nodeid))
646         if ndx < 0:
647             raise KeyError, "nodeid %s not found" % nodeid
649         row = view[ndx]
650         oldvalues = self.uncommitted.setdefault(row.id, {})
651         oldval = oldvalues['_isdel'] = row._isdel
652         row._isdel = 1
654         if self.do_journal:
655             self.db.addjournal(self.classname, nodeid, _RETIRE, {})
656         if self.keyname:
657             iv = self.getindexview(1)
658             ndx = iv.find(k=getattr(row, self.keyname),i=row.id)
659             if ndx > -1:
660                 iv.delete(ndx)
661         self.db.dirty = 1
662         self.fireReactors('retire', nodeid, None)
664     def restore(self, nodeid):
665         '''Restpre a retired node.
667         Make node available for all operations like it was before retirement.
668         '''
669         if self.db.journaltag is None:
670             raise hyperdb.DatabaseError, 'Database open read-only'
672         # check if key property was overrided
673         key = self.getkey()
674         keyvalue = self.get(nodeid, key)
675         try:
676             id = self.lookup(keyvalue)
677         except KeyError:
678             pass
679         else:
680             raise KeyError, "Key property (%s) of retired node clashes with \
681                 existing one (%s)" % (key, keyvalue)
682         # Now we can safely restore node
683         self.fireAuditors('restore', nodeid, None)
684         view = self.getview(1)
685         ndx = view.find(id=int(nodeid))
686         if ndx < 0:
687             raise KeyError, "nodeid %s not found" % nodeid
689         row = view[ndx]
690         oldvalues = self.uncommitted.setdefault(row.id, {})
691         oldval = oldvalues['_isdel'] = row._isdel
692         row._isdel = 0
694         if self.do_journal:
695             self.db.addjournal(self.classname, nodeid, _RESTORE, {})
696         if self.keyname:
697             iv = self.getindexview(1)
698             ndx = iv.find(k=getattr(row, self.keyname),i=row.id)
699             if ndx > -1:
700                 iv.delete(ndx)
701         self.db.dirty = 1
702         self.fireReactors('restore', nodeid, None)
704     def is_retired(self, nodeid):
705         view = self.getview(1)
706         # node must exist & not be retired
707         id = int(nodeid)
708         ndx = view.find(id=id)
709         if ndx < 0:
710             raise IndexError, "%s has no node %s" % (self.classname, nodeid)
711         row = view[ndx]
712         return row._isdel
714     def history(self, nodeid):
715         if not self.do_journal:
716             raise ValueError, 'Journalling is disabled for this class'
717         return self.db.getjournal(self.classname, nodeid)
719     def setkey(self, propname):
720         if self.keyname:
721             if propname == self.keyname:
722                 return
723             raise ValueError, "%s already indexed on %s"%(self.classname,
724                 self.keyname)
725         prop = self.properties.get(propname, None)
726         if prop is None:
727             prop = self.privateprops.get(propname, None)
728         if prop is None:
729             raise KeyError, "no property %s" % propname
730         if not isinstance(prop, hyperdb.String):
731             raise TypeError, "%s is not a String" % propname
733         # first setkey for this run
734         self.keyname = propname
735         iv = self.db._db.view('_%s' % self.classname)
736         if self.db.fastopen and iv.structure():
737             return
739         # very first setkey ever
740         self.db.dirty = 1
741         iv = self.db._db.getas('_%s[k:S,i:I]' % self.classname)
742         iv = iv.ordered(1)
743         for row in self.getview():
744             iv.append(k=getattr(row, propname), i=row.id)
745         self.db.commit()
747     def getkey(self):
748         return self.keyname
750     def lookup(self, keyvalue):
751         if type(keyvalue) is not _STRINGTYPE:
752             raise TypeError, "%r is not a string" % keyvalue
753         iv = self.getindexview()
754         if iv:
755             ndx = iv.find(k=keyvalue)
756             if ndx > -1:
757                 return str(iv[ndx].i)
758         else:
759             view = self.getview()
760             ndx = view.find({self.keyname:keyvalue, '_isdel':0})
761             if ndx > -1:
762                 return str(view[ndx].id)
763         raise KeyError, keyvalue
765     def destroy(self, id):
766         view = self.getview(1)
767         ndx = view.find(id=int(id))
768         if ndx > -1:
769             if self.keyname:
770                 keyvalue = getattr(view[ndx], self.keyname)
771                 iv = self.getindexview(1)
772                 if iv:
773                     ivndx = iv.find(k=keyvalue)
774                     if ivndx > -1:
775                         iv.delete(ivndx)
776             view.delete(ndx)
777             self.db.destroyjournal(self.classname, id)
778             self.db.dirty = 1
779         
780     def find(self, **propspec):
781         """Get the ids of nodes in this class which link to the given nodes.
783         'propspec' consists of keyword args propname={nodeid:1,}   
784         'propname' must be the name of a property in this class, or a
785                    KeyError is raised.  That property must be a Link or
786                    Multilink property, or a TypeError is raised.
788         Any node in this class whose propname property links to any of the
789         nodeids will be returned. Used by the full text indexing, which knows
790         that "foo" occurs in msg1, msg3 and file7; so we have hits on these
791         issues:
793             db.issue.find(messages={'1':1,'3':1}, files={'7':1})
795         """
796         propspec = propspec.items()
797         for propname, nodeid in propspec:
798             # check the prop is OK
799             prop = self.ruprops[propname]
800             if (not isinstance(prop, hyperdb.Link) and
801                     not isinstance(prop, hyperdb.Multilink)):
802                 raise TypeError, "'%s' not a Link/Multilink property"%propname
804         vws = []
805         for propname, ids in propspec:
806             if type(ids) is _STRINGTYPE:
807                 ids = {int(ids):1}
808             elif ids is None:
809                 ids = {0:1}
810             else:
811                 d = {}
812                 for id in ids.keys():
813                     d[int(id)] = 1
814                 ids = d
815             prop = self.ruprops[propname]
816             view = self.getview()
817             if isinstance(prop, hyperdb.Multilink):
818                 def ff(row, nm=propname, ids=ids):
819                     sv = getattr(row, nm)
820                     for sr in sv:
821                         if ids.has_key(sr.fid):
822                             return 1
823                     return 0
824             else:
825                 def ff(row, nm=propname, ids=ids):
826                     return ids.has_key(getattr(row, nm))
827             ndxview = view.filter(ff)
828             vws.append(ndxview.unique())
830         # handle the empty match case
831         if not vws:
832             return []
834         ndxview = vws[0]
835         for v in vws[1:]:
836             ndxview = ndxview.union(v)
837         view = self.getview().remapwith(ndxview)
838         rslt = []
839         for row in view:
840             rslt.append(str(row.id))
841         return rslt
842             
844     def list(self):
845         l = []
846         for row in self.getview().select(_isdel=0):
847             l.append(str(row.id))
848         return l
850     def getnodeids(self):
851         l = []
852         for row in self.getview():
853             l.append(str(row.id))
854         return l
856     def count(self):
857         return len(self.getview())
859     def getprops(self, protected=1):
860         # protected is not in ping's spec
861         allprops = self.ruprops.copy()
862         if protected and self.privateprops is not None:
863             allprops.update(self.privateprops)
864         return allprops
866     def addprop(self, **properties):
867         for key in properties.keys():
868             if self.ruprops.has_key(key):
869                 raise ValueError, "%s is already a property of %s"%(key,
870                     self.classname)
871         self.ruprops.update(properties)
872         # Class structure has changed
873         self.db.fastopen = 0
874         view = self.__getview()
875         self.db.commit()
876     # ---- end of ping's spec
878     def filter(self, search_matches, filterspec, sort=(None,None),
879             group=(None,None)):
880         # search_matches is None or a set (dict of {nodeid: {propname:[nodeid,...]}})
881         # filterspec is a dict {propname:value}
882         # sort and group are (dir, prop) where dir is '+', '-' or None
883         #                    and prop is a prop name or None
885         timezone = self.db.getUserTimezone()
887         where = {'_isdel':0}
888         wherehigh = {}
889         mlcriteria = {}
890         regexes = {}
891         orcriteria = {}
892         for propname, value in filterspec.items():
893             prop = self.ruprops.get(propname, None)
894             if prop is None:
895                 prop = self.privateprops[propname]
896             if isinstance(prop, hyperdb.Multilink):
897                 if value in ('-1', ['-1']):
898                     value = []
899                 elif type(value) is not _LISTTYPE:
900                     value = [value]
901                 # transform keys to ids
902                 u = []
903                 for item in value:
904                     try:
905                         item = int(item)
906                     except (TypeError, ValueError):
907                         item = int(self.db.getclass(prop.classname).lookup(item))
908                     if item == -1:
909                         item = 0
910                     u.append(item)
911                 mlcriteria[propname] = u
912             elif isinstance(prop, hyperdb.Link):
913                 if type(value) is not _LISTTYPE:
914                     value = [value]
915                 # transform keys to ids
916                 u = []
917                 for item in value:
918                     try:
919                         item = int(item)
920                     except (TypeError, ValueError):
921                         item = int(self.db.getclass(prop.classname).lookup(item))
922                     if item == -1:
923                         item = 0
924                     u.append(item)
925                 if len(u) == 1:
926                     where[propname] = u[0]
927                 else:
928                     orcriteria[propname] = u
929             elif isinstance(prop, hyperdb.String):
930                 if type(value) is not type([]):
931                     value = [value]
932                 m = []
933                 for v in value:
934                     # simple glob searching
935                     v = re.sub(r'([\|\{\}\\\.\+\[\]\(\)])', r'\\\1', v)
936                     v = v.replace('?', '.')
937                     v = v.replace('*', '.*?')
938                     m.append(v)
939                 regexes[propname] = re.compile('(%s)'%('|'.join(m)), re.I)
940             elif propname == 'id':
941                 where[propname] = int(value)
942             elif isinstance(prop, hyperdb.Boolean):
943                 if type(value) is _STRINGTYPE:
944                     bv = value.lower() in ('yes', 'true', 'on', '1')
945                 else:
946                     bv = value
947                 where[propname] = bv
948             elif isinstance(prop, hyperdb.Date):
949                 try:
950                     # Try to filter on range of dates
951                     date_rng = Range(value, date.Date, offset=timezone)
952                     if date_rng.from_value:
953                         t = date_rng.from_value.get_tuple()
954                         where[propname] = int(calendar.timegm(t))
955                     else:
956                         # use minimum possible value to exclude items without
957                         # 'prop' property
958                         where[propname] = 0
959                     if date_rng.to_value:
960                         t = date_rng.to_value.get_tuple()
961                         wherehigh[propname] = int(calendar.timegm(t))
962                     else:
963                         wherehigh[propname] = None
964                 except ValueError:
965                     # If range creation fails - ignore that search parameter
966                     pass                        
967             elif isinstance(prop, hyperdb.Interval):
968                 try:
969                     # Try to filter on range of intervals
970                     date_rng = Range(value, date.Interval)
971                     if date_rng.from_value:
972                         #t = date_rng.from_value.get_tuple()
973                         where[propname] = date_rng.from_value.serialise()
974                     else:
975                         # use minimum possible value to exclude items without
976                         # 'prop' property
977                         where[propname] = '-99999999999999'
978                     if date_rng.to_value:
979                         #t = date_rng.to_value.get_tuple()
980                         wherehigh[propname] = date_rng.to_value.serialise()
981                     else:
982                         wherehigh[propname] = None
983                 except ValueError:
984                     # If range creation fails - ignore that search parameter
985                     pass                        
986             elif isinstance(prop, hyperdb.Number):
987                 where[propname] = int(value)
988             else:
989                 where[propname] = str(value)
990         v = self.getview()
991         #print "filter start at  %s" % time.time() 
992         if where:
993             where_higherbound = where.copy()
994             where_higherbound.update(wherehigh)
995             v = v.select(where, where_higherbound)
996         #print "filter where at  %s" % time.time() 
998         if mlcriteria:
999             # multilink - if any of the nodeids required by the
1000             # filterspec aren't in this node's property, then skip it
1001             def ff(row, ml=mlcriteria):
1002                 for propname, values in ml.items():
1003                     sv = getattr(row, propname)
1004                     if not values and sv:
1005                         return 0
1006                     for id in values:
1007                         if sv.find(fid=id) == -1:
1008                             return 0
1009                 return 1
1010             iv = v.filter(ff)
1011             v = v.remapwith(iv)
1013         #print "filter mlcrit at %s" % time.time() 
1014         
1015         if orcriteria:
1016             def ff(row, crit=orcriteria):
1017                 for propname, allowed in crit.items():
1018                     val = getattr(row, propname)
1019                     if val not in allowed:
1020                         return 0
1021                 return 1
1022             
1023             iv = v.filter(ff)
1024             v = v.remapwith(iv)
1025         
1026         #print "filter orcrit at %s" % time.time() 
1027         if regexes:
1028             def ff(row, r=regexes):
1029                 for propname, regex in r.items():
1030                     val = str(getattr(row, propname))
1031                     if not regex.search(val):
1032                         return 0
1033                 return 1
1034             
1035             iv = v.filter(ff)
1036             v = v.remapwith(iv)
1037         #print "filter regexs at %s" % time.time() 
1038         
1039         if sort or group:
1040             sortspec = []
1041             rev = []
1042             for dir, propname in group, sort:
1043                 if propname is None: continue
1044                 isreversed = 0
1045                 if dir == '-':
1046                     isreversed = 1
1047                 try:
1048                     prop = getattr(v, propname)
1049                 except AttributeError:
1050                     print "MK has no property %s" % propname
1051                     continue
1052                 propclass = self.ruprops.get(propname, None)
1053                 if propclass is None:
1054                     propclass = self.privateprops.get(propname, None)
1055                     if propclass is None:
1056                         print "Schema has no property %s" % propname
1057                         continue
1058                 if isinstance(propclass, hyperdb.Link):
1059                     linkclass = self.db.getclass(propclass.classname)
1060                     lv = linkclass.getview()
1061                     lv = lv.rename('id', propname)
1062                     v = v.join(lv, prop, 1)
1063                     if linkclass.getprops().has_key('order'):
1064                         propname = 'order'
1065                     else:
1066                         propname = linkclass.labelprop()
1067                     prop = getattr(v, propname)
1068                 if isreversed:
1069                     rev.append(prop)
1070                 sortspec.append(prop)
1071             v = v.sortrev(sortspec, rev)[:] #XXX Metakit bug
1072         #print "filter sort   at %s" % time.time() 
1073             
1074         rslt = []
1075         for row in v:
1076             id = str(row.id)
1077             if search_matches is not None:
1078                 if search_matches.has_key(id):
1079                     rslt.append(id)
1080             else:
1081                 rslt.append(id)
1082         return rslt
1083     
1084     def hasnode(self, nodeid):
1085         return int(nodeid) < self.maxid
1086     
1087     def labelprop(self, default_to_id=0):
1088         ''' Return the property name for a label for the given node.
1090         This method attempts to generate a consistent label for the node.
1091         It tries the following in order:
1092             1. key property
1093             2. "name" property
1094             3. "title" property
1095             4. first property from the sorted property name list
1096         '''
1097         k = self.getkey()
1098         if  k:
1099             return k
1100         props = self.getprops()
1101         if props.has_key('name'):
1102             return 'name'
1103         elif props.has_key('title'):
1104             return 'title'
1105         if default_to_id:
1106             return 'id'
1107         props = props.keys()
1108         props.sort()
1109         return props[0]
1111     def stringFind(self, **requirements):
1112         """Locate a particular node by matching a set of its String
1113         properties in a caseless search.
1115         If the property is not a String property, a TypeError is raised.
1116         
1117         The return is a list of the id of all nodes that match.
1118         """
1119         for propname in requirements.keys():
1120             prop = self.properties[propname]
1121             if isinstance(not prop, hyperdb.String):
1122                 raise TypeError, "'%s' not a String property"%propname
1123             requirements[propname] = requirements[propname].lower()
1124         requirements['_isdel'] = 0
1125         
1126         l = []
1127         for row in self.getview().select(requirements):
1128             l.append(str(row.id))
1129         return l
1131     def addjournal(self, nodeid, action, params):
1132         self.db.addjournal(self.classname, nodeid, action, params)
1134     def index(self, nodeid):
1135         ''' Add (or refresh) the node to search indexes '''
1136         # find all the String properties that have indexme
1137         for prop, propclass in self.getprops().items():
1138             if isinstance(propclass, hyperdb.String) and propclass.indexme:
1139                 # index them under (classname, nodeid, property)
1140                 self.db.indexer.add_text((self.classname, nodeid, prop),
1141                                 str(self.get(nodeid, prop)))
1143     def export_list(self, propnames, nodeid):
1144         ''' Export a node - generate a list of CSV-able data in the order
1145             specified by propnames for the given node.
1146         '''
1147         properties = self.getprops()
1148         l = []
1149         for prop in propnames:
1150             proptype = properties[prop]
1151             value = self.get(nodeid, prop)
1152             # "marshal" data where needed
1153             if value is None:
1154                 pass
1155             elif isinstance(proptype, hyperdb.Date):
1156                 value = value.get_tuple()
1157             elif isinstance(proptype, hyperdb.Interval):
1158                 value = value.get_tuple()
1159             elif isinstance(proptype, hyperdb.Password):
1160                 value = str(value)
1161             l.append(repr(value))
1163         # append retired flag
1164         l.append(self.is_retired(nodeid))
1166         return l
1167         
1168     def import_list(self, propnames, proplist):
1169         ''' Import a node - all information including "id" is present and
1170             should not be sanity checked. Triggers are not triggered. The
1171             journal should be initialised using the "creator" and "creation"
1172             information.
1174             Return the nodeid of the node imported.
1175         '''
1176         if self.db.journaltag is None:
1177             raise hyperdb.DatabaseError, 'Database open read-only'
1178         properties = self.getprops()
1180         d = {}
1181         view = self.getview(1)
1182         for i in range(len(propnames)):
1183             value = eval(proplist[i])
1184             if not value:
1185                 continue
1187             propname = propnames[i]
1188             if propname == 'id':
1189                 newid = value = int(value)
1190             elif propname == 'is retired':
1191                 # is the item retired?
1192                 if int(value):
1193                     d['_isdel'] = 1
1194                 continue
1195             elif value is None:
1196                 d[propname] = None
1197                 continue
1199             prop = properties[propname]
1200             if isinstance(prop, hyperdb.Date):
1201                 value = int(calendar.timegm(value))
1202             elif isinstance(prop, hyperdb.Interval):
1203                 value = date.Interval(value).serialise()
1204             elif isinstance(prop, hyperdb.Number):
1205                 value = int(value)
1206             elif isinstance(prop, hyperdb.Boolean):
1207                 value = int(value)
1208             elif isinstance(prop, hyperdb.Link) and value:
1209                 value = int(value)
1210             elif isinstance(prop, hyperdb.Multilink):
1211                 # we handle multilinks separately
1212                 continue
1213             d[propname] = value
1215         # possibly make a new node
1216         if not d.has_key('id'):
1217             d['id'] = newid = self.maxid
1218             self.maxid += 1
1220         # save off the node
1221         view.append(d)
1223         # fix up multilinks
1224         ndx = view.find(id=newid)
1225         row = view[ndx]
1226         for i in range(len(propnames)):
1227             value = eval(proplist[i])
1228             propname = propnames[i]
1229             if propname == 'is retired':
1230                 continue
1231             prop = properties[propname]
1232             if not isinstance(prop, hyperdb.Multilink):
1233                 continue
1234             sv = getattr(row, propname)
1235             for entry in value:
1236                 sv.append(int(entry))
1238         self.db.dirty = 1
1239         creator = d.get('creator', 0)
1240         creation = d.get('creation', 0)
1241         self.db.addjournal(self.classname, str(newid), _CREATE, {}, creator,
1242             creation)
1243         return newid
1245     # --- used by Database
1246     def _commit(self):
1247         """ called post commit of the DB.
1248             interested subclasses may override """
1249         self.uncommitted = {}
1250         self.rbactions = []
1251         self.idcache = {}
1252     def _rollback(self):  
1253         """ called pre rollback of the DB.
1254             interested subclasses may override """
1255         for action in self.rbactions:
1256             action()
1257         self.rbactions = []
1258         self.uncommitted = {}
1259         self.idcache = {}
1260     def _clear(self):
1261         view = self.getview(1)
1262         if len(view):
1263             view[:] = []
1264             self.db.dirty = 1
1265         iv = self.getindexview(1)
1266         if iv:
1267             iv[:] = []
1268     def rollbackaction(self, action):
1269         """ call this to register a callback called on rollback
1270             callback is removed on end of transaction """
1271         self.rbactions.append(action)
1272     # --- internal
1273     def __getview(self):
1274         ''' Find the interface for a specific Class in the hyperdb.
1276             This method checks to see whether the schema has changed and
1277             re-works the underlying metakit structure if it has.
1278         '''
1279         db = self.db._db
1280         view = db.view(self.classname)
1281         mkprops = view.structure()
1283         # if we have structure in the database, and the structure hasn't
1284         # changed
1285         if mkprops and self.db.fastopen:
1286             return view.ordered(1)
1288         # is the definition the same?
1289         for nm, rutyp in self.ruprops.items():
1290             for mkprop in mkprops:
1291                 if mkprop.name == nm:
1292                     break
1293             else:
1294                 mkprop = None
1295             if mkprop is None:
1296                 break
1297             if _typmap[rutyp.__class__] != mkprop.type:
1298                 break
1299         else:
1300             return view.ordered(1)
1301         # need to create or restructure the mk view
1302         # id comes first, so MK will order it for us
1303         self.db.dirty = 1
1304         s = ["%s[id:I" % self.classname]
1305         for nm, rutyp in self.ruprops.items():
1306             mktyp = _typmap[rutyp.__class__]
1307             s.append('%s:%s' % (nm, mktyp))
1308             if mktyp == 'V':
1309                 s[-1] += ('[fid:I]')
1310         s.append('_isdel:I,activity:I,creation:I,creator:I]')
1311         v = self.db._db.getas(','.join(s))
1312         self.db.commit()
1313         return v.ordered(1)
1314     def getview(self, RW=0):
1315         return self.db._db.view(self.classname).ordered(1)
1316     def getindexview(self, RW=0):
1317         return self.db._db.view("_%s" % self.classname).ordered(1)
1319 def _fetchML(sv):
1320     l = []
1321     for row in sv:
1322         if row.fid:
1323             l.append(str(row.fid))
1324     return l
1326 def _fetchPW(s):
1327     ''' Convert to a password.Password unless the password is '' which is
1328         our sentinel for "unset".
1329     '''
1330     if s == '':
1331         return None
1332     p = password.Password()
1333     p.unpack(s)
1334     return p
1336 def _fetchLink(n):
1337     ''' Return None if the link is 0 - otherwise strify it.
1338     '''
1339     return n and str(n) or None
1341 def _fetchDate(n):
1342     ''' Convert the timestamp to a date.Date instance - unless it's 0 which
1343         is our sentinel for "unset".
1344     '''
1345     if n == 0:
1346         return None
1347     return date.Date(time.gmtime(n))
1349 def _fetchInterval(n):
1350     ''' Convert to a date.Interval unless the interval is '' which is our
1351         sentinel for "unset".
1352     '''
1353     if n == '':
1354         return None
1355     return date.Interval(n)
1357 _converters = {
1358     hyperdb.Date   : _fetchDate,
1359     hyperdb.Link   : _fetchLink,
1360     hyperdb.Multilink : _fetchML,
1361     hyperdb.Interval  : _fetchInterval,
1362     hyperdb.Password  : _fetchPW,
1363     hyperdb.Boolean   : lambda n: n,
1364     hyperdb.Number    : lambda n: n,
1365     hyperdb.String    : lambda s: s and str(s) or None,
1366 }                
1368 class FileName(hyperdb.String):
1369     isfilename = 1            
1371 _typmap = {
1372     FileName : 'S',
1373     hyperdb.String : 'S',
1374     hyperdb.Date   : 'I',
1375     hyperdb.Link   : 'I',
1376     hyperdb.Multilink : 'V',
1377     hyperdb.Interval  : 'S',
1378     hyperdb.Password  : 'S',
1379     hyperdb.Boolean   : 'I',
1380     hyperdb.Number    : 'I',
1382 class FileClass(Class, hyperdb.FileClass):
1383     ''' like Class but with a content property
1384     '''
1385     default_mime_type = 'text/plain'
1386     def __init__(self, db, classname, **properties):
1387         properties['content'] = FileName()
1388         if not properties.has_key('type'):
1389             properties['type'] = hyperdb.String()
1390         Class.__init__(self, db, classname, **properties)
1392     def get(self, nodeid, propname, default=_marker, cache=1):
1393         x = Class.get(self, nodeid, propname, default)
1394         poss_msg = 'Possibly an access right configuration problem.'
1395         if propname == 'content':
1396             if x.startswith('file:'):
1397                 fnm = x[5:]
1398                 try:
1399                     x = open(fnm, 'rb').read()
1400                 except IOError, (strerror):
1401                     # XXX by catching this we donot see an error in the log.
1402                     return 'ERROR reading file: %s%s\n%s\n%s'%(
1403                             self.classname, nodeid, poss_msg, strerror)
1404         return x
1406     def create(self, **propvalues):
1407         self.fireAuditors('create', None, propvalues)
1408         content = propvalues['content']
1409         del propvalues['content']
1410         newid = Class.create_inner(self, **propvalues)
1411         if not content:
1412             return newid
1413         nm = bnm = '%s%s' % (self.classname, newid)
1414         sd = str(int(int(newid) / 1000))
1415         d = os.path.join(self.db.config.DATABASE, 'files', self.classname, sd)
1416         if not os.path.exists(d):
1417             os.makedirs(d)
1418         nm = os.path.join(d, nm)
1419         open(nm, 'wb').write(content)
1420         self.set(newid, content = 'file:'+nm)
1421         mimetype = propvalues.get('type', self.default_mime_type)
1422         self.db.indexer.add_text((self.classname, newid, 'content'), content,
1423             mimetype)
1424         def undo(fnm=nm, action1=os.remove, indexer=self.db.indexer):
1425             action1(fnm)
1426         self.rollbackaction(undo)
1427         return newid
1429     def index(self, nodeid):
1430         Class.index(self, nodeid)
1431         mimetype = self.get(nodeid, 'type')
1432         if not mimetype:
1433             mimetype = self.default_mime_type
1434         self.db.indexer.add_text((self.classname, nodeid, 'content'),
1435                     self.get(nodeid, 'content'), mimetype)
1436  
1437 class IssueClass(Class, roundupdb.IssueClass):
1438     ''' The newly-created class automatically includes the "messages",
1439         "files", "nosy", and "superseder" properties.  If the 'properties'
1440         dictionary attempts to specify any of these properties or a
1441         "creation" or "activity" property, a ValueError is raised.
1442     '''
1443     def __init__(self, db, classname, **properties):
1444         if not properties.has_key('title'):
1445             properties['title'] = hyperdb.String(indexme='yes')
1446         if not properties.has_key('messages'):
1447             properties['messages'] = hyperdb.Multilink("msg")
1448         if not properties.has_key('files'):
1449             properties['files'] = hyperdb.Multilink("file")
1450         if not properties.has_key('nosy'):
1451             # note: journalling is turned off as it really just wastes
1452             # space. this behaviour may be overridden in an instance
1453             properties['nosy'] = hyperdb.Multilink("user", do_journal="no")
1454         if not properties.has_key('superseder'):
1455             properties['superseder'] = hyperdb.Multilink(classname)
1456         Class.__init__(self, db, classname, **properties)
1457         
1458 CURVERSION = 2
1460 class Indexer(indexer.Indexer):
1461     disallows = {'THE':1, 'THIS':1, 'ZZZ':1, 'THAT':1, 'WITH':1}
1462     def __init__(self, path, datadb):
1463         self.path = os.path.join(path, 'index.mk4')
1464         self.db = metakit.storage(self.path, 1)
1465         self.datadb = datadb
1466         self.reindex = 0
1467         v = self.db.view('version')
1468         if not v.structure():
1469             v = self.db.getas('version[vers:I]')
1470             self.db.commit()
1471             v.append(vers=CURVERSION)
1472             self.reindex = 1
1473         elif v[0].vers != CURVERSION:
1474             v[0].vers = CURVERSION
1475             self.reindex = 1
1476         if self.reindex:
1477             self.db.getas('ids[tblid:I,nodeid:I,propid:I,ignore:I]')
1478             self.db.getas('index[word:S,hits[pos:I]]')
1479             self.db.commit()
1480             self.reindex = 1
1481         self.changed = 0
1482         self.propcache = {}
1484     def force_reindex(self):
1485         v = self.db.view('ids')
1486         v[:] = []
1487         v = self.db.view('index')
1488         v[:] = []
1489         self.db.commit()
1490         self.reindex = 1
1492     def should_reindex(self):
1493         return self.reindex
1495     def _getprops(self, classname):
1496         props = self.propcache.get(classname, None)
1497         if props is None:
1498             props = self.datadb.view(classname).structure()
1499             props = [prop.name for prop in props]
1500             self.propcache[classname] = props
1501         return props
1503     def _getpropid(self, classname, propname):
1504         return self._getprops(classname).index(propname)
1506     def _getpropname(self, classname, propid):
1507         return self._getprops(classname)[propid]
1509     def add_text(self, identifier, text, mime_type='text/plain'):
1510         if mime_type != 'text/plain':
1511             return
1512         classname, nodeid, property = identifier
1513         tbls = self.datadb.view('tables')
1514         tblid = tbls.find(name=classname)
1515         if tblid < 0:
1516             raise KeyError, "unknown class %r"%classname
1517         nodeid = int(nodeid)
1518         propid = self._getpropid(classname, property)
1519         ids = self.db.view('ids')
1520         oldpos = ids.find(tblid=tblid,nodeid=nodeid,propid=propid,ignore=0)
1521         if oldpos > -1:
1522             ids[oldpos].ignore = 1
1523             self.changed = 1
1524         pos = ids.append(tblid=tblid,nodeid=nodeid,propid=propid)
1525         
1526         wordlist = re.findall(r'\b\w{2,25}\b', text.upper())
1527         words = {}
1528         for word in wordlist:
1529             if not self.disallows.has_key(word):
1530                 words[word] = 1
1531         words = words.keys()
1532         
1533         index = self.db.view('index').ordered(1)
1534         for word in words:
1535             ndx = index.find(word=word)
1536             if ndx < 0:
1537                 index.append(word=word)
1538                 ndx = index.find(word=word)
1539             index[ndx].hits.append(pos=pos)
1540             self.changed = 1
1542     def find(self, wordlist):
1543         hits = None
1544         index = self.db.view('index').ordered(1)
1545         for word in wordlist:
1546             word = word.upper()
1547             if not 2 < len(word) < 26:
1548                 continue
1549             ndx = index.find(word=word)
1550             if ndx < 0:
1551                 return {}
1552             if hits is None:
1553                 hits = index[ndx].hits
1554             else:
1555                 hits = hits.intersect(index[ndx].hits)
1556             if len(hits) == 0:
1557                 return {}
1558         if hits is None:
1559             return {}
1560         rslt = {}
1561         ids = self.db.view('ids').remapwith(hits)
1562         tbls = self.datadb.view('tables')
1563         for i in range(len(ids)):
1564             hit = ids[i]
1565             if not hit.ignore:
1566                 classname = tbls[hit.tblid].name
1567                 nodeid = str(hit.nodeid)
1568                 property = self._getpropname(classname, hit.propid)
1569                 rslt[i] = (classname, nodeid, property)
1570         return rslt
1572     def save_index(self):
1573         if self.changed:
1574             self.db.commit()
1575         self.changed = 0
1577     def rollback(self):
1578         if self.changed:
1579             self.db.rollback()
1580             self.db = metakit.storage(self.path, 1)
1581         self.changed = 0