(in-package :web-user)
CL-XML: How To: Access Document Components: Modular Path Methods
20031024
james anderson (c)2003,
Combined Methods
(defVar *dm* (parse-document #4P"xml:documentation;howto;howto.xml"))
an application often needs to express combinations of operations on related sets of components which are derived from a given base document. for these purposes, the elementary accessor definitions described in the note on path accessors are too primitive.
this note demonstrates how to define functions which can be combined and reused flexibly.
the basic approach is shared with elementary accessors. that is, a path expressions is translated in to an iterator function which can be applied to a source to generate the matching nodes.
in order that the results can be combined and reused, an alternative interface to the path compiler is demonstrated in to form of two generic functions, all and each, and two iteration forms are demonstrated forall and foreach which combine to express complex operatons in simple terms.
(defGeneric all (source path-collector consumer)
(:documentation "
transform a path expression into a collector function and support various modes
of operation with that function. the general method for string path designators
compiles the path expression and delegates to the methods for a functional collector.
these methods distinguish various source types - nodes, nodes sets, generator
functions and a null source, as well as two consumer types - a function or null,
and generate either combinators or the collected node set depending on which
argument combination is provided.
")
(:method ((source t) (path-expression string) (consumer t))
"compile the path expression and delegate to the all method for a path iterator"
(multiple-value-bind (result index complete) (xpath-parser path-expression)
(cond (complete (all source (eval result) consumer))
(t
(warn "incomplete parse of xpath expression (@~s): ~s" index
path-expression)
nil))))
(:method ((node-or-node-set t) (iterator function) (consumer null))
"given a null consumer, return a function which accepts its own consumer and
which generates all matching nodes from the the original source"
#'(lambda (deferred-consumer)
(all node-or-node-set iterator deferred-consumer)))
(:method ((node null) (iterator function) (consumer null))
"given a null source and consumer, return a function which generates all matching
nodes given its own argument node or node-set and consumer"
#'(lambda (deferred-node-or-set deferred-consumer)
(all deferred-node-or-set iterator deferred-consumer)))
(:method ((node elem-property-node) (iterator function) (consumer function))
"call the given consumer with the collected matching nodes
for the given iterator and source property node"
(funcall consumer (call-with-collector (funcall iterator node))))
(:method ((node elem-node) (iterator function) (consumer function))
"call the given consumer with the collected matching nodes
for the given iterator and source element node"
(funcall consumer (call-with-collector (funcall iterator node))))
(:method ((node doc-node) (iterator function) (consumer function))
"call the given consumer with the collected matching nodes
for the given iterator and source document"
(funcall consumer (call-with-collector (funcall iterator node))))
(:method ((node-set list) (iterator function) (consumer function))
"call the given consumer with the collected matching nodes
for the given iterator and source node-set"
(let ((set-collection nil))
(flet
((collector (node-collection)
(setf set-collection (append set-collection node-collection))))
(declare (dynamic-extent collector))
(dolist (node node-set) (all node iterator #'collector))
(funcall consumer set-collection))))
(:method
((node-set-generator function) (iterator function) (consumer function))
"call the given consumer with the collected matching nodes
for the given iterator and a generated set of source nodes"
(let ((set-collection nil))
(flet
((collector (node-collection)
(setf set-collection (append set-collection node-collection))))
(declare (dynamic-extent collector))
(flet ((walker (node) (all node iterator #'collector)))
(declare (dynamic-extent #'walker))
(map-nodes #'walker node-set-generator))))))
(defGeneric each (node path-iterator consumer)
(:documentation "
transform a path expression into an iterator function and support various
modes of operation with that function. the general method for string path
designators compiles the path expression and delegates to the methods for
a functional iterator. these methods distinguish various source types -
nodes, nodes sets, generator functions and a null source, as well as two
consumer types - a function or null, and generate either combinators or
iterate over the node set depending on which argument combination is provided.
")
(:method ((node t) (path-expression string) (consumer t))
"compile the path expression and delegate to the each method for a path iterator"
(multiple-value-bind (result index complete) (xpath-parser path-expression)
(cond (complete (each node (eval result) consumer))
(t
(warn "incomplete parse of xpath expression (@~s): ~s" index
path-expression)
nil))))
(:method ((node t) (iterator function) (consumer null))
#'(lambda (deferred-consumer)
(map-nodes deferred-consumer (funcall iterator node))))
(:method ((node t) (iterator function) (consumer (eql t)))
(funcall iterator node))
(:method ((node null) (iterator function) (consumer null))
#'(lambda (deferred-node-or-set deferred-consumer)
(map-nodes deferred-consumer (funcall iterator deferred-node-or-set))))
(:method ((node elem-property-node) (iterator function) (consumer function))
(map-nodes consumer (funcall iterator node)))
(:method ((node elem-node) (iterator function) (consumer function))
(map-nodes consumer (funcall iterator node)))
(:method ((node doc-node) (iterator function) (consumer function))
(map-nodes consumer (funcall iterator node)))
(:method ((node-set list) (iterator function) (consumer function))
(dolist (node node-set) (each node iterator consumer)))
(:method
((node-set-generator function) (iterator function) (consumer function))
(flet ((walker (node) (each node iterator consumer)))
(declare (dynamic-extent #'walker))
(map-nodes #'walker node-set-generator))))
(defMacro with-collector
((collector) &rest body &aux (head (gensym)) (tail (gensym)))
(LIST* 'let*
(LIST* (LIST* (LIST* head '((list nil)))
(LIST (LIST* tail (LIST head))))
(LIST* (LIST* 'flet
(LIST* (LIST (LIST*
collector
(LIST*
'(node)
(LIST
(LIST*
'unless
(LIST*
(LIST*
'member
(LIST* 'node (LIST head)))
(LIST
(LIST*
'setf
(LIST*
(LIST* 'rest (LIST tail))
(LIST*
'(list node)
(LIST*
tail
(LIST
(LIST*
'rest
(LIST tail))))))))))))))
body))
(LIST (LIST* 'rest (LIST head)))))))
these could be used directly with the
howto document to extract all nodes which match a path and to iterate over those nodes.
(all *dm* "//@upc" #'(lambda (nodes) (mapcar #'value nodes)))
==
("123456789" "445322344" "485672034" "132957764")
or
(with-collector (collect)
(each *dm* "//@upc" #'(lambda (node) (collect (value node)))))
==
("123456789" "445322344" "485672034" "132957764")
Path Forms
in order to combine selection operations, one can simply combine functions like all and each.
(with-collector (collect)
(each *dm* "//item"
#'(lambda (item)
(each item "./@upc"
#'(lambda (upc)
(each item "./name"
#'(lambda (name)
(collect (cons (value-string name) (value upc))))))))))
== (("Invisibility Cream" . "123456789")
("Levitation Salve" . "445322344")
("Blork and Freen Instameal" . "485672034")
("Grob winglets" . "132957764"))
as this approach can prove unwieldy when combining selection and construction operations, it may be better,
as an alternative, to define forms which interleave iteration and construction.
(defMacro foreach (bindings &rest body)
(let
((iterator-vars
(mapcar #'(lambda (binding) (declare (ignore binding)) (gensym "I-"))
bindings)))
(LIST* 'let
(LIST* (mapcar
#'(lambda (i-var binding)
(destructuring-bind (n-var path source) binding
(declare (ignore n-var))
(LIST* i-var
(LIST (if (stringp path)
(LIST* (translate-xpath-accessor path)
(LIST source))
(LIST* 'each
(LIST*
source
(LIST* path '(t)))))))))
iterator-vars bindings)
(LIST (LIST* 'do
(LIST* (mapcar
#'(lambda
(i-var binding)
(destructuring-bind
(n-var path source)
binding
(declare (ignore path source))
(LIST*
n-var
(LIST*
(LIST* 'funcall (LIST i-var))
(LIST
(LIST* 'funcall (LIST i-var)))))))
iterator-vars bindings)
(LIST* (LIST
(LIST*
'not
(LIST
(LIST*
'and
(mapcar #'first bindings)))))
body))))))))
this approach can be be applied both for static paths ...
(with-collector (collect)
(foreach ((item "//item" *dm*))
(foreach ((upc "./@upc" item) (name "./name" item))
(collect (cons (value-string name) (value upc))))))
== (("Invisibility Cream" . "123456789")
("Levitation Salve" . "445322344")
("Blork and Freen Instameal" . "485672034")
("Grob winglets" . "132957764"))
and for dynamic path values
(defun document-items (document attribute-path)
(with-collector (collect)
(foreach ((item "//item" document))
(foreach ((upc attribute-path item) (name "./name" item))
(collect (cons (value-string name) (value upc)))))))
(document-items *dm* "./@upc")
== (("Invisibility Cream" . "123456789")
("Levitation Salve" . "445322344")
("Blork and Freen Instameal" . "485672034")
("Grob winglets" . "132957764"))
(document-items *dm* "./@stock")
== (("Invisibility Cream" . "12") ("Levitation Salve" . "18")
("Blork and Freen Instameal" . "653") ("Grob winglets" . "44"))
:eof