Code

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