Code

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