Code

7d173c1856b134de95d2ea647e688bdf5d3ad5f1
[roundup.git] / roundup / backends / back_metakit.py
1 from roundup import hyperdb, date, password, roundupdb, security
2 import metakit
3 from sessions import Sessions
4 import re, marshal, os, sys, weakref, time, calendar
5 from roundup import indexer
6 import locking
8 _dbs = {}
10 def Database(config, journaltag=None):
11     db = _dbs.get(config.DATABASE, None)
12     if db is None or db._db is None:
13         db = _Database(config, journaltag)
14         _dbs[config.DATABASE] = db
15     return db
17 class _Database(hyperdb.Database):
18     def __init__(self, config, journaltag=None):
19         self.config = config
20         self.journaltag = journaltag
21         self.classes = {}
22         self.dirty = 0
23         self.lockfile = None
24         self._db = self.__open()
25         self.indexer = Indexer(self.config.DATABASE, self._db)
26         self.sessions = Sessions(self.config)
27         self.security = security.Security(self)
29         os.umask(0002)
30     def post_init(self):
31         if self.indexer.should_reindex():
32             self.reindex()
34     def reindex(self):
35         for klass in self.classes.values():
36             for nodeid in klass.list():
37                 klass.index(nodeid)
38         self.indexer.save_index()
39         
40             
41     # --- defined in ping's spec
42     def __getattr__(self, classname):
43         if classname == 'curuserid':
44             try:
45                 self.curuserid = x = int(self.classes['user'].lookup(self.journaltag))
46             except KeyError:
47                 x = 0
48             return x
49         return self.getclass(classname)
50     def getclass(self, classname):
51         return self.classes[classname]
52     def getclasses(self):
53         return self.classes.keys()
54     # --- end of ping's spec 
55     # --- exposed methods
56     def commit(self):
57         if self.dirty:
58             self._db.commit()
59             for cl in self.classes.values():
60                 cl._commit()
61             self.indexer.save_index()
62         self.dirty = 0
63     def rollback(self):
64         if self.dirty:
65             for cl in self.classes.values():
66                 cl._rollback()
67             self._db.rollback()
68         self.dirty = 0
69     def clear(self):
70         for cl in self.classes.values():
71             cl._clear()
72     def hasnode(self, classname, nodeid):
73         return self.getclass(classname).hasnode(nodeid)
74     def pack(self, pack_before):
75         pass
76     def addclass(self, cl):
77         self.classes[cl.classname] = cl
78         if self.tables.find(name=cl.classname) < 0:
79             self.tables.append(name=cl.classname)
80     def addjournal(self, tablenm, nodeid, action, params):
81         tblid = self.tables.find(name=tablenm)
82         if tblid == -1:
83             tblid = self.tables.append(name=tablenm)
84         # tableid:I,nodeid:I,date:I,user:I,action:I,params:B
85         self.hist.append(tableid=tblid,
86                          nodeid=int(nodeid),
87                          date=int(time.time()),
88                          action=action,
89                          user = self.curuserid,
90                          params = marshal.dumps(params))
91     def gethistory(self, tablenm, nodeid):
92         rslt = []
93         tblid = self.tables.find(name=tablenm)
94         if tblid == -1:
95             return rslt
96         q = self.hist.select(tableid=tblid, nodeid=int(nodeid))
97         i = 0
98         userclass = self.getclass('user')
99         for row in q:
100             try:
101                 params = marshal.loads(row.params)
102             except ValueError:
103                 print "history couldn't unmarshal %r" % row.params
104                 params = {}
105             usernm = userclass.get(str(row.user), 'username')
106             dt = date.Date(time.gmtime(row.date))
107             rslt.append((i, dt, usernm, _actionnames[row.action], params))
108             i += 1
109         return rslt
110             
111     def close(self):
112         for cl in self.classes.values():
113             cl.db = None
114         self._db = None
115         locking.release_lock(self.lockfile)
116         del _dbs[self.config.DATABASE]
117         self.lockfile.close()
118         self.classes = {}
119         self.indexer = None
121     # --- internal
122     def __open(self):
123         self.dbnm = db = os.path.join(self.config.DATABASE, 'tracker.mk4')
124         lockfilenm = db[:-3]+'lck'
125         self.lockfile = locking.acquire_lock(lockfilenm)
126         self.lockfile.write(str(os.getpid()))
127         self.lockfile.flush()
128         self.fastopen = 0
129         if os.path.exists(db):
130             dbtm = os.path.getmtime(db)
131             pkgnm = self.config.__name__.split('.')[0]
132             schemamod = sys.modules.get(pkgnm+'.dbinit', None)
133             if schemamod:
134                 if os.path.exists(schemamod.__file__):
135                     schematm = os.path.getmtime(schemamod.__file__)
136                     if schematm < dbtm:
137                         # found schema mod - it's older than the db
138                         self.fastopen = 1
139                 else:
140                      # can't find schemamod - must be frozen
141                     self.fastopen = 1
142         db = metakit.storage(db, 1)
143         hist = db.view('history')
144         tables = db.view('tables')
145         if not self.fastopen:
146             if not hist.structure():
147                 hist = db.getas('history[tableid:I,nodeid:I,date:I,user:I,action:I,params:B]')
148             if not tables.structure():
149                 tables = db.getas('tables[name:S]')
150         self.tables = tables
151         self.hist = hist
152         return db
153         
154 _STRINGTYPE = type('')
155 _LISTTYPE = type([])
156 _CREATE, _SET, _RETIRE, _LINK, _UNLINK = range(5)
158 _actionnames = {
159     _CREATE : 'create',
160     _SET : 'set',
161     _RETIRE : 'retire',
162     _LINK : 'link',
163     _UNLINK : 'unlink',
166 _marker = []
168 _ALLOWSETTINGPRIVATEPROPS = 0
170 class Class:    
171     privateprops = None
172     def __init__(self, db, classname, **properties):
173         #self.db = weakref.proxy(db)
174         self.db = db
175         self.classname = classname
176         self.keyname = None
177         self.ruprops = properties
178         self.privateprops = { 'id' : hyperdb.String(),
179                               'activity' : hyperdb.Date(),
180                               'creation' : hyperdb.Date(),
181                               'creator'  : hyperdb.Link('user') }
182         self.auditors = {'create': [], 'set': [], 'retire': []} # event -> list of callables
183         self.reactors = {'create': [], 'set': [], 'retire': []} # ditto
184         view = self.__getview()
185         self.maxid = 1
186         if view:
187             self.maxid = view[-1].id + 1
188         self.uncommitted = {}
189         self.rbactions = []
190         # people reach inside!!
191         self.properties = self.ruprops
192         self.db.addclass(self)
193         self.idcache = {}
195         # default is to journal changes
196         self.do_journal = 1
198     def enableJournalling(self):
199         '''Turn journalling on for this class
200         '''
201         self.do_journal = 1
203     def disableJournalling(self):
204         '''Turn journalling off for this class
205         '''
206         self.do_journal = 0
207         
208     # --- the roundup.Class methods
209     def audit(self, event, detector):
210         l = self.auditors[event]
211         if detector not in l:
212             self.auditors[event].append(detector)
213     def fireAuditors(self, action, nodeid, newvalues):
214         for audit in self.auditors[action]:
215             audit(self.db, self, nodeid, newvalues)
216     def fireReactors(self, action, nodeid, oldvalues):
217         for react in self.reactors[action]:
218             react(self.db, self, nodeid, oldvalues)
219     def react(self, event, detector):
220         l = self.reactors[event]
221         if detector not in l:
222             self.reactors[event].append(detector)
223     # --- the hyperdb.Class methods
224     def create(self, **propvalues):
225         self.fireAuditors('create', None, propvalues)
226         rowdict = {}
227         rowdict['id'] = newid = self.maxid
228         self.maxid += 1
229         ndx = self.getview(1).append(rowdict)
230         propvalues['#ISNEW'] = 1
231         try:
232             self.set(str(newid), **propvalues)
233         except Exception:
234             self.maxid -= 1
235             raise
236         return str(newid)
237     
238     def get(self, nodeid, propname, default=_marker, cache=1):
239         # default and cache aren't in the spec
240         # cache=0 means "original value"
242         view = self.getview()        
243         id = int(nodeid)
244         if cache == 0:
245             oldnode = self.uncommitted.get(id, None)
246             if oldnode and oldnode.has_key(propname):
247                 return oldnode[propname]
248         ndx = self.idcache.get(id, None)
249         if ndx is None:
250             ndx = view.find(id=id)
251             if ndx < 0:
252                 raise IndexError, "%s has no node %s" % (self.classname, nodeid)
253             self.idcache[id] = ndx
254         try:
255             raw = getattr(view[ndx], propname)
256         except AttributeError:
257             raise KeyError, propname
258         rutyp = self.ruprops.get(propname, None)
259         if rutyp is None:
260             rutyp = self.privateprops[propname]
261         converter = _converters.get(rutyp.__class__, None)
262         if converter:
263             raw = converter(raw)
264         return raw
265         
266     def set(self, nodeid, **propvalues):
267         isnew = 0
268         if propvalues.has_key('#ISNEW'):
269             isnew = 1
270             del propvalues['#ISNEW']
271         if not isnew:
272             self.fireAuditors('set', nodeid, propvalues)
273         if not propvalues:
274             return
275         if propvalues.has_key('id'):
276             raise KeyError, '"id" is reserved'
277         if self.db.journaltag is None:
278             raise DatabaseError, 'Database open read-only'
279         view = self.getview(1)
280         # node must exist & not be retired
281         id = int(nodeid)
282         ndx = view.find(id=id)
283         if ndx < 0:
284             raise IndexError, "%s has no node %s" % (self.classname, nodeid)
285         row = view[ndx]
286         if row._isdel:
287             raise IndexError, "%s has no node %s" % (self.classname, nodeid)
288         oldnode = self.uncommitted.setdefault(id, {})
289         changes = {}
290         
291         for key, value in propvalues.items():
292             # this will raise the KeyError if the property isn't valid
293             # ... we don't use getprops() here because we only care about
294             # the writeable properties.
295             if _ALLOWSETTINGPRIVATEPROPS:
296                 prop = self.ruprops.get(key, None)
297                 if not prop:
298                     prop = self.privateprops[key]
299             else:
300                 prop = self.ruprops[key]
301             converter = _converters.get(prop.__class__, lambda v: v)
302             # if the value's the same as the existing value, no sense in
303             # doing anything
304             oldvalue = converter(getattr(row, key))
305             if  value == oldvalue:
306                 del propvalues[key]
307                 continue
308             
309             # check to make sure we're not duplicating an existing key
310             if key == self.keyname:
311                 iv = self.getindexview(1)
312                 ndx = iv.find(k=value)
313                 if ndx == -1:
314                     iv.append(k=value, i=row.id)
315                     if not isnew:
316                         ndx = iv.find(k=oldvalue)
317                         if ndx > -1:
318                             iv.delete(ndx)
319                 else:
320                     raise ValueError, 'node with key "%s" exists'%value
322             # do stuff based on the prop type
323             if isinstance(prop, hyperdb.Link):
324                 link_class = prop.classname
325                 # if it isn't a number, it's a key
326                 if type(value) != _STRINGTYPE:
327                     raise ValueError, 'link value must be String'
328                 try:
329                     int(value)
330                 except ValueError:
331                     try:
332                         value = self.db.getclass(link_class).lookup(value)
333                     except (TypeError, KeyError):
334                         raise IndexError, 'new property "%s": %s not a %s'%(
335                             key, value, prop.classname)
337                 if not self.db.getclass(link_class).hasnode(value):
338                     raise IndexError, '%s has no node %s'%(link_class, value)
340                 setattr(row, key, int(value))
341                 changes[key] = oldvalue
342                 
343                 if self.do_journal and prop.do_journal:
344                     # register the unlink with the old linked node
345                     if oldvalue:
346                         self.db.addjournal(link_class, value, _UNLINK, (self.classname, str(row.id), key))
348                     # register the link with the newly linked node
349                     if value:
350                         self.db.addjournal(link_class, value, _LINK, (self.classname, str(row.id), key))
352             elif isinstance(prop, hyperdb.Multilink):
353                 if type(value) != _LISTTYPE:
354                     raise TypeError, 'new property "%s" not a list of ids'%key
355                 link_class = prop.classname
356                 l = []
357                 for entry in value:
358                     if type(entry) != _STRINGTYPE:
359                         raise ValueError, 'new property "%s" link value ' \
360                             'must be a string'%key
361                     # if it isn't a number, it's a key
362                     try:
363                         int(entry)
364                     except ValueError:
365                         try:
366                             entry = self.db.getclass(link_class).lookup(entry)
367                         except (TypeError, KeyError):
368                             raise IndexError, 'new property "%s": %s not a %s'%(
369                                 key, entry, prop.classname)
370                     l.append(entry)
371                 propvalues[key] = value = l
373                 # handle removals
374                 rmvd = []
375                 for id in oldvalue:
376                     if id not in value:
377                         rmvd.append(id)
378                         # register the unlink with the old linked node
379                         if self.do_journal and prop.do_journal:
380                             self.db.addjournal(link_class, id, _UNLINK, (self.classname, str(row.id), key))
382                 # handle additions
383                 adds = []
384                 for id in value:
385                     if id not in oldvalue:
386                         if not self.db.getclass(link_class).hasnode(id):
387                             raise IndexError, '%s has no node %s'%(
388                                 link_class, id)
389                         adds.append(id)
390                         # register the link with the newly linked node
391                         if self.do_journal and prop.do_journal:
392                             self.db.addjournal(link_class, id, _LINK, (self.classname, str(row.id), key))
393                             
394                 sv = getattr(row, key)
395                 i = 0
396                 while i < len(sv):
397                     if str(sv[i].fid) in rmvd:
398                         sv.delete(i)
399                     else:
400                         i += 1
401                 for id in adds:
402                     sv.append(fid=int(id))
403                 changes[key] = oldvalue
404                     
406             elif isinstance(prop, hyperdb.String):
407                 if value is not None and type(value) != _STRINGTYPE:
408                     raise TypeError, 'new property "%s" not a string'%key
409                 setattr(row, key, value)
410                 changes[key] = oldvalue
411                 if hasattr(prop, 'isfilename') and prop.isfilename:
412                     propvalues[key] = os.path.basename(value)
413                 if prop.indexme:
414                     self.db.indexer.add_text((self.classname, nodeid, key), value, 'text/plain')
416             elif isinstance(prop, hyperdb.Password):
417                 if not isinstance(value, password.Password):
418                     raise TypeError, 'new property "%s" not a Password'% key
419                 setattr(row, key, str(value))
420                 changes[key] = str(oldvalue)
421                 propvalues[key] = str(value)
423             elif value is not None and isinstance(prop, hyperdb.Date):
424                 if not isinstance(value, date.Date):
425                     raise TypeError, 'new property "%s" not a Date'% key
426                 setattr(row, key, int(calendar.timegm(value.get_tuple())))
427                 changes[key] = str(oldvalue)
428                 propvalues[key] = str(value)
430             elif value is not None and isinstance(prop, hyperdb.Interval):
431                 if not isinstance(value, date.Interval):
432                     raise TypeError, 'new property "%s" not an Interval'% key
433                 setattr(row, key, str(value))
434                 changes[key] = str(oldvalue)
435                 propvalues[key] = str(value)
436                 
437             elif value is not None and isinstance(prop, hyperdb.Number):
438                 setattr(row, key, int(value))
439                 changes[key] = oldvalue
440                 propvalues[key] = value
441                 
442             elif value is not None and isinstance(prop, hyperdb.Boolean):
443                 bv = value != 0
444                 setattr(row, key, bv)
445                 changes[key] = oldvalue
446                 propvalues[key] = value
448             oldnode[key] = oldvalue
450         # nothing to do?
451         if not propvalues:
452             return
453         if not propvalues.has_key('activity'):
454             row.activity = int(time.time())
455         if isnew:
456             if not row.creation:
457                 row.creation = int(time.time())
458             if not row.creator:
459                 row.creator = self.db.curuserid
460             
461         self.db.dirty = 1
462         if self.do_journal:
463             if isnew:
464                 self.db.addjournal(self.classname, nodeid, _CREATE, {})
465                 self.fireReactors('create', nodeid, None)
466             else:
467                 self.db.addjournal(self.classname, nodeid, _SET, changes)
468                 self.fireReactors('set', nodeid, oldnode)
470     def retire(self, nodeid):
471         self.fireAuditors('retire', nodeid, None)
472         view = self.getview(1)
473         ndx = view.find(id=int(nodeid))
474         if ndx < 0:
475             raise KeyError, "nodeid %s not found" % nodeid
476         row = view[ndx]
477         oldvalues = self.uncommitted.setdefault(row.id, {})
478         oldval = oldvalues['_isdel'] = row._isdel
479         row._isdel = 1
480         if self.do_journal:
481             self.db.addjournal(self.classname, nodeid, _RETIRE, {})
482         iv = self.getindexview(1)
483         ndx = iv.find(k=getattr(row, self.keyname),i=row.id)
484         if ndx > -1:
485             iv.delete(ndx)
486         self.db.dirty = 1
487         self.fireReactors('retire', nodeid, None)
488     def history(self, nodeid):
489         if not self.do_journal:
490             raise ValueError, 'Journalling is disabled for this class'
491         return self.db.gethistory(self.classname, nodeid)
492     def setkey(self, propname):
493         if self.keyname:
494             if propname == self.keyname:
495                 return
496             raise ValueError, "%s already indexed on %s" % (self.classname, self.keyname)
497         # first setkey for this run
498         self.keyname = propname
499         iv = self.db._db.view('_%s' % self.classname)
500         if self.db.fastopen and iv.structure():
501             return
502         # very first setkey ever
503         self.db.dirty = 1
504         iv = self.db._db.getas('_%s[k:S,i:I]' % self.classname)
505         iv = iv.ordered(1)
506 #        print "setkey building index"
507         for row in self.getview():
508             iv.append(k=getattr(row, propname), i=row.id)
509         self.db.commit()
510     def getkey(self):
511         return self.keyname
512     def lookup(self, keyvalue):
513         if type(keyvalue) is not _STRINGTYPE:
514             raise TypeError, "%r is not a string" % keyvalue
515         iv = self.getindexview()
516         if iv:
517             ndx = iv.find(k=keyvalue)
518             if ndx > -1:
519                 return str(iv[ndx].i)
520         else:
521             view = self.getview()
522             ndx = view.find({self.keyname:keyvalue, '_isdel':0})
523             if ndx > -1:
524                 return str(view[ndx].id)
525         raise KeyError, keyvalue
527     def destroy(self, keyvalue):
528         #TODO clean this up once Richard's said how it should work
529         iv = self.getindexview()
530         if iv:
531             ndx = iv.find(k=keyvalue)
532             if ndx > -1:
533                 id = iv[ndx].i
534                 iv.delete(ndx)
535                 view = self.getview()
536                 ndx = view.find(id=id)
537                 if ndx > -1:
538                     view.delete(ndx)
540     def find(self, **propspec):
541         """Get the ids of nodes in this class which link to the given nodes.
543         'propspec' consists of keyword args propname={nodeid:1,}   
544         'propname' must be the name of a property in this class, or a
545                    KeyError is raised.  That property must be a Link or
546                    Multilink property, or a TypeError is raised.
548         Any node in this class whose propname property links to any of the
549         nodeids will be returned. Used by the full text indexing, which knows
550         that "foo" occurs in msg1, msg3 and file7; so we have hits on these
551         issues:
553             db.issue.find(messages={'1':1,'3':1}, files={'7':1})
555         """
556         propspec = propspec.items()
557         for propname, nodeid in propspec:
558             # check the prop is OK
559             prop = self.ruprops[propname]
560             if (not isinstance(prop, hyperdb.Link) and
561                     not isinstance(prop, hyperdb.Multilink)):
562                 raise TypeError, "'%s' not a Link/Multilink property"%propname
564         vws = []
565         for propname, ids in propspec:
566             if type(ids) is _STRINGTYPE:
567                 ids = {ids:1}
568             prop = self.ruprops[propname]
569             view = self.getview()
570             if isinstance(prop, hyperdb.Multilink):
571                 view = view.flatten(getattr(view, propname))
572                 def ff(row, nm=propname, ids=ids):
573                     return ids.has_key(str(row.fid))
574             else:
575                 def ff(row, nm=propname, ids=ids):
576                     return ids.has_key(str(getattr(row, nm)))
577             ndxview = view.filter(ff)
578             vws.append(ndxview.unique())
580         # handle the empty match case
581         if not vws:
582             return []
584         ndxview = vws[0]
585         for v in vws[1:]:
586             ndxview = ndxview.union(v)
587         view = view.remapwith(ndxview)
588         rslt = []
589         for row in view:
590             rslt.append(str(row.id))
591         return rslt
592             
594     def list(self):
595         l = []
596         for row in self.getview().select(_isdel=0):
597             l.append(str(row.id))
598         return l
599     def count(self):
600         return len(self.getview())
601     def getprops(self, protected=1):
602         # protected is not in ping's spec
603         allprops = self.ruprops.copy()
604         if protected and self.privateprops is not None:
605             allprops.update(self.privateprops)
606         return allprops
607     def addprop(self, **properties):
608         for key in properties.keys():
609             if self.ruprops.has_key(key):
610                 raise ValueError, "%s is already a property of %s" % (key, self.classname)
611         self.ruprops.update(properties)
612         self.db.fastopen = 0
613         view = self.__getview()
614         self.db.commit()
615     # ---- end of ping's spec
616     def filter(self, search_matches, filterspec, sort, group):
617         # search_matches is None or a set (dict of {nodeid: {propname:[nodeid,...]}})
618         # filterspec is a dict {propname:value}
619         # sort and group are lists of propnames
620         
621         where = {'_isdel':0}
622         mlcriteria = {}
623         regexes = {}
624         orcriteria = {}
625         for propname, value in filterspec.items():
626             prop = self.ruprops.get(propname, None)
627             if prop is None:
628                 prop = self.privateprops[propname]
629             if isinstance(prop, hyperdb.Multilink):
630                 if type(value) is not _LISTTYPE:
631                     value = [value]
632                 # transform keys to ids
633                 u = []
634                 for item in value:
635                     try:
636                         item = int(item)
637                     except (TypeError, ValueError):
638                         item = int(self.db.getclass(prop.classname).lookup(item))
639                     if item == -1:
640                         item = 0
641                     u.append(item)
642                 mlcriteria[propname] = u
643             elif isinstance(prop, hyperdb.Link):
644                 if type(value) is not _LISTTYPE:
645                     value = [value]
646                 # transform keys to ids
647                 u = []
648                 for item in value:
649                     try:
650                         item = int(item)
651                     except (TypeError, ValueError):
652                         item = int(self.db.getclass(prop.classname).lookup(item))
653                     if item == -1:
654                         item = 0
655                     u.append(item)
656                 if len(u) == 1:
657                     where[propname] = u[0]
658                 else:
659                     orcriteria[propname] = u
660             elif isinstance(prop, hyperdb.String):
661                 # simple glob searching
662                 v = re.sub(r'([\|\{\}\\\.\+\[\]\(\)])', r'\\\1', value)
663                 v = v.replace('?', '.')
664                 v = v.replace('*', '.*?')
665                 regexes[propname] = re.compile(v, re.I)
666             elif propname == 'id':
667                 where[propname] = int(value)
668             elif isinstance(prop, hyperdb.Boolean):
669                 if type(value) is _STRINGTYPE:
670                     bv = value.lower() in ('yes', 'true', 'on', '1')
671                 else:
672                     bv = value
673                 where[propname] = bv
674             elif isinstance(prop, hyperdb.Number):
675                 where[propname] = int(value)
676             else:
677                 where[propname] = str(value)
678         v = self.getview()
679         #print "filter start at  %s" % time.time() 
680         if where:
681             v = v.select(where)
682         #print "filter where at  %s" % time.time() 
683             
684         if mlcriteria:
685                     # multilink - if any of the nodeids required by the
686                     # filterspec aren't in this node's property, then skip
687                     # it
688             def ff(row, ml=mlcriteria):
689                 for propname, values in ml.items():
690                     sv = getattr(row, propname)
691                     for id in values:
692                         if sv.find(fid=id) == -1:
693                             return 0
694                 return 1
695             iv = v.filter(ff)
696             v = v.remapwith(iv)
698         #print "filter mlcrit at %s" % time.time() 
699         
700         if orcriteria:
701             def ff(row, crit=orcriteria):
702                 for propname, allowed in crit.items():
703                     val = getattr(row, propname)
704                     if val not in allowed:
705                         return 0
706                 return 1
707             
708             iv = v.filter(ff)
709             v = v.remapwith(iv)
710         
711         #print "filter orcrit at %s" % time.time() 
712         if regexes:
713             def ff(row, r=regexes):
714                 for propname, regex in r.items():
715                     val = getattr(row, propname)
716                     if not regex.search(val):
717                         return 0
718                 return 1
719             
720             iv = v.filter(ff)
721             v = v.remapwith(iv)
722         #print "filter regexs at %s" % time.time() 
723         
724         if sort or group:
725             sortspec = []
726             rev = []
727             for propname in group + sort:
728                 isreversed = 0
729                 if propname[0] == '-':
730                     propname = propname[1:]
731                     isreversed = 1
732                 try:
733                     prop = getattr(v, propname)
734                 except AttributeError:
735                     continue
736                 if isreversed:
737                     rev.append(prop)
738                 sortspec.append(prop)
739             v = v.sortrev(sortspec, rev)[:] #XXX Metakit bug
740         #print "filter sort   at %s" % time.time() 
741             
742         rslt = []
743         for row in v:
744             id = str(row.id)
745             if search_matches is not None:
746                 if search_matches.has_key(id):
747                     rslt.append(id)
748             else:
749                 rslt.append(id)
750         return rslt
751     
752     def hasnode(self, nodeid):
753         return int(nodeid) < self.maxid
754     
755     def labelprop(self, default_to_id=0):
756         ''' Return the property name for a label for the given node.
758         This method attempts to generate a consistent label for the node.
759         It tries the following in order:
760             1. key property
761             2. "name" property
762             3. "title" property
763             4. first property from the sorted property name list
764         '''
765         k = self.getkey()
766         if  k:
767             return k
768         props = self.getprops()
769         if props.has_key('name'):
770             return 'name'
771         elif props.has_key('title'):
772             return 'title'
773         if default_to_id:
774             return 'id'
775         props = props.keys()
776         props.sort()
777         return props[0]
778     def stringFind(self, **requirements):
779         """Locate a particular node by matching a set of its String
780         properties in a caseless search.
782         If the property is not a String property, a TypeError is raised.
783         
784         The return is a list of the id of all nodes that match.
785         """
786         for propname in requirements.keys():
787             prop = self.properties[propname]
788             if isinstance(not prop, hyperdb.String):
789                 raise TypeError, "'%s' not a String property"%propname
790             requirements[propname] = requirements[propname].lower()
791         requirements['_isdel'] = 0
792         
793         l = []
794         for row in self.getview().select(requirements):
795             l.append(str(row.id))
796         return l
798     def addjournal(self, nodeid, action, params):
799         self.db.addjournal(self.classname, nodeid, action, params)
801     def index(self, nodeid):
802         ''' Add (or refresh) the node to search indexes '''
803         # find all the String properties that have indexme
804         for prop, propclass in self.getprops().items():
805             if isinstance(propclass, hyperdb.String) and propclass.indexme:
806                 # index them under (classname, nodeid, property)
807                 self.db.indexer.add_text((self.classname, nodeid, prop),
808                                 str(self.get(nodeid, prop)))
810     # --- used by Database
811     def _commit(self):
812         """ called post commit of the DB.
813             interested subclasses may override """
814         self.uncommitted = {}
815         self.rbactions = []
816         self.idcache = {}
817     def _rollback(self):  
818         """ called pre rollback of the DB.
819             interested subclasses may override """
820         for action in self.rbactions:
821             action()
822         self.rbactions = []
823         self.uncommitted = {}
824         self.idcache = {}
825     def _clear(self):
826         view = self.getview(1)
827         if len(view):
828             view[:] = []
829             self.db.dirty = 1
830         iv = self.getindexview(1)
831         if iv:
832             iv[:] = []
833     def rollbackaction(self, action):
834         """ call this to register a callback called on rollback
835             callback is removed on end of transaction """
836         self.rbactions.append(action)
837     # --- internal
838     def __getview(self):
839         db = self.db._db
840         view = db.view(self.classname)
841         mkprops = view.structure()
842         if mkprops and self.db.fastopen:
843             return view.ordered(1)
844         # is the definition the same?
845         for nm, rutyp in self.ruprops.items():
846             for mkprop in mkprops:
847                 if mkprop.name == nm:
848                     break
849             else:
850                 mkprop = None
851             if mkprop is None:
852                 break
853             if _typmap[rutyp.__class__] != mkprop.type:
854                 break
855         else:
856             return view.ordered(1)
857         # need to create or restructure the mk view
858         # id comes first, so MK will order it for us
859         self.db.dirty = 1
860         s = ["%s[id:I" % self.classname]
861         for nm, rutyp in self.ruprops.items():
862             mktyp = _typmap[rutyp.__class__]
863             s.append('%s:%s' % (nm, mktyp))
864             if mktyp == 'V':
865                 s[-1] += ('[fid:I]')
866         s.append('_isdel:I,activity:I,creation:I,creator:I]')
867         v = self.db._db.getas(','.join(s))
868         self.db.commit()
869         return v.ordered(1)
870     def getview(self, RW=0):
871         return self.db._db.view(self.classname).ordered(1)
872     def getindexview(self, RW=0):
873         return self.db._db.view("_%s" % self.classname).ordered(1)
874     
875 def _fetchML(sv):
876     l = []
877     for row in sv:
878         if row.fid:
879             l.append(str(row.fid))
880     return l
882 def _fetchPW(s):
883     p = password.Password()
884     p.unpack(s)
885     return p
887 def _fetchLink(n):
888     return n and str(n) or None
890 def _fetchDate(n):
891     return date.Date(time.gmtime(n))
893 _converters = {
894     hyperdb.Date   : _fetchDate,
895     hyperdb.Link   : _fetchLink,
896     hyperdb.Multilink : _fetchML,
897     hyperdb.Interval  : date.Interval,
898     hyperdb.Password  : _fetchPW,
899     hyperdb.Boolean   : lambda n: n,
900     hyperdb.Number    : lambda n: n,
901     hyperdb.String    : str,
902 }                
904 class FileName(hyperdb.String):
905     isfilename = 1            
907 _typmap = {
908     FileName : 'S',
909     hyperdb.String : 'S',
910     hyperdb.Date   : 'I',
911     hyperdb.Link   : 'I',
912     hyperdb.Multilink : 'V',
913     hyperdb.Interval  : 'S',
914     hyperdb.Password  : 'S',
915     hyperdb.Boolean   : 'I',
916     hyperdb.Number    : 'I',
918 class FileClass(Class):
919     ' like Class but with a content property '
920     default_mime_type = 'text/plain'
921     def __init__(self, db, classname, **properties):
922         properties['content'] = FileName()
923         if not properties.has_key('type'):
924             properties['type'] = hyperdb.String()
925         Class.__init__(self, db, classname, **properties)
926     def get(self, nodeid, propname, default=_marker, cache=1):
927         x = Class.get(self, nodeid, propname, default, cache)
928         if propname == 'content':
929             if x.startswith('file:'):
930                 fnm = x[5:]
931                 try:
932                     x = open(fnm, 'rb').read()
933                 except Exception, e:
934                     x = repr(e)
935         return x
936     def create(self, **propvalues):
937         content = propvalues['content']
938         del propvalues['content']
939         newid = Class.create(self, **propvalues)
940         if not content:
941             return newid
942         nm = bnm = '%s%s' % (self.classname, newid)
943         sd = str(int(int(newid) / 1000))
944         d = os.path.join(self.db.config.DATABASE, 'files', self.classname, sd)
945         if not os.path.exists(d):
946             os.makedirs(d)
947         nm = os.path.join(d, nm)
948         open(nm, 'wb').write(content)
949         self.set(newid, content = 'file:'+nm)
950         mimetype = propvalues.get('type', self.default_mime_type)
951         self.db.indexer.add_text((self.classname, newid, 'content'), content, mimetype)
952         def undo(fnm=nm, action1=os.remove, indexer=self.db.indexer):
953             action1(fnm)
954         self.rollbackaction(undo)
955         return newid
956     def index(self, nodeid):
957         Class.index(self, nodeid)
958         mimetype = self.get(nodeid, 'type')
959         if not mimetype:
960             mimetype = self.default_mime_type
961         self.db.indexer.add_text((self.classname, nodeid, 'content'),
962                     self.get(nodeid, 'content'), mimetype)
963  
964 class IssueClass(Class, roundupdb.IssueClass):
965     # Overridden methods:
966     def __init__(self, db, classname, **properties):
967         """The newly-created class automatically includes the "messages",
968         "files", "nosy", and "superseder" properties.  If the 'properties'
969         dictionary attempts to specify any of these properties or a
970         "creation" or "activity" property, a ValueError is raised."""
971         if not properties.has_key('title'):
972             properties['title'] = hyperdb.String(indexme='yes')
973         if not properties.has_key('messages'):
974             properties['messages'] = hyperdb.Multilink("msg")
975         if not properties.has_key('files'):
976             properties['files'] = hyperdb.Multilink("file")
977         if not properties.has_key('nosy'):
978             properties['nosy'] = hyperdb.Multilink("user")
979         if not properties.has_key('superseder'):
980             properties['superseder'] = hyperdb.Multilink(classname)
981         Class.__init__(self, db, classname, **properties)
982         
983 CURVERSION = 1
985 class Indexer(indexer.Indexer):
986     disallows = {'THE':1, 'THIS':1, 'ZZZ':1, 'THAT':1, 'WITH':1}
987     def __init__(self, path, datadb):
988         self.db = metakit.storage(os.path.join(path, 'index.mk4'), 1)
989         self.datadb = datadb
990         self.reindex = 0
991         v = self.db.view('version')
992         if not v.structure():
993             v = self.db.getas('version[vers:I]')
994             self.db.commit()
995             v.append(vers=CURVERSION)
996             self.reindex = 1
997         elif v[0].vers != CURVERSION:
998             v[0].vers = CURVERSION
999             self.reindex = 1
1000         if self.reindex:
1001             self.db.getas('ids[tblid:I,nodeid:I,propid:I]')
1002             self.db.getas('index[word:S,hits[pos:I]]')
1003             self.db.commit()
1004             self.reindex = 1
1005         self.changed = 0
1006         self.propcache = {}
1007     def force_reindex(self):
1008         v = self.db.view('ids')
1009         v[:] = []
1010         v = self.db.view('index')
1011         v[:] = []
1012         self.db.commit()
1013         self.reindex = 1
1014     def should_reindex(self):
1015         return self.reindex
1016     def _getprops(self, classname):
1017         props = self.propcache.get(classname, None)
1018         if props is None:
1019             props = self.datadb.view(classname).structure()
1020             props = [prop.name for prop in props]
1021             self.propcache[classname] = props
1022         return props
1023     def _getpropid(self, classname, propname):
1024         return self._getprops(classname).index(propname)
1025     def _getpropname(self, classname, propid):
1026         return self._getprops(classname)[propid]
1027     def add_text(self, identifier, text, mime_type='text/plain'):
1028         if mime_type != 'text/plain':
1029             return
1030         classname, nodeid, property = identifier
1031         tbls = self.datadb.view('tables')
1032         tblid = tbls.find(name=classname)
1033         if tblid < 0:
1034             raise KeyError, "unknown class %r"%classname
1035         nodeid = int(nodeid)
1036         propid = self._getpropid(classname, property)
1037         pos = self.db.view('ids').append(tblid=tblid,nodeid=nodeid,propid=propid)
1038         
1039         wordlist = re.findall(r'\b\w{3,25}\b', text)
1040         words = {}
1041         for word in wordlist:
1042             word = word.upper()
1043             if not self.disallows.has_key(word):
1044                 words[word] = 1
1045         words = words.keys()
1046         
1047         index = self.db.view('index').ordered(1)
1048         for word in words:
1049             ndx = index.find(word=word)
1050             if ndx < 0:
1051                 ndx = index.append(word=word)
1052             hits = index[ndx].hits
1053             if len(hits)==0 or hits.find(pos=pos) < 0:
1054                 hits.append(pos=pos)
1055                 self.changed = 1
1056     def find(self, wordlist):
1057         hits = None
1058         index = self.db.view('index').ordered(1)
1059         for word in wordlist:
1060             if not 2 < len(word) < 26:
1061                 continue
1062             ndx = index.find(word=word)
1063             if ndx < 0:
1064                 return {}
1065             if hits is None:
1066                 hits = index[ndx].hits
1067             else:
1068                 hits = hits.intersect(index[ndx].hits)
1069             if len(hits) == 0:
1070                 return {}
1071         if hits is None:
1072             return {}
1073         rslt = {}
1074         ids = self.db.view('ids').remapwith(hits)
1075         tbls = self.datadb.view('tables')
1076         for i in range(len(ids)):
1077             hit = ids[i]
1078             classname = tbls[hit.tblid].name
1079             nodeid = str(hit.nodeid)
1080             property = self._getpropname(classname, hit.propid)
1081             rslt[i] = (classname, nodeid, property)
1082         return rslt
1083     def save_index(self):
1084         if self.changed:
1085             self.db.commit()
1086         self.changed = 0