Code

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