Code

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