Code

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