source: tol/tolbase/lib/notebookdb/notebookdb.tcl @ 73aa767e

1941-docker-image-to-testdevel
Last change on this file since 73aa767e was 73aa767e, checked in by Jorge Pérez <josp.jorge@…>, 15 years ago

refs #1347, notebook data base

git-svn-id: ​https://www.tol-project.org/svn/tolp/trunk@3882 be4846ae-f051-483a-831f-b64e7d08527f

  • Property mode set to 100644
File size: 17.1 KB
Line 
1#-----------------------------------------------------------------------
2# TITLE:
3#       notebookdb.tcl
4#
5# AUTHOR:
6#       Will Duquette
7#
8# DESCRIPTION:
9#       Notebook application's notebook page database.  Create a new
10#       page database by calling notebookdb::notebookdb.  Any number
11#       may be created; a single database may be shared by multiple
12#       clients.  Note: no file locking is done.
13#
14# ERROR CODES:
15#       notebookdb defines the following error codes:
16#
17#       ::notebook::loaderror
18#           The requested -dbfile could not be loaded or created.
19#
20#       ::notebook::saveerror
21#           The -dbfile could not be saved.
22#
23#       These are environmental errors, which the caller can't
24#       reasonably check before requesting the failed action, so
25#       the codes are defined to ease error recovery.
26#
27#       All other errors are avoidable, and so the error code is not set.
28#
29# LICENSE:
30#       Copyright (C) 2002,2003,2004,2005 by William H. Duquette. 
31#       This file may be used subject to the terms in license.txt.
32#
33#-----------------------------------------------------------------------
34
35#-----------------------------------------------------------------------
36# Provided package
37
38package provide notebookdb 1.0
39
40package require trycatch
41package require snit
42package require markupparser
43
44namespace eval ::notebookdb::notebookdb:: {
45    namespace import -force ::trycatch::*
46}
47
48#-----------------------------------------------------------------------
49# Public Functions
50
51namespace eval ::notebookdb:: {
52    namespace export notebookdb
53}
54
55snit::type ::notebookdb::notebookdb {
56    #-------------------------------------------------------------------
57    # Creation Options
58
59    # The database file name.  It must be specified when the database
60    # is created.
61    option -dbfile ""
62
63    onconfigure -dbfile {value} {
64        if {"" != $options(-dbfile)} {
65            error "-dbfile cannot be changed after the notebookdb is created"
66        }
67        set options(-dbfile) $value
68    }
69
70    #------------------------------------------------------------------
71    # Instance Variables
72
73    # Array of database contents, indexed on page ID.  The page ID is
74    # the page name in lower case.
75    #
76    # id-$id    The ID itself.
77    # name-$id  The canonical casing for the page name.
78    # lock-$id  1 if the page is locked, 0 (or nonexistent) otherwise.
79    # page-$id  The page's text.
80    # time-$id  The page's modification time.
81    variable db
82    variable dbfields {id name lock page time}
83
84    # Observers array.  index is observer name, value is command to execute.
85    variable observers
86
87    # Flag: notify observers only if true.
88    variable notifyObservers 1
89
90    #
91    # CONSTRUCTOR
92    #
93
94    constructor {args} {
95        global auto_path
96
97        # Save the options
98        $self configurelist $args
99
100        set filename $options(-dbfile)
101
102        if {"" == $filename} {
103            error "required option -dbfile is missing."
104        }
105
106        # Next, try to load the file from disk, if it exists.
107        if {[file exists $filename]} {
108            interp create $self.loader
109            $self.loader alias page $self Loader.Page
110
111            try {
112                try {
113                    $self.loader eval [list source $filename]
114                } catch -msg result {
115                    throw notebookdb::loaderror \
116                        "Could not load notebook file '$filename': $result"
117                }
118            } finally {
119                interp delete $self.loader
120            }
121        }
122
123        # NEXT, create the database's slave interpreter and populate it.
124        interp create $self.interp
125        $self.interp eval [list set auto_path $auto_path]
126
127        $self.interp eval {
128            # Create an expander in the slave, but put it in the ::db::
129            # namespace so that it's somewhat hidden.
130            package require textutil::expander
131
132            namespace eval ::db:: {
133                ::textutil::expander PageExpander
134                PageExpander setbrackets {[@} {@]}
135                PageExpander errmode macro
136            }
137
138            # Alias its expand function back into the global namespace of
139            # the slave.
140            interp alias {} expand {} ::db::PageExpander expand
141        }
142
143        # Alias the database object itself into the slave.
144        $self.interp alias ::db::db $self
145    }
146   
147    # Destroys the interpreter; all other cleanup is done by snit.
148    destructor {
149        if {[info commands $self.interp] != ""} {
150            interp delete $self.interp
151        }
152    }
153
154    #
155    # Methods
156    #
157
158    # Evaluates its args in the database's slave interpreter
159    method eval {args} {
160        return [uplevel $self.interp eval $args]
161    }
162
163    # Aliases a command into the database's slave interpreter.
164    #
165    # newcmd    The new command to alias into the slave interpreter.
166    # targetcmd The command being aliased in.
167    # args      Any additional args to targetcmd.
168
169    method alias {newcmd targetcmd args} {
170        eval $self.interp alias $newcmd $targetcmd $args
171    }
172
173    # Tries to save the database to disk
174    method save {} {
175        set fname $options(-dbfile)
176
177        try {
178            $self SaveDatabase $fname
179        } catch -msg errmsg {
180            throw notebookdb::saveerror \
181                "Could not save notebook file:\n\n'$fname'\n\n$errmsg"
182        }
183    }
184
185    method backupname {} {
186        set fname $options(-dbfile)
187
188        return [file rootname $fname].bak
189    }
190
191    # Checks whether there's a page with a given name or not, and returns
192    # 1 if so and 0 otherwise
193    method exists {name} {
194        set id [string tolower $name]
195        return [info exists db(id-$id)]
196    }
197
198    # Retrieves the text of the named page.  The text is returned as is,
199    # with no expansion.
200    method get {name} {
201        set id [string tolower $name]
202        return $db(page-$id)
203    }
204
205    # Retrieves the text of the named page, with macros expanded.
206    method pageexpand {name} {
207        $self expand [$self get $name]
208    }
209
210    # Expands macros in the text in the context of the database.
211    method expand {text} {
212        $self.interp eval [list expand $text]
213    }
214
215    # Retrieves the canonical casing of the page name.
216    method name {name} {
217        set id [string tolower $name]
218        return $db(name-$id)
219    }
220
221    # If no format string is given, retrieves the timestamp (in seconds)
222    # of the named page.  Otherwise formats the time according to the format.
223    #
224    # name      The page name
225    # fmt       A [clock format] format string.
226    method pagetime {name {fmt ""}} {
227        set id [string tolower $name]
228
229        if {"" == $fmt} {
230            return $db(time-$id)
231        } else {
232            return [clock format $db(time-$id) -format $fmt]
233        }
234    }
235
236    # Returns a sorted list of pages, or "" if none match the pattern.
237    #
238    # pattern  Optional; the pattern to match.  Defaults to *
239    method pages {{pattern *}} {
240        set plist {}
241
242        foreach ndx [array names db "name-*"] {
243            if {[string match -nocase $pattern $db($ndx)]} {
244                lappend plist $db($ndx)
245            }
246        }
247
248        return [lsort -dictionary $plist]
249    }
250
251    # Returns a sorted list of page IDs, or "" if none match the pattern.
252    #
253    # pattern   Optional; the pattern to match.  Defaults to *
254    method pageids {{pattern *}} {
255        set plist {}
256
257        foreach ndx [array names db "id-*"] {
258            if {[string match -nocase $pattern $db($ndx)]} {
259                lappend plist $db($ndx)
260            }
261        }
262
263        return [lsort -dictionary $plist]
264    }
265
266    # Returns a list of pages, sorted by time starting with the most
267    # recent, or "" if none match the pattern.
268    #
269    # pattern   Optional; the pattern to match.  Defaults to *
270    #
271    # TBD: Consider keeping a list of recent edits in the database.
272    method pagesbytime {{pattern *}} {
273        set plist {}
274
275        foreach ndx [array names db "name-*"] {
276            if {[string match -nocase $pattern $db($ndx)]} {
277                lappend plist $db($ndx)
278            }
279        }
280       
281        return [lsort -decreasing \
282                    -command [list [myproc TimeCmp] $self] $plist]
283    }
284
285    # lsort comparison function for pagesbytime.
286    proc TimeCmp {self p1 p2} {
287        set t1 [$self pagetime $p1]
288        set t2 [$self pagetime $p2]
289
290        if {$t1 < $t2} {
291            return -1
292        } elseif {$t2 > $t1} {
293            return 1
294        } else {
295            return 0
296        }
297    }
298
299    # Returns a list of pages whose names or bodies contain the searchText.
300    method search {searchText} {
301        $self searchx byname all $searchText
302    }
303
304    # Returns a list of pages whose names or bodies contain the searchText.
305    # in time order.
306    method searchbytime {searchText} {
307        $self searchx bytime all $searchText
308    }
309
310    # Returns an unsorted list of matches
311    method searchx {sort mode searchText} {
312        set plist {}
313
314        set contentFlag [string equal $mode all]
315
316        foreach ndx [array names db "name-*"] {
317            set name $db($ndx)
318
319            # TBD: search strings containing special characters are
320            # problematic.  I need to fix that.
321            if {[string match -nocase "*$searchText*" $name]} {
322                lappend plist $name
323                continue
324            }
325           
326            if {$contentFlag &&
327                [string match -nocase "*$searchText*" [$self get $name]]} {
328                lappend plist $name
329                continue
330            }
331        }
332
333
334        return [$self $sort $plist]
335    }
336
337    method byname {pagelist} {
338        lsort -dictionary $pagelist
339    }
340
341    method bytime {pagelist} {
342        lsort -decreasing \
343            -command [list [myproc TimeCmp] $self] $pagelist
344    }
345
346    # Tries to lock the page.  A locked page cannot be edited (at least,
347    # not by another user of this notebookdb).  Returns 1 on
348    # success and 0 if it's already locked.
349    method lock {name} {
350        set id [string tolower $name]
351       
352        if {[info exists db(lock-$id)]} {
353            return 0
354        }
355
356        set db(lock-$id) 1
357    }
358
359    # Unlocks the page, whether it was locked or not.
360    method unlock {name} {
361        set id [string tolower $name]
362
363        if {[info exists db(lock-$id)]} {
364            unset db(lock-$id)
365        }
366    }
367
368    # Returns 1 if the page is locked, and 0 otherwise.
369    method locked {name} {
370        set id [string tolower $name]
371        return [info exists db(lock-$id)]
372    }
373
374    # Saves the text for the page.  The page must not be locked.  Does not
375    # write the database to disk.  Any trailing whitespace
376    # is trimmed, and a final newline is always added.  Also, sets the
377    # page's timestamp.
378    #
379    # name      The page name
380    # text      The new page text.
381    method set {name text} {
382        set id [string tolower $name]
383
384        if {[$self locked $name]} {
385            error "Error, '$name' is locked."
386        }
387
388        set db(id-$id)   $id
389        set db(name-$id) $name
390        set db(page-$id) "[string trimright $text]\n"
391        set db(time-$id) [clock seconds]
392
393        $self Notify set $name
394    }
395
396    # Deletes the page, which must not be locked.  Does not
397    # write the database to disk.
398    method delete {name} {
399        set id [string tolower $name]
400
401        if {[$self locked $name]} {
402            error "Error, '$name' is locked."
403        }
404       
405        # NOTE: Originally, this did a loop on
406        # [array names db(*-$id)].  Unfortunately, this means that
407        # if you delete Fred you also delete *-Fred.
408        foreach field $dbfields {
409            if {[info exists db($field-$id)]} {
410                unset db($field-$id)
411            }
412        }
413
414        $self Notify delete $name
415    }
416
417    # Renames a page, if possible.  If the newName maps to the same ID as
418    # the oldName, it's simply saved for later use.  If it already names some
419    # other existing page, an error is thrown.  Otherwise, finds all pages that
420    # might include a link to the old name, and renames the link in each one.
421    # finally, a new page is created with the old name, and the old page is
422    # deleted.
423    #
424    # It's an error if the page is locked.
425
426    method rename {oldName newName} {
427        set oldId [string tolower $oldName]
428        set newId [string tolower $newName]
429
430        # First, if the page doesn't exist, throw an error.
431        if {![$self exists $oldName]} {
432            error "Cannot rename '$oldName'; no such page."
433        }
434
435        # Next, if the page is locked, throw an error.
436        if {[$self locked $oldName]} {
437            error "Error, '$oldName' is locked."
438        }
439
440        # Next, if it's just changing the capitalization, change it and
441        # return.
442        if {[string equal $oldId $newId]} {
443            set db(name-$oldId) $newName
444            $self Notify set $newName
445            return
446        }
447
448        # Next, is the new name already in use?
449        if {[$self exists $newName]} {
450            error "The name '$newName' is already in use."
451        }
452
453        set notifyObservers 0
454
455        # Next, create the new page and delete the old one.
456        $self set $newName [$self get $oldName]
457        $self delete $oldName
458
459        # Next, update links.  Do this after creating the new page, as it
460        # guarantees that the new page exists when the other pages are
461        # updated.
462        foreach pname [$self search "$oldName"] {
463            set text [$self get $pname]
464            set text [::markupparser::renameLink $text $oldName $newName]
465            $self set $pname $text
466        }
467
468        set notifyObservers 1
469        $self Notify rename [list $oldName $newName]
470    }
471
472    # Calls pages to get a list of page names that match the pattern.  If the
473    # specified name is in the list, and has a predecessor, the name of the
474    # predecessor is returned; otherwise "".
475    #
476    # name         The page name
477    # pattern  A pattern for retrieving page names.
478    method prevpage {name {pattern "*"}} {
479        set id [string tolower $name]
480        set ids [$self pageids $pattern]
481
482        set ndx [lsearch $ids $id]
483
484        set pred [expr {$ndx - 1}]
485
486        if {$pred < 0} {
487            return ""
488        }
489
490        return [$self name [lindex $ids $pred]]
491    }
492
493    # Calls pages to get a list of page names that match the pattern.  If the
494    # specified name is in the list, and has a successor, the name of the
495    # successor is returned; otherwise "".
496    #
497    # name         The page name
498    # pattern  A pattern for retrieving page names.
499
500    method nextpage {name {pattern "*"}} {
501        set id [string tolower $name]
502        set ids [$self pageids $pattern]
503
504        set ndx [lsearch $ids $id]
505
506        if {$ndx == -1} {
507            return ""
508        }
509
510        set succ [expr {$ndx + 1}]
511
512        if {$succ == [llength $ids]} {
513            return ""
514        }
515
516        return [$self name [lindex $ids $succ]]
517    }
518
519
520    # Register observer.  obj is the observing object; command is the
521    # command to execute.
522    #
523    # Registered observers are notified when the notebookdb's contents is
524    # modified.  Two arguments will be appended to their command. 
525    # The first indicates the nature of the change; the second contains
526    # the relevant data, as shown:
527    #
528    #     Operation  Data
529    #     ---------  -----------------------
530    #     set        string: Page name
531    #     delete     string: Page name
532    #     rename     list: oldname newname
533    #
534    # Note that renames look like several sets followed by a delete.
535   
536    method register {obj command} {
537        set observers($obj) $command
538    }
539
540    # Unregister observer.  obj is the observing object.
541    method unregister {obj} {
542        unset observers($obj)
543    }
544
545
546    #
547    # Private Methods
548    #
549
550    # Notify
551    #
552    # Notifies observers, if any.
553    method Notify {operation page} {
554        if {!$notifyObservers} {
555            return
556        }
557
558        foreach name [array names observers] {
559            set cmd $observers($name)
560            lappend cmd $operation $page
561            uplevel \#0 $cmd
562        }
563
564        return
565    }
566
567    # SaveDatabase fname
568    #
569    # Save the database to the file.
570
571    method SaveDatabase {fname} {
572        if {[file exists $fname]} {
573            file rename -force -- $fname [$self backupname]
574        }
575
576        set f [open $fname w]
577
578        puts $f "# Notebook Database File"
579
580        foreach name [$self pages] {
581            puts $f "\n#--------------------------------------------------"
582            puts $f "# $name\n"
583       
584            puts $f [list page $name [$self get $name] [$self pagetime $name]]
585            puts $f ""
586        }
587
588        puts $f "\n# End of Notebook Database File"
589        close $f
590    }
591
592    #
593    # Database Loading Commands
594    #
595    # These commands are aliased into a temporary slave interpreter that's
596    # used to load the notebook file.
597    #
598
599    # Creates a page in the database.  Aliased to "page".
600    # TBD: combine this with method set in some way.
601    method Loader.Page {name text {time 0}} {
602        set id [string tolower $name]
603
604        set db(id-$id)   $id
605        set db(name-$id) $name
606        set db(page-$id) [string trimright $text]
607
608        if {$time == 0} {
609            set time [clock seconds]
610        }
611        set db(time-$id) $time
612    }
613}
Note: See TracBrowser for help on using the repository browser.