source: trunk/tcl/scripts/library.tcl @ 6

Last change on this file since 6 was 6, checked in by mmc, 20 years ago

Fixed the Tcl library to mirror the API developed for XML
libraries on the Python side. The Tcl Rappture::library
now has methods like "children", "element", "put", etc.
One difference: On the Tcl side, the default -flavor for
element/children is "component", since that works better
in Tcl code. In Python, the default is flavor=object.

Also fixed the Tcl install script to install not just
the tcl/scripts library, but also the ../gui and ../lib
directories.

File size: 19.2 KB
Line 
1# ----------------------------------------------------------------------
2#  COMPONENT: library - provides access to the XML library
3#
4#  These routines make it easy to load the XML description for a
5#  series of tool parameters and browse through the results.
6# ======================================================================
7#  AUTHOR:  Michael McLennan, Purdue University
8#  Copyright (c) 2004  Purdue Research Foundation, West Lafayette, IN
9# ======================================================================
10package require tdom
11package require Itcl
12
13namespace eval Rappture { # forward declaration }
14
15# ----------------------------------------------------------------------
16# USAGE: library ?-std? <file>
17# USAGE: library isvalid <object>
18#
19# Used to open a <file> containing an XML description of tool
20# parameters.  Loads the file and returns the name of the LibraryObj
21# file that represents it.
22#
23# If the -std flag is included, then the file is treated as the
24# name of a standard file, which is part of the Rappture installation.
25#
26# The isvalid operation checks an <object> to see if it is a valid
27# library object.  Returns 1 if so, and 0 otherwise.
28# ----------------------------------------------------------------------
29proc Rappture::library {args} {
30    # handle the isvalid operation...
31    if {[llength $args] > 1 && [lindex $args 0] == "isvalid"} {
32        if {[llength $args] != 2} {
33            error "wrong # args: should be \"library isvalid object\""
34        }
35        set obj [lindex $args 1]
36        if {[catch {$obj isa ::Rappture::LibraryObj} valid] == 0 && $valid} {
37            return 1
38        }
39        return 0
40    }
41
42    # handle the open operation...
43    set stdfile 0
44    while {[llength $args] > 1} {
45        set switch [lindex $args 0]
46        set args [lrange $args 1 end]
47        if {$switch == "-std"} {
48            set stdfile 1
49        } else {
50            error "bad option \"$switch\": should be -std"
51        }
52    }
53    set fname [lindex $args 0]
54
55    if {$stdfile && [file pathtype $fname] != "absolute"} {
56        set fname [file join $Rappture::installdir lib $fname]
57    }
58
59    # otherwise, try to open the file and create its LibraryObj
60    set fid [::open $fname r]
61    set info [read $fid]
62    close $fid
63
64    set obj [Rappture::LibraryObj ::#auto $info]
65    return $obj
66}
67
68# ----------------------------------------------------------------------
69itcl::class Rappture::LibraryObj {
70    constructor {info} { # defined below }
71    destructor { # defined below }
72
73    public method element {args}
74    public method children {args}
75    public method get {{path ""}}
76    public method put {args}
77    public method remove {{path ""}}
78    public method xml {}
79
80    protected method find {path}
81    protected method path2list {path}
82    protected method node2name {node}
83    protected method node2comp {node}
84
85    private variable _root 0       ;# non-zero => this obj owns document
86    private variable _document ""  ;# XML DOM tree
87    private variable _node ""      ;# node within
88}
89
90# ----------------------------------------------------------------------
91# CONSTRUCTOR
92# ----------------------------------------------------------------------
93itcl::body Rappture::LibraryObj::constructor {info} {
94    if {[regexp {<?[Xx][Mm][Ll]} $info]} {
95        set _root 1
96        set _document [dom parse $info]
97        set _node [$_document documentElement]
98    } elseif {[regexp {^domNode} $info]} {
99        set _root 0
100        set _document [$info ownerDocument]
101        set _node $info
102    } else {
103        error "bad info: should be XML text or DOM node"
104    }
105}
106
107# ----------------------------------------------------------------------
108# DESTRUCTOR
109# ----------------------------------------------------------------------
110itcl::body Rappture::LibraryObj::destructor {} {
111    if {$_root && $_document != ""} {
112        $_document delete
113    }
114}
115
116# ----------------------------------------------------------------------
117# USAGE: element ?-flavor <fval>? ?<path>?
118#
119# Clients use this to query a particular element within the entire
120# data structure.  The path is a string of the form
121# "structure.box(source).corner".  This example represents the tag
122# <corner> within a tag <box id="source"> within a tag <structure>,
123# which must be found at the top level within this document.
124#
125# By default, this method returns the component name "type(id)".
126# This is changed by setting the -flavor argument to "id" (for name
127# of the tail element), to "type" (for the type of the tail element),
128# to "object" (for an object representing the DOM node referenced by
129# the path.
130# ----------------------------------------------------------------------
131itcl::body Rappture::LibraryObj::element {args} {
132    array set params {
133        -flavor component
134    }
135    while {[llength $args] > 1} {
136        set first [lindex $args 0]
137        if {[string index $first 0] == "-"} {
138            set choices [array names params]
139            if {[lsearch $choices $first] < 0} {
140                error "bad option \"$first\": should be [join [lsort $choices] {, }]"
141            }
142            set params($first) [lindex $args 1]
143            set args [lrange $args 2 end]
144        } else {
145            break
146        }
147    }
148    if {[llength $args] > 1} {
149        error "wrong # args: should be \"element ?-flavor fval? ?path?\""
150    }
151    set path [lindex $args 0]
152
153    set node [find $path]
154    if {$node == ""} {
155        return ""
156    }
157
158    switch -- $params(-flavor) {
159      object {
160          return [::Rappture::LibraryObj ::#auto $node]
161      }
162      component {
163          return [node2comp $node]
164      }
165      id {
166          return [node2name $node]
167      }
168      type {
169          return [$node nodeName]
170      }
171      default {
172          error "bad flavor \"$params(-flavor)\": should be object, id, type, component"
173      }
174    }
175}
176
177# ----------------------------------------------------------------------
178# USAGE: children ?-flavor <fval>? ?-type <name>? ?<path>?
179#
180# Clients use this to query the children of a particular element
181# within the entire data structure.  This is just like the "element"
182# method, but it returns the children of the element instead of the
183# element itself.  If the optional -type argument is specified, then
184# the return list is restricted to children of the specified type.
185#
186# By default, this method returns a list of component names "type(id)".
187# This is changed by setting the -flavor argument to "id" (for tail
188# names of all children), to "type" (for the types of all children),
189# to "object" (for a list of objects representing the DOM nodes for
190# all children).
191# ----------------------------------------------------------------------
192itcl::body Rappture::LibraryObj::children {args} {
193    array set params {
194        -flavor component
195        -type ""
196    }
197    while {[llength $args] > 1} {
198        set first [lindex $args 0]
199        if {[string index $first 0] == "-"} {
200            set choices [array names params]
201            if {[lsearch $choices $first] < 0} {
202                error "bad option \"$first\": should be [join [lsort $choices] {, }]"
203            }
204            set params($first) [lindex $args 1]
205            set args [lrange $args 2 end]
206        } else {
207            break
208        }
209    }
210    if {[llength $args] > 1} {
211        error "wrong # args: should be \"children ?-flavor fval? ?-type name? ?path?\""
212    }
213    set path [lindex $args 0]
214
215    set node [find $path]
216    if {$node == ""} {
217        return ""
218    }
219
220    set nlist ""
221    foreach n [$node childNodes] {
222        set type [$n nodeName]
223        if {[regexp {^#} $type]} {
224            continue
225        }
226        if {$params(-type) != "" && $params(-type) != $type} {
227            continue
228        }
229        lappend nlist $n
230    }
231
232    set rlist ""
233    switch -- $params(-flavor) {
234      object {
235          foreach n $nlist {
236              lappend rlist [::Rappture::LibraryObj ::#auto $n]
237          }
238      }
239      component {
240          foreach n $nlist {
241              lappend rlist [node2comp $n]
242          }
243      }
244      id {
245          foreach n $nlist {
246              lappend rlist [node2name $n]
247          }
248      }
249      type {
250          foreach n $nlist {
251              lappend rlist [$n nodeName]
252          }
253      }
254      default {
255          error "bad flavor \"$params(-flavor)\": should be object, id, type, component"
256      }
257    }
258    return $rlist
259}
260
261# ----------------------------------------------------------------------
262# USAGE: get ?<path>?
263#
264# Clients use this to query the value of a node.  If the path is not
265# specified, it returns the value associated with the root node.
266# Otherwise, it returns the value for the element specified by the
267# path.
268# ----------------------------------------------------------------------
269itcl::body Rappture::LibraryObj::get {{path ""}} {
270    set node [find $path]
271    if {$node == ""} {
272        return ""
273    }
274    return [string trim [$node text]]
275}
276
277# ----------------------------------------------------------------------
278# USAGE: put ?-append yes? ?-id num? ?<path>? <string>
279#
280# Clients use this to set the value of a node.  If the path is not
281# specified, it sets the value for the root node.  Otherwise, it sets
282# the value for the element specified by the path.  If the value is a
283# string, then it is treated as the text within the tag at the tail
284# of the path.  If it is a DOM node or a library, then it is inserted
285# into the tree at the specified path.
286#
287# If the optional id is specified, then it sets the identifier for
288# the tag at the tail of the path.  If the optional append flag is
289# specified, then the value is appended to the current value.
290# Otherwise, the value replaces the current value.
291# ----------------------------------------------------------------------
292itcl::body Rappture::LibraryObj::put {args} {
293    array set params {
294        -id ""
295        -append no
296    }
297    while {[llength $args] > 1} {
298        set first [lindex $args 0]
299        if {[string index $first 0] == "-"} {
300            set choices [array names params]
301            if {[lsearch $choices $first] < 0} {
302                error "bad option \"$first\": should be [join [lsort $choices] {, }]"
303            }
304            set params($first) [lindex $args 1]
305            set args [lrange $args 2 end]
306        } else {
307            break
308        }
309    }
310    if {[llength $args] > 2} {
311        error "wrong # args: should be \"put ?-append bval? ?-id num? ?path? string\""
312    }
313    if {[llength $args] == 2} {
314        set path [lindex $args 0]
315        set str [lindex $args 1]
316    } else {
317        set path ""
318        set str [lindex $args 0]
319    }
320    set node [find -create $path]
321
322    #
323    # Clean up any nodes that don't belong.  If we're appending
324    # the value, then clean up only child <tag> nodes.  Otherwise,
325    # clean up all nodes.
326    #
327    set nlist ""
328    if {$params(-append)} {
329        foreach n [$node childNodes] {
330            if {[$n nodeType] != "TEXT_NODE"} {
331                lappend nlist $n
332            }
333        }
334    } else {
335        set nlist [$node childNodes]
336    }
337    foreach n $nlist {
338        $n delete
339    }
340
341    if {[Rappture::library isvalid $str]} {
342        error "not yet implemented"
343    } else {
344        set n [$_document createText $str]
345        $node appendChild $n
346        if {"" != $params(-id)} {
347            $node setAttribute id $params(-id)
348        }
349    }
350    return ""
351}
352
353# ----------------------------------------------------------------------
354# USAGE: remove ?<path>?
355#
356# Clients use this to remove the specified node.  Removes the node
357# from the tree.
358# ----------------------------------------------------------------------
359itcl::body Rappture::LibraryObj::remove {{path ""}} {
360    set node [find $path]
361    if {$node != ""} {
362        $node delete
363    }
364}
365
366# ----------------------------------------------------------------------
367# USAGE: xml
368#
369# Returns a string representing the XML information for the information
370# in this library.
371# ----------------------------------------------------------------------
372itcl::body Rappture::LibraryObj::xml {} {
373    return [$_node asXML]
374}
375
376# ----------------------------------------------------------------------
377# USAGE: find ?-create? <path>
378#
379# Used internally to find a particular element within the root node
380# according to the path, which is a string of the form
381# "typeNN(id).typeNN(id). ...", where each "type" is a tag <type>;
382# if the optional NN is specified, it indicates an index for the
383# <type> tag within its parent; if the optional (id) part is included,
384# it indicates a tag of the form <type id="id">.
385#
386# By default, it looks for an element along the path and returns None
387# if not found.  If the create flag is set, it creates various elements
388# along the path as it goes.  This is useful for "put" operations.
389#
390# If you include "#" instead of a specific number, a node will be
391# created automatically with a new number.  For example, the path
392# "foo.bar#" called the first time will create "foo.bar", the second
393# time "foo.bar1", the third time "foo.bar2" and so forth.
394#
395# Returns an object representing the element indicated by the path,
396# or "" if the path is not found.
397# ----------------------------------------------------------------------
398itcl::body Rappture::LibraryObj::find {args} {
399    set create 0
400    while {[llength $args] > 1} {
401        set first [lindex $args 0]
402        set args [lrange $args 1 end]
403        if {$first == "-create"} {
404            set create 1
405        } else {
406            error "bad option \"$first\": should be -create"
407        }
408    }
409    if {[llength $args] != 1} {
410        error "wrong # args: should be \"find ?-create? path\""
411    }
412    set path [lindex $args 0]
413
414    if {$path == ""} {
415        return $_node
416    }
417    set path [path2list $path]
418
419    #
420    # Follow the given path and look for all of the parts.
421    #
422    set lastnode $_node
423    set node $lastnode
424    foreach part $path {
425        if {![regexp {^(([a-zA-Z_]+#?)([0-9]*))?(\(([^\)]+)\))?$} $part \
426               match dummy type index dummy name]} {
427            error "bad path component \"$part\""
428        }
429        #
430        # If the name is like "type2", then look for elements with
431        # the type name and return the one with the given index.
432        # If the name is like "type", then assume the index is 0.
433        #
434        if {$name == ""} {
435            if {$index == ""} {
436                set index 0
437            }
438            set nlist [$node getElementsByTagName $type]
439            set node [lindex $nlist $index]
440        } else {
441            #
442            # If the name is like "type(id)", then look for elements
443            # that match the type and see if one has the requested name.
444            # if the name is like "(id)", then look for any elements
445            # with the requested name.
446            #
447            if {$type != ""} {
448                set nlist [$node getElementsByTagName $type]
449            } else {
450                set nlist [$node childNodes]
451            }
452            set found 0
453            foreach n $nlist {
454                if {[catch {$n getAttribute id} tag]} { set tag "" }
455                if {$tag == $name} {
456                    set found 1
457                    break
458                }
459            }
460            set node [expr {($found) ? $n : ""}]
461        }
462
463        if {$node == ""} {
464            if {!$create} {
465                return ""
466            }
467
468            #
469            # If the "create" flag is set, then create a node
470            # with the specified "type(id)" and continue on.
471            # If the type is "type#", then create a node with
472            # an automatic number.
473            #
474            if {![regexp {^([^\(]+)\(([^\)]+)\)$} $part match type name]} {
475                set type $part
476                set name ""
477            }
478
479            if {[string match *# $type]} {
480                set type [string trimright $type #]
481                set node [$_document createElement $type]
482
483                # find the last node of same type and append there
484                set pos ""
485                foreach n [$lastnode childNodes] {
486                    if {[$n nodeName] == $type} {
487                        set pos $n
488                    }
489                }
490                if {$pos != ""} {
491                    set pos [$pos nextSibling]
492                }
493                if {$pos != ""} {
494                    $lastnode insertBefore $node $pos
495                } else {
496                    $lastnode appendChild $node
497                }
498            } else {
499                set node [$_document createElement $type]
500                $lastnode appendChild $node
501            }
502            if {"" != $name} {
503                $node setAttribute id $name
504            }
505        }
506        set lastnode $node
507    }
508    return $node
509}
510
511# ----------------------------------------------------------------------
512# USAGE: path2list <path>
513#
514# Converts a path of the form "foo(a).bar.baz" into a list of the
515# form "foo(a) bar baz".  This is a little more complicated than
516# splitting on the .'s, since the stuff in ()'s might have embedded
517# .'s.  Returns a proper Tcl list for all elements of the path.
518# ----------------------------------------------------------------------
519itcl::body Rappture::LibraryObj::path2list {path} {
520    #
521    # Normally, we just split on .'s within the path.  But there
522    # might be some .'s embedded within ()'s in the path.  Change
523    # any embedded .'s to an out-of-band character, then split on
524    # the .'s, and change the embedded .'s back.
525    #
526    if {[regexp {(\([^\)]*)\.([^\)]*\))} $path]} {
527        while {[regsub -all {(\([^\)]*)\.([^\)]*\))} $path "\\1\007\\2" path]} {
528            # keep fixing...
529        }
530    }
531    set path [split $path .]
532    regsub -all {\007} $path {.} path
533
534    return $path
535}
536
537# ----------------------------------------------------------------------
538# USAGE: node2name <node>
539#
540# Used internally to create a name for the specified node.  If the
541# node doesn't have a specific name ("id" attribute) then a name of
542# the form "type123" is constructed.
543# ----------------------------------------------------------------------
544itcl::body Rappture::LibraryObj::node2name {node} {
545    if {[catch {$node getAttribute id} name]} { set name "" }
546    if {$name == ""} {
547        set pnode [$node parentNode]
548        if {$pnode == ""} {
549            return ""
550        }
551        set type [$node nodeName]
552        set siblings [$pnode getElementsByTagName $type]
553        set index [lsearch $siblings $node]
554        if {$index == 0} {
555            set name $type
556        } else {
557            set name "$type$index"
558        }
559    }
560    return $name
561}
562
563# ----------------------------------------------------------------------
564# USAGE: node2comp <node>
565#
566# Used internally to create a path component name for the specified
567# node.  A path component name has the form "type(id)" or just
568# "type##" if the node doesn't have a name.  This name can be used
569# in a path to uniquely address the component.
570# ----------------------------------------------------------------------
571itcl::body Rappture::LibraryObj::node2comp {node} {
572    set type [$node nodeName]
573    if {[catch {$node getAttribute id} name]} { set name "" }
574    if {$name == ""} {
575        set pnode [$node parentNode]
576        if {$pnode == ""} {
577            return ""
578        }
579        set siblings [$pnode getElementsByTagName $type]
580        set index [lsearch $siblings $node]
581        if {$index == 0} {
582            set name $type
583        } else {
584            set name "$type$index"
585        }
586    } else {
587        set name "${type}($name)"
588    }
589    return $name
590}
Note: See TracBrowser for help on using the repository browser.