Code

match empty multilinks implemented for metakit
[roundup.git] / roundup / backends / back_metakit.py
1 # $Id: back_metakit.py,v 1.44 2003-03-26 06:36:11 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      ''    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                     setattr(row, key, str(value))
607                 changes[key] = str(oldvalue)
608                 propvalues[key] = str(value)
609                 
610             elif isinstance(prop, hyperdb.Number):
611                 if value is None:
612                     value = 0
613                 try:
614                     v = int(value)
615                 except ValueError:
616                     raise TypeError, "%s (%s) is not numeric"%(key, repr(value))
617                 setattr(row, key, v)
618                 changes[key] = oldvalue
619                 propvalues[key] = value
621             elif isinstance(prop, hyperdb.Boolean):
622                 if value is None:
623                     bv = 0
624                 elif value not in (0,1):
625                     raise TypeError, "%s (%s) is not boolean"%(key, repr(value))
626                 else:
627                     bv = value 
628                 setattr(row, key, bv)
629                 changes[key] = oldvalue
630                 propvalues[key] = value
632             oldnode[key] = oldvalue
634         # nothing to do?
635         if not propvalues:
636             return propvalues
637         if not propvalues.has_key('activity'):
638             row.activity = int(time.time())
639         if isnew:
640             if not row.creation:
641                 row.creation = int(time.time())
642             if not row.creator:
643                 row.creator = self.db.curuserid
645         self.db.dirty = 1
646         if self.do_journal:
647             if isnew:
648                 self.db.addjournal(self.classname, nodeid, _CREATE, {})
649                 self.fireReactors('create', nodeid, None)
650             else:
651                 self.db.addjournal(self.classname, nodeid, _SET, changes)
652                 self.fireReactors('set', nodeid, oldnode)
654         return propvalues
655     
656     def retire(self, nodeid):
657         if self.db.journaltag is None:
658             raise hyperdb.DatabaseError, 'Database open read-only'
659         self.fireAuditors('retire', nodeid, None)
660         view = self.getview(1)
661         ndx = view.find(id=int(nodeid))
662         if ndx < 0:
663             raise KeyError, "nodeid %s not found" % nodeid
665         row = view[ndx]
666         oldvalues = self.uncommitted.setdefault(row.id, {})
667         oldval = oldvalues['_isdel'] = row._isdel
668         row._isdel = 1
670         if self.do_journal:
671             self.db.addjournal(self.classname, nodeid, _RETIRE, {})
672         if self.keyname:
673             iv = self.getindexview(1)
674             ndx = iv.find(k=getattr(row, self.keyname),i=row.id)
675             if ndx > -1:
676                 iv.delete(ndx)
677         self.db.dirty = 1
678         self.fireReactors('retire', nodeid, None)
680     def restore(self, nodeid):
681         '''Restpre a retired node.
683         Make node available for all operations like it was before retirement.
684         '''
685         if self.db.journaltag is None:
686             raise hyperdb.DatabaseError, 'Database open read-only'
688         # check if key property was overrided
689         key = self.getkey()
690         keyvalue = self.get(nodeid, key)
691         try:
692             id = self.lookup(keyvalue)
693         except KeyError:
694             pass
695         else:
696             raise KeyError, "Key property (%s) of retired node clashes with \
697                 existing one (%s)" % (key, keyvalue)
698         # Now we can safely restore node
699         self.fireAuditors('restore', nodeid, None)
700         view = self.getview(1)
701         ndx = view.find(id=int(nodeid))
702         if ndx < 0:
703             raise KeyError, "nodeid %s not found" % nodeid
705         row = view[ndx]
706         oldvalues = self.uncommitted.setdefault(row.id, {})
707         oldval = oldvalues['_isdel'] = row._isdel
708         row._isdel = 0
710         if self.do_journal:
711             self.db.addjournal(self.classname, nodeid, _RESTORE, {})
712         if self.keyname:
713             iv = self.getindexview(1)
714             ndx = iv.find(k=getattr(row, self.keyname),i=row.id)
715             if ndx > -1:
716                 iv.delete(ndx)
717         self.db.dirty = 1
718         self.fireReactors('restore', nodeid, None)
720     def is_retired(self, nodeid):
721         view = self.getview(1)
722         # node must exist & not be retired
723         id = int(nodeid)
724         ndx = view.find(id=id)
725         if ndx < 0:
726             raise IndexError, "%s has no node %s" % (self.classname, nodeid)
727         row = view[ndx]
728         return row._isdel
730     def history(self, nodeid):
731         if not self.do_journal:
732             raise ValueError, 'Journalling is disabled for this class'
733         return self.db.getjournal(self.classname, nodeid)
735     def setkey(self, propname):
736         if self.keyname:
737             if propname == self.keyname:
738                 return
739             raise ValueError, "%s already indexed on %s"%(self.classname,
740                 self.keyname)
741         prop = self.properties.get(propname, None)
742         if prop is None:
743             prop = self.privateprops.get(propname, None)
744         if prop is None:
745             raise KeyError, "no property %s" % propname
746         if not isinstance(prop, hyperdb.String):
747             raise TypeError, "%s is not a String" % propname
749         # first setkey for this run
750         self.keyname = propname
751         iv = self.db._db.view('_%s' % self.classname)
752         if self.db.fastopen and iv.structure():
753             return
755         # very first setkey ever
756         self.db.dirty = 1
757         iv = self.db._db.getas('_%s[k:S,i:I]' % self.classname)
758         iv = iv.ordered(1)
759         for row in self.getview():
760             iv.append(k=getattr(row, propname), i=row.id)
761         self.db.commit()
763     def getkey(self):
764         return self.keyname
766     def lookup(self, keyvalue):
767         if type(keyvalue) is not _STRINGTYPE:
768             raise TypeError, "%r is not a string" % keyvalue
769         iv = self.getindexview()
770         if iv:
771             ndx = iv.find(k=keyvalue)
772             if ndx > -1:
773                 return str(iv[ndx].i)
774         else:
775             view = self.getview()
776             ndx = view.find({self.keyname:keyvalue, '_isdel':0})
777             if ndx > -1:
778                 return str(view[ndx].id)
779         raise KeyError, keyvalue
781     def destroy(self, id):
782         view = self.getview(1)
783         ndx = view.find(id=int(id))
784         if ndx > -1:
785             if self.keyname:
786                 keyvalue = getattr(view[ndx], self.keyname)
787                 iv = self.getindexview(1)
788                 if iv:
789                     ivndx = iv.find(k=keyvalue)
790                     if ivndx > -1:
791                         iv.delete(ivndx)
792             view.delete(ndx)
793             self.db.destroyjournal(self.classname, id)
794             self.db.dirty = 1
795         
796     def find(self, **propspec):
797         """Get the ids of nodes in this class which link to the given nodes.
799         'propspec' consists of keyword args propname={nodeid:1,}   
800         'propname' must be the name of a property in this class, or a
801                    KeyError is raised.  That property must be a Link or
802                    Multilink property, or a TypeError is raised.
804         Any node in this class whose propname property links to any of the
805         nodeids will be returned. Used by the full text indexing, which knows
806         that "foo" occurs in msg1, msg3 and file7; so we have hits on these
807         issues:
809             db.issue.find(messages={'1':1,'3':1}, files={'7':1})
811         """
812         propspec = propspec.items()
813         for propname, nodeid in propspec:
814             # check the prop is OK
815             prop = self.ruprops[propname]
816             if (not isinstance(prop, hyperdb.Link) and
817                     not isinstance(prop, hyperdb.Multilink)):
818                 raise TypeError, "'%s' not a Link/Multilink property"%propname
820         vws = []
821         for propname, ids in propspec:
822             if type(ids) is _STRINGTYPE:
823                 ids = {int(ids):1}
824             else:
825                 d = {}
826                 for id in ids.keys():
827                     d[int(id)] = 1
828                 ids = d
829             prop = self.ruprops[propname]
830             view = self.getview()
831             if isinstance(prop, hyperdb.Multilink):
832                 def ff(row, nm=propname, ids=ids):
833                     sv = getattr(row, nm)
834                     for sr in sv:
835                         if ids.has_key(sr.fid):
836                             return 1
837                     return 0
838             else:
839                 def ff(row, nm=propname, ids=ids):
840                     return ids.has_key(getattr(row, nm))
841             ndxview = view.filter(ff)
842             vws.append(ndxview.unique())
844         # handle the empty match case
845         if not vws:
846             return []
848         ndxview = vws[0]
849         for v in vws[1:]:
850             ndxview = ndxview.union(v)
851         view = self.getview().remapwith(ndxview)
852         rslt = []
853         for row in view:
854             rslt.append(str(row.id))
855         return rslt
856             
858     def list(self):
859         l = []
860         for row in self.getview().select(_isdel=0):
861             l.append(str(row.id))
862         return l
864     def getnodeids(self):
865         l = []
866         for row in self.getview():
867             l.append(str(row.id))
868         return l
870     def count(self):
871         return len(self.getview())
873     def getprops(self, protected=1):
874         # protected is not in ping's spec
875         allprops = self.ruprops.copy()
876         if protected and self.privateprops is not None:
877             allprops.update(self.privateprops)
878         return allprops
880     def addprop(self, **properties):
881         for key in properties.keys():
882             if self.ruprops.has_key(key):
883                 raise ValueError, "%s is already a property of %s"%(key,
884                     self.classname)
885         self.ruprops.update(properties)
886         # Class structure has changed
887         self.db.fastopen = 0
888         view = self.__getview()
889         self.db.commit()
890     # ---- end of ping's spec
892     def filter(self, search_matches, filterspec, sort=(None,None),
893             group=(None,None)):
894         # search_matches is None or a set (dict of {nodeid: {propname:[nodeid,...]}})
895         # filterspec is a dict {propname:value}
896         # sort and group are (dir, prop) where dir is '+', '-' or None
897         #                    and prop is a prop name or None
899         timezone = self.db.getUserTimezone()
901         where = {'_isdel':0}
902         wherehigh = {}
903         mlcriteria = {}
904         regexes = {}
905         orcriteria = {}
906         for propname, value in filterspec.items():
907             prop = self.ruprops.get(propname, None)
908             if prop is None:
909                 prop = self.privateprops[propname]
910             if isinstance(prop, hyperdb.Multilink):
911                 if value in ('-1', ['-1']):
912                     value = []
913                 elif 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                 mlcriteria[propname] = u
926             elif isinstance(prop, hyperdb.Link):
927                 if type(value) is not _LISTTYPE:
928                     value = [value]
929                 # transform keys to ids
930                 u = []
931                 for item in value:
932                     try:
933                         item = int(item)
934                     except (TypeError, ValueError):
935                         item = int(self.db.getclass(prop.classname).lookup(item))
936                     if item == -1:
937                         item = 0
938                     u.append(item)
939                 if len(u) == 1:
940                     where[propname] = u[0]
941                 else:
942                     orcriteria[propname] = u
943             elif isinstance(prop, hyperdb.String):
944                 # simple glob searching
945                 v = re.sub(r'([\|\{\}\\\.\+\[\]\(\)])', r'\\\1', value)
946                 v = v.replace('?', '.')
947                 v = v.replace('*', '.*?')
948                 regexes[propname] = re.compile(v, re.I)
949             elif propname == 'id':
950                 where[propname] = int(value)
951             elif isinstance(prop, hyperdb.Boolean):
952                 if type(value) is _STRINGTYPE:
953                     bv = value.lower() in ('yes', 'true', 'on', '1')
954                 else:
955                     bv = value
956                 where[propname] = bv
957             elif isinstance(prop, hyperdb.Date):
958                 try:
959                     # Try to filter on range of dates
960                     date_rng = Range(value, date.Date, offset=timezone)
961                     if date_rng.from_value:
962                         t = date_rng.from_value.get_tuple()
963                         where[propname] = int(calendar.timegm(t))
964                     if date_rng.to_value:
965                         t = date_rng.to_value.get_tuple()
966                         wherehigh[propname] = int(calendar.timegm(t))
967                     else:
968                         wherehigh[propname] = None
969                 except ValueError:
970                     # If range creation fails - ignore that search parameter
971                     pass                        
972             elif isinstance(prop, hyperdb.Interval):
973                 where[propname] = str(date.Interval(value))
974             elif isinstance(prop, hyperdb.Number):
975                 where[propname] = int(value)
976             else:
977                 where[propname] = str(value)
978         v = self.getview()
979         #print "filter start at  %s" % time.time() 
980         if where:
981             where_higherbound = where.copy()
982             where_higherbound.update(wherehigh)
983             v = v.select(where, where_higherbound)
984         #print "filter where at  %s" % time.time() 
986         if mlcriteria:
987             # multilink - if any of the nodeids required by the
988             # filterspec aren't in this node's property, then skip it
989             def ff(row, ml=mlcriteria):
990                 for propname, values in ml.items():
991                     sv = getattr(row, propname)
992                     if not values and sv:
993                         return 0
994                     for id in values:
995                         if sv.find(fid=id) == -1:
996                             return 0
997                 return 1
998             iv = v.filter(ff)
999             v = v.remapwith(iv)
1001         #print "filter mlcrit at %s" % time.time() 
1002         
1003         if orcriteria:
1004             def ff(row, crit=orcriteria):
1005                 for propname, allowed in crit.items():
1006                     val = getattr(row, propname)
1007                     if val not in allowed:
1008                         return 0
1009                 return 1
1010             
1011             iv = v.filter(ff)
1012             v = v.remapwith(iv)
1013         
1014         #print "filter orcrit at %s" % time.time() 
1015         if regexes:
1016             def ff(row, r=regexes):
1017                 for propname, regex in r.items():
1018                     val = str(getattr(row, propname))
1019                     if not regex.search(val):
1020                         return 0
1021                 return 1
1022             
1023             iv = v.filter(ff)
1024             v = v.remapwith(iv)
1025         #print "filter regexs at %s" % time.time() 
1026         
1027         if sort or group:
1028             sortspec = []
1029             rev = []
1030             for dir, propname in group, sort:
1031                 if propname is None: continue
1032                 isreversed = 0
1033                 if dir == '-':
1034                     isreversed = 1
1035                 try:
1036                     prop = getattr(v, propname)
1037                 except AttributeError:
1038                     print "MK has no property %s" % propname
1039                     continue
1040                 propclass = self.ruprops.get(propname, None)
1041                 if propclass is None:
1042                     propclass = self.privateprops.get(propname, None)
1043                     if propclass is None:
1044                         print "Schema has no property %s" % propname
1045                         continue
1046                 if isinstance(propclass, hyperdb.Link):
1047                     linkclass = self.db.getclass(propclass.classname)
1048                     lv = linkclass.getview()
1049                     lv = lv.rename('id', propname)
1050                     v = v.join(lv, prop, 1)
1051                     if linkclass.getprops().has_key('order'):
1052                         propname = 'order'
1053                     else:
1054                         propname = linkclass.labelprop()
1055                     prop = getattr(v, propname)
1056                 if isreversed:
1057                     rev.append(prop)
1058                 sortspec.append(prop)
1059             v = v.sortrev(sortspec, rev)[:] #XXX Metakit bug
1060         #print "filter sort   at %s" % time.time() 
1061             
1062         rslt = []
1063         for row in v:
1064             id = str(row.id)
1065             if search_matches is not None:
1066                 if search_matches.has_key(id):
1067                     rslt.append(id)
1068             else:
1069                 rslt.append(id)
1070         return rslt
1071     
1072     def hasnode(self, nodeid):
1073         return int(nodeid) < self.maxid
1074     
1075     def labelprop(self, default_to_id=0):
1076         ''' Return the property name for a label for the given node.
1078         This method attempts to generate a consistent label for the node.
1079         It tries the following in order:
1080             1. key property
1081             2. "name" property
1082             3. "title" property
1083             4. first property from the sorted property name list
1084         '''
1085         k = self.getkey()
1086         if  k:
1087             return k
1088         props = self.getprops()
1089         if props.has_key('name'):
1090             return 'name'
1091         elif props.has_key('title'):
1092             return 'title'
1093         if default_to_id:
1094             return 'id'
1095         props = props.keys()
1096         props.sort()
1097         return props[0]
1099     def stringFind(self, **requirements):
1100         """Locate a particular node by matching a set of its String
1101         properties in a caseless search.
1103         If the property is not a String property, a TypeError is raised.
1104         
1105         The return is a list of the id of all nodes that match.
1106         """
1107         for propname in requirements.keys():
1108             prop = self.properties[propname]
1109             if isinstance(not prop, hyperdb.String):
1110                 raise TypeError, "'%s' not a String property"%propname
1111             requirements[propname] = requirements[propname].lower()
1112         requirements['_isdel'] = 0
1113         
1114         l = []
1115         for row in self.getview().select(requirements):
1116             l.append(str(row.id))
1117         return l
1119     def addjournal(self, nodeid, action, params):
1120         self.db.addjournal(self.classname, nodeid, action, params)
1122     def index(self, nodeid):
1123         ''' Add (or refresh) the node to search indexes '''
1124         # find all the String properties that have indexme
1125         for prop, propclass in self.getprops().items():
1126             if isinstance(propclass, hyperdb.String) and propclass.indexme:
1127                 # index them under (classname, nodeid, property)
1128                 self.db.indexer.add_text((self.classname, nodeid, prop),
1129                                 str(self.get(nodeid, prop)))
1131     def export_list(self, propnames, nodeid):
1132         ''' Export a node - generate a list of CSV-able data in the order
1133             specified by propnames for the given node.
1134         '''
1135         properties = self.getprops()
1136         l = []
1137         for prop in propnames:
1138             proptype = properties[prop]
1139             value = self.get(nodeid, prop)
1140             # "marshal" data where needed
1141             if value is None:
1142                 pass
1143             elif isinstance(proptype, hyperdb.Date):
1144                 value = value.get_tuple()
1145             elif isinstance(proptype, hyperdb.Interval):
1146                 value = value.get_tuple()
1147             elif isinstance(proptype, hyperdb.Password):
1148                 value = str(value)
1149             l.append(repr(value))
1151         # append retired flag
1152         l.append(self.is_retired(nodeid))
1154         return l
1155         
1156     def import_list(self, propnames, proplist):
1157         ''' Import a node - all information including "id" is present and
1158             should not be sanity checked. Triggers are not triggered. The
1159             journal should be initialised using the "creator" and "creation"
1160             information.
1162             Return the nodeid of the node imported.
1163         '''
1164         if self.db.journaltag is None:
1165             raise hyperdb.DatabaseError, 'Database open read-only'
1166         properties = self.getprops()
1168         d = {}
1169         view = self.getview(1)
1170         for i in range(len(propnames)):
1171             value = eval(proplist[i])
1172             if not value:
1173                 continue
1175             propname = propnames[i]
1176             if propname == 'id':
1177                 newid = value = int(value)
1178             elif propname == 'is retired':
1179                 # is the item retired?
1180                 if int(value):
1181                     d['_isdel'] = 1
1182                 continue
1184             prop = properties[propname]
1185             if isinstance(prop, hyperdb.Date):
1186                 value = int(calendar.timegm(value))
1187             elif isinstance(prop, hyperdb.Interval):
1188                 value = str(date.Interval(value))
1189             elif isinstance(prop, hyperdb.Number):
1190                 value = int(value)
1191             elif isinstance(prop, hyperdb.Boolean):
1192                 value = int(value)
1193             elif isinstance(prop, hyperdb.Link) and value:
1194                 value = int(value)
1195             elif isinstance(prop, hyperdb.Multilink):
1196                 # we handle multilinks separately
1197                 continue
1198             d[propname] = value
1200         # possibly make a new node
1201         if not d.has_key('id'):
1202             d['id'] = newid = self.maxid
1203             self.maxid += 1
1205         # save off the node
1206         view.append(d)
1208         # fix up multilinks
1209         ndx = view.find(id=newid)
1210         row = view[ndx]
1211         for i in range(len(propnames)):
1212             value = eval(proplist[i])
1213             propname = propnames[i]
1214             if propname == 'is retired':
1215                 continue
1216             prop = properties[propname]
1217             if not isinstance(prop, hyperdb.Multilink):
1218                 continue
1219             sv = getattr(row, propname)
1220             for entry in value:
1221                 sv.append(int(entry))
1223         self.db.dirty = 1
1224         creator = d.get('creator', 0)
1225         creation = d.get('creation', 0)
1226         self.db.addjournal(self.classname, str(newid), _CREATE, {}, creator,
1227             creation)
1228         return newid
1230     # --- used by Database
1231     def _commit(self):
1232         """ called post commit of the DB.
1233             interested subclasses may override """
1234         self.uncommitted = {}
1235         self.rbactions = []
1236         self.idcache = {}
1237     def _rollback(self):  
1238         """ called pre rollback of the DB.
1239             interested subclasses may override """
1240         for action in self.rbactions:
1241             action()
1242         self.rbactions = []
1243         self.uncommitted = {}
1244         self.idcache = {}
1245     def _clear(self):
1246         view = self.getview(1)
1247         if len(view):
1248             view[:] = []
1249             self.db.dirty = 1
1250         iv = self.getindexview(1)
1251         if iv:
1252             iv[:] = []
1253     def rollbackaction(self, action):
1254         """ call this to register a callback called on rollback
1255             callback is removed on end of transaction """
1256         self.rbactions.append(action)
1257     # --- internal
1258     def __getview(self):
1259         ''' Find the interface for a specific Class in the hyperdb.
1261             This method checks to see whether the schema has changed and
1262             re-works the underlying metakit structure if it has.
1263         '''
1264         db = self.db._db
1265         view = db.view(self.classname)
1266         mkprops = view.structure()
1268         # if we have structure in the database, and the structure hasn't
1269         # changed
1270         if mkprops and self.db.fastopen:
1271             return view.ordered(1)
1273         # is the definition the same?
1274         for nm, rutyp in self.ruprops.items():
1275             for mkprop in mkprops:
1276                 if mkprop.name == nm:
1277                     break
1278             else:
1279                 mkprop = None
1280             if mkprop is None:
1281                 break
1282             if _typmap[rutyp.__class__] != mkprop.type:
1283                 break
1284         else:
1285             return view.ordered(1)
1286         # need to create or restructure the mk view
1287         # id comes first, so MK will order it for us
1288         self.db.dirty = 1
1289         s = ["%s[id:I" % self.classname]
1290         for nm, rutyp in self.ruprops.items():
1291             mktyp = _typmap[rutyp.__class__]
1292             s.append('%s:%s' % (nm, mktyp))
1293             if mktyp == 'V':
1294                 s[-1] += ('[fid:I]')
1295         s.append('_isdel:I,activity:I,creation:I,creator:I]')
1296         v = self.db._db.getas(','.join(s))
1297         self.db.commit()
1298         return v.ordered(1)
1299     def getview(self, RW=0):
1300         return self.db._db.view(self.classname).ordered(1)
1301     def getindexview(self, RW=0):
1302         return self.db._db.view("_%s" % self.classname).ordered(1)
1304 def _fetchML(sv):
1305     l = []
1306     for row in sv:
1307         if row.fid:
1308             l.append(str(row.fid))
1309     return l
1311 def _fetchPW(s):
1312     ''' Convert to a password.Password unless the password is '' which is
1313         our sentinel for "unset".
1314     '''
1315     if s == '':
1316         return None
1317     p = password.Password()
1318     p.unpack(s)
1319     return p
1321 def _fetchLink(n):
1322     ''' Return None if the link is 0 - otherwise strify it.
1323     '''
1324     return n and str(n) or None
1326 def _fetchDate(n):
1327     ''' Convert the timestamp to a date.Date instance - unless it's 0 which
1328         is our sentinel for "unset".
1329     '''
1330     if n == 0:
1331         return None
1332     return date.Date(time.gmtime(n))
1334 def _fetchInterval(n):
1335     ''' Convert to a date.Interval unless the interval is '' which is our
1336         sentinel for "unset".
1337     '''
1338     if n == '':
1339         return None
1340     return date.Interval(n)
1342 _converters = {
1343     hyperdb.Date   : _fetchDate,
1344     hyperdb.Link   : _fetchLink,
1345     hyperdb.Multilink : _fetchML,
1346     hyperdb.Interval  : _fetchInterval,
1347     hyperdb.Password  : _fetchPW,
1348     hyperdb.Boolean   : lambda n: n,
1349     hyperdb.Number    : lambda n: n,
1350     hyperdb.String    : lambda s: s and str(s) or None,
1351 }                
1353 class FileName(hyperdb.String):
1354     isfilename = 1            
1356 _typmap = {
1357     FileName : 'S',
1358     hyperdb.String : 'S',
1359     hyperdb.Date   : 'I',
1360     hyperdb.Link   : 'I',
1361     hyperdb.Multilink : 'V',
1362     hyperdb.Interval  : 'S',
1363     hyperdb.Password  : 'S',
1364     hyperdb.Boolean   : 'I',
1365     hyperdb.Number    : 'I',
1367 class FileClass(Class, hyperdb.FileClass):
1368     ''' like Class but with a content property
1369     '''
1370     default_mime_type = 'text/plain'
1371     def __init__(self, db, classname, **properties):
1372         properties['content'] = FileName()
1373         if not properties.has_key('type'):
1374             properties['type'] = hyperdb.String()
1375         Class.__init__(self, db, classname, **properties)
1377     def get(self, nodeid, propname, default=_marker, cache=1):
1378         x = Class.get(self, nodeid, propname, default, cache)
1379         poss_msg = 'Possibly an access right configuration problem.'
1380         if propname == 'content':
1381             if x.startswith('file:'):
1382                 fnm = x[5:]
1383                 try:
1384                     x = open(fnm, 'rb').read()
1385                 except IOError, (strerror):
1386                     # XXX by catching this we donot see an error in the log.
1387                     return 'ERROR reading file: %s%s\n%s\n%s'%(
1388                             self.classname, nodeid, poss_msg, strerror)
1389         return x
1391     def create(self, **propvalues):
1392         self.fireAuditors('create', None, propvalues)
1393         content = propvalues['content']
1394         del propvalues['content']
1395         newid = Class.create_inner(self, **propvalues)
1396         if not content:
1397             return newid
1398         nm = bnm = '%s%s' % (self.classname, newid)
1399         sd = str(int(int(newid) / 1000))
1400         d = os.path.join(self.db.config.DATABASE, 'files', self.classname, sd)
1401         if not os.path.exists(d):
1402             os.makedirs(d)
1403         nm = os.path.join(d, nm)
1404         open(nm, 'wb').write(content)
1405         self.set(newid, content = 'file:'+nm)
1406         mimetype = propvalues.get('type', self.default_mime_type)
1407         self.db.indexer.add_text((self.classname, newid, 'content'), content,
1408             mimetype)
1409         def undo(fnm=nm, action1=os.remove, indexer=self.db.indexer):
1410             action1(fnm)
1411         self.rollbackaction(undo)
1412         return newid
1414     def index(self, nodeid):
1415         Class.index(self, nodeid)
1416         mimetype = self.get(nodeid, 'type')
1417         if not mimetype:
1418             mimetype = self.default_mime_type
1419         self.db.indexer.add_text((self.classname, nodeid, 'content'),
1420                     self.get(nodeid, 'content'), mimetype)
1421  
1422 class IssueClass(Class, roundupdb.IssueClass):
1423     ''' The newly-created class automatically includes the "messages",
1424         "files", "nosy", and "superseder" properties.  If the 'properties'
1425         dictionary attempts to specify any of these properties or a
1426         "creation" or "activity" property, a ValueError is raised.
1427     '''
1428     def __init__(self, db, classname, **properties):
1429         if not properties.has_key('title'):
1430             properties['title'] = hyperdb.String(indexme='yes')
1431         if not properties.has_key('messages'):
1432             properties['messages'] = hyperdb.Multilink("msg")
1433         if not properties.has_key('files'):
1434             properties['files'] = hyperdb.Multilink("file")
1435         if not properties.has_key('nosy'):
1436             # note: journalling is turned off as it really just wastes
1437             # space. this behaviour may be overridden in an instance
1438             properties['nosy'] = hyperdb.Multilink("user", do_journal="no")
1439         if not properties.has_key('superseder'):
1440             properties['superseder'] = hyperdb.Multilink(classname)
1441         Class.__init__(self, db, classname, **properties)
1442         
1443 CURVERSION = 2
1445 class Indexer(indexer.Indexer):
1446     disallows = {'THE':1, 'THIS':1, 'ZZZ':1, 'THAT':1, 'WITH':1}
1447     def __init__(self, path, datadb):
1448         self.path = os.path.join(path, 'index.mk4')
1449         self.db = metakit.storage(self.path, 1)
1450         self.datadb = datadb
1451         self.reindex = 0
1452         v = self.db.view('version')
1453         if not v.structure():
1454             v = self.db.getas('version[vers:I]')
1455             self.db.commit()
1456             v.append(vers=CURVERSION)
1457             self.reindex = 1
1458         elif v[0].vers != CURVERSION:
1459             v[0].vers = CURVERSION
1460             self.reindex = 1
1461         if self.reindex:
1462             self.db.getas('ids[tblid:I,nodeid:I,propid:I,ignore:I]')
1463             self.db.getas('index[word:S,hits[pos:I]]')
1464             self.db.commit()
1465             self.reindex = 1
1466         self.changed = 0
1467         self.propcache = {}
1469     def force_reindex(self):
1470         v = self.db.view('ids')
1471         v[:] = []
1472         v = self.db.view('index')
1473         v[:] = []
1474         self.db.commit()
1475         self.reindex = 1
1477     def should_reindex(self):
1478         return self.reindex
1480     def _getprops(self, classname):
1481         props = self.propcache.get(classname, None)
1482         if props is None:
1483             props = self.datadb.view(classname).structure()
1484             props = [prop.name for prop in props]
1485             self.propcache[classname] = props
1486         return props
1488     def _getpropid(self, classname, propname):
1489         return self._getprops(classname).index(propname)
1491     def _getpropname(self, classname, propid):
1492         return self._getprops(classname)[propid]
1494     def add_text(self, identifier, text, mime_type='text/plain'):
1495         if mime_type != 'text/plain':
1496             return
1497         classname, nodeid, property = identifier
1498         tbls = self.datadb.view('tables')
1499         tblid = tbls.find(name=classname)
1500         if tblid < 0:
1501             raise KeyError, "unknown class %r"%classname
1502         nodeid = int(nodeid)
1503         propid = self._getpropid(classname, property)
1504         ids = self.db.view('ids')
1505         oldpos = ids.find(tblid=tblid,nodeid=nodeid,propid=propid,ignore=0)
1506         if oldpos > -1:
1507             ids[oldpos].ignore = 1
1508             self.changed = 1
1509         pos = ids.append(tblid=tblid,nodeid=nodeid,propid=propid)
1510         
1511         wordlist = re.findall(r'\b\w{2,25}\b', text.upper())
1512         words = {}
1513         for word in wordlist:
1514             if not self.disallows.has_key(word):
1515                 words[word] = 1
1516         words = words.keys()
1517         
1518         index = self.db.view('index').ordered(1)
1519         for word in words:
1520             ndx = index.find(word=word)
1521             if ndx < 0:
1522                 index.append(word=word)
1523                 ndx = index.find(word=word)
1524             index[ndx].hits.append(pos=pos)
1525             self.changed = 1
1527     def find(self, wordlist):
1528         hits = None
1529         index = self.db.view('index').ordered(1)
1530         for word in wordlist:
1531             word = word.upper()
1532             if not 2 < len(word) < 26:
1533                 continue
1534             ndx = index.find(word=word)
1535             if ndx < 0:
1536                 return {}
1537             if hits is None:
1538                 hits = index[ndx].hits
1539             else:
1540                 hits = hits.intersect(index[ndx].hits)
1541             if len(hits) == 0:
1542                 return {}
1543         if hits is None:
1544             return {}
1545         rslt = {}
1546         ids = self.db.view('ids').remapwith(hits)
1547         tbls = self.datadb.view('tables')
1548         for i in range(len(ids)):
1549             hit = ids[i]
1550             if not hit.ignore:
1551                 classname = tbls[hit.tblid].name
1552                 nodeid = str(hit.nodeid)
1553                 property = self._getpropname(classname, hit.propid)
1554                 rslt[i] = (classname, nodeid, property)
1555         return rslt
1557     def save_index(self):
1558         if self.changed:
1559             self.db.commit()
1560         self.changed = 0
1562     def rollback(self):
1563         if self.changed:
1564             self.db.rollback()
1565             self.db = metakit.storage(self.path, 1)
1566         self.changed = 0