source: tol/tolbaseBLT/lib/notebookdb/notebookdb.tcl @ 56ade3b

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

fixes #1932, se ha creado la rama trunk/tolbaseBLT copia de trunk/tolbase

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

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