xml-mapping 0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. data/LICENSE +56 -0
  2. data/README +386 -0
  3. data/README_XPATH +175 -0
  4. data/Rakefile +214 -0
  5. data/TODO.txt +32 -0
  6. data/doc/xpath_impl_notes.txt +119 -0
  7. data/examples/company.rb +34 -0
  8. data/examples/company.xml +26 -0
  9. data/examples/company_usage.intin.rb +19 -0
  10. data/examples/company_usage.intout +39 -0
  11. data/examples/order.rb +61 -0
  12. data/examples/order.xml +54 -0
  13. data/examples/order_signature_enhanced.rb +7 -0
  14. data/examples/order_signature_enhanced.xml +9 -0
  15. data/examples/order_signature_enhanced_usage.intin.rb +12 -0
  16. data/examples/order_signature_enhanced_usage.intout +16 -0
  17. data/examples/order_usage.intin.rb +73 -0
  18. data/examples/order_usage.intout +147 -0
  19. data/examples/time_augm.intin.rb +19 -0
  20. data/examples/time_augm.intout +23 -0
  21. data/examples/time_node.rb +27 -0
  22. data/examples/xpath_create_new.intin.rb +85 -0
  23. data/examples/xpath_create_new.intout +181 -0
  24. data/examples/xpath_docvsroot.intin.rb +30 -0
  25. data/examples/xpath_docvsroot.intout +34 -0
  26. data/examples/xpath_ensure_created.intin.rb +62 -0
  27. data/examples/xpath_ensure_created.intout +114 -0
  28. data/examples/xpath_pathological.intin.rb +42 -0
  29. data/examples/xpath_pathological.intout +56 -0
  30. data/examples/xpath_usage.intin.rb +51 -0
  31. data/examples/xpath_usage.intout +57 -0
  32. data/install.rb +40 -0
  33. data/lib/xml/mapping.rb +14 -0
  34. data/lib/xml/mapping/base.rb +563 -0
  35. data/lib/xml/mapping/standard_nodes.rb +343 -0
  36. data/lib/xml/mapping/version.rb +8 -0
  37. data/lib/xml/xxpath.rb +354 -0
  38. data/test/all_tests.rb +6 -0
  39. data/test/company.rb +54 -0
  40. data/test/documents_folders.rb +33 -0
  41. data/test/fixtures/bookmarks1.xml +24 -0
  42. data/test/fixtures/company1.xml +85 -0
  43. data/test/fixtures/documents_folders.xml +71 -0
  44. data/test/fixtures/documents_folders2.xml +30 -0
  45. data/test/multiple_mappings.rb +80 -0
  46. data/test/tests_init.rb +2 -0
  47. data/test/xml_mapping_adv_test.rb +84 -0
  48. data/test/xml_mapping_test.rb +182 -0
  49. data/test/xpath_test.rb +273 -0
  50. metadata +96 -0
@@ -0,0 +1,343 @@
1
+ # xml-mapping -- bidirectional Ruby-XML mapper
2
+ # Copyright (C) 2004,2005 Olaf Klischat
3
+
4
+ module XML
5
+
6
+ module Mapping
7
+
8
+ # Node factory function synopsis:
9
+ #
10
+ # text_node :_attrname_, _path_ [, :default_value=>_obj_]
11
+ # [, :optional=>true]
12
+ #
13
+ # Node that maps an XML node's text (the element's first child
14
+ # text node resp. the attribute's value) to a (string) attribute
15
+ # of the mapped object. Since TextNode inherits from
16
+ # SingleAttributeNode, the first argument to the node factory
17
+ # function is the attribute name (as a symbol). Handling of
18
+ # <tt>:default_value</tt> and <tt>:optional</tt> option arguments
19
+ # (if given) is also provided by the superclass -- see there for
20
+ # details.
21
+ class TextNode < SingleAttributeNode
22
+ # Initializer. _path_ (a string, the 2nd argument to the node
23
+ # factory function) is the XPath expression that locates the
24
+ # mapped node in the XML.
25
+ def initialize_impl(path)
26
+ @path = XML::XXPath.new(path)
27
+ end
28
+ def extract_attr_value(xml) # :nodoc:
29
+ default_when_xpath_err{ @path.first(xml).text }
30
+ end
31
+ def set_attr_value(xml, value) # :nodoc:
32
+ @path.first(xml,:ensure_created=>true).text = value
33
+ end
34
+ end
35
+
36
+ # Node factory function synopsis:
37
+ #
38
+ # numeric_node :_attrname_, _path_ [, :default_value=>_obj_]
39
+ # [, :optional=>true]
40
+ #
41
+ # Like TextNode, but interprets the XML node's text as a number
42
+ # (Integer or Float, depending on the nodes's text) and maps it to
43
+ # an Integer or Float attribute.
44
+ class NumericNode < SingleAttributeNode
45
+ def initialize_impl(path)
46
+ @path = XML::XXPath.new(path)
47
+ end
48
+ def extract_attr_value(xml) # :nodoc:
49
+ txt = default_when_xpath_err{ @path.first(xml).text }
50
+ begin
51
+ Integer(txt)
52
+ rescue ArgumentError
53
+ Float(txt)
54
+ end
55
+ end
56
+ def set_attr_value(xml, value) # :nodoc:
57
+ raise RuntimeError, "Not an integer: #{value}" unless Numeric===value
58
+ @path.first(xml,:ensure_created=>true).text = value.to_s
59
+ end
60
+ end
61
+
62
+ # (does somebody have a better name for this class?) base node
63
+ # class that provides an initializer which lets the user specify a
64
+ # means to marshal/unmarshal a Ruby object to/from XML. Used as
65
+ # the base class for nodes that map some sub-nodes of their XML
66
+ # tree to (Ruby-)sub-objects of their attribute.
67
+ class SubObjectBaseNode < SingleAttributeNode
68
+ # processes the keyword arguments :class, :marshaller, and
69
+ # :unmarshaller (_args_ is ignored). When this initiaizer
70
+ # returns, @options[:marshaller] and @options[:unmarshaller] are
71
+ # set to procs that marshal/unmarshal a Ruby object to/from an
72
+ # XML tree according to the keyword arguments that were passed
73
+ # to the initializer:
74
+ #
75
+ # You either supply a :class argument with a class implementing
76
+ # XML::Mapping -- in that case, the subtree will be mapped to an
77
+ # instance of that class (using load_from_xml
78
+ # resp. fill_into_xml). Or, you supply :marshaller and
79
+ # :unmarshaller arguments specifying explicit
80
+ # unmarshaller/marshaller procs. The :marshaller proc takes
81
+ # arguments _xml_,_value_ and must fill _value_ (the object to
82
+ # be marshalled) into _xml_; the :unmarshaller proc takes _xml_
83
+ # and must extract and return the object value from it. Or, you
84
+ # specify none of those arguments, in which case the name of the
85
+ # class to create will be automatically deduced from the root
86
+ # element name of the XML node (see
87
+ # XML::Mapping::load_object_from_xml,
88
+ # XML::Mapping::class_for_root_elt_name).
89
+ #
90
+ # If both :class and :marshaller/:unmarshaller arguments are
91
+ # supplied, the latter take precedence.
92
+ def initialize_impl(*args)
93
+ if @options[:class]
94
+ unless @options[:marshaller]
95
+ @options[:marshaller] = proc {|xml,value|
96
+ value.fill_into_xml(xml)
97
+ }
98
+ end
99
+ unless @options[:unmarshaller]
100
+ @options[:unmarshaller] = proc {|xml|
101
+ @options[:class].load_from_xml(xml)
102
+ }
103
+ end
104
+ end
105
+
106
+ unless @options[:marshaller]
107
+ @options[:marshaller] = proc {|xml,value|
108
+ value.fill_into_xml(xml)
109
+ if xml.unspecified?
110
+ xml.name = value.class.root_element_name
111
+ xml.unspecified = false
112
+ end
113
+ }
114
+ end
115
+ unless @options[:unmarshaller]
116
+ @options[:unmarshaller] = proc {|xml|
117
+ XML::Mapping.load_object_from_xml(xml)
118
+ }
119
+ end
120
+ end
121
+ end
122
+
123
+ # Node factory function synopsis:
124
+ #
125
+ # object_node :_attrname_, _path_ [, :default_value=>_obj_]
126
+ # [, :optional=>true]
127
+ # [, :class=>_c_]
128
+ # [, :marshaller=>_proc_]
129
+ # [, :unmarshaller=>_proc_]
130
+ #
131
+ # Node that maps a subtree in the source XML to a Ruby
132
+ # object. :_attrname_ and _path_ are again the attribute name
133
+ # resp. XPath expression of the mapped attribute; the keyword
134
+ # arguments <tt>:default_value</tt> and <tt>:optional</tt> are
135
+ # handled by the SingleAttributeNode superclass. The XML subnode
136
+ # named by _path_ is mapped to the attribute named by :_attrname_
137
+ # according to the keyword arguments <tt>:class</tt>,
138
+ # <tt>:marshaller</tt>, and <tt>:unmarshaller</tt>, which are
139
+ # handled by the SubObjectBaseNode superclass.
140
+ class ObjectNode < SubObjectBaseNode
141
+ # Initializer. _path_ (a string denoting an XPath expression) is
142
+ # the location of the subtree.
143
+ def initialize_impl(path)
144
+ super
145
+ @path = XML::XXPath.new(path)
146
+ end
147
+ def extract_attr_value(xml) # :nodoc:
148
+ @options[:unmarshaller].call(default_when_xpath_err{@path.first(xml)})
149
+ end
150
+ def set_attr_value(xml, value) # :nodoc:
151
+ @options[:marshaller].call(@path.first(xml,:ensure_created=>true), value)
152
+ end
153
+ end
154
+
155
+ # Node factory function synopsis:
156
+ #
157
+ # boolean_node :_attrname_, _path_,
158
+ # _true_value_, _false_value_ [, :default_value=>_obj_]
159
+ # [, :optional=>true]
160
+ #
161
+ # Node that maps an XML node's text (the element name resp. the
162
+ # attribute value) to a boolean attribute of the mapped
163
+ # object. The attribute named by :_attrname_ is mapped to/from the
164
+ # XML subnode named by the XPath expression _path_. _true_value_
165
+ # is the text the node must have in order to represent the +true+
166
+ # boolean value, _false_value_ (actually, any value other than
167
+ # _true_value_) is the text the node must have in order to
168
+ # represent the +false+ boolean value.
169
+ class BooleanNode < SingleAttributeNode
170
+ # Initializer.
171
+ def initialize_impl(path,true_value,false_value)
172
+ @path = XML::XXPath.new(path)
173
+ @true_value = true_value; @false_value = false_value
174
+ end
175
+ def extract_attr_value(xml) # :nodoc:
176
+ default_when_xpath_err{ @path.first(xml).text==@true_value }
177
+ end
178
+ def set_attr_value(xml, value) # :nodoc:
179
+ @path.first(xml,:ensure_created=>true).text = value ? @true_value : @false_value
180
+ end
181
+ end
182
+
183
+ # Node factory function synopsis:
184
+ #
185
+ # array_node :_attrname_, _per_arrelement_path_
186
+ # [, :default_value=>_obj_]
187
+ # [, :optional=>true]
188
+ # [, :class=>_c_]
189
+ # [, :marshaller=>_proc_]
190
+ # [, :unmarshaller=>_proc_]
191
+ #
192
+ # -or-
193
+ #
194
+ # array_node :_attrname_, _base_path_, _per_arrelement_path_
195
+ # [keyword args the same]
196
+ #
197
+ # Node that maps a sequence of sub-nodes of the XML tree to an
198
+ # attribute containing an array of Ruby objects, with each array
199
+ # element mapping to a corresponding member of the sequence of
200
+ # sub-nodes.
201
+ #
202
+ # If _base_path_ is not supplied, it is assumed to be
203
+ # "". _base_path_+<tt>"/"</tt>+_per_arrelement_path_ is an XPath
204
+ # expression that must "yield" the sequence of XML nodes that is
205
+ # to be mapped to the array. The difference between _base_path_
206
+ # and _per_arrelement_path_ becomes important when marshalling the
207
+ # array attribute back to XML. When that happens, _base_path_
208
+ # names the most specific common parent node of all the mapped
209
+ # sub-nodes, and _per_arrelement_path_ names (relative to
210
+ # _base_path_) the part of the path that is duplicated for each
211
+ # array element. For example, with _base_path_==<tt>"foo/bar"</tt>
212
+ # and _per_arrelement_path_==<tt>"hi/ho"</tt>, an array
213
+ # <tt>[x,y,z]</tt> will be written to an XML structure that looks
214
+ # like this:
215
+ #
216
+ # <foo>
217
+ # <bar>
218
+ # <hi>
219
+ # <ho>
220
+ # [marshalled object x]
221
+ # </ho>
222
+ # </hi>
223
+ # <hi>
224
+ # <ho>
225
+ # [marshalled object y]
226
+ # </ho>
227
+ # </hi>
228
+ # <hi>
229
+ # <ho>
230
+ # [marshalled object z]
231
+ # </ho>
232
+ # </hi>
233
+ # </bar>
234
+ # </foo>
235
+ class ArrayNode < SubObjectBaseNode
236
+ # Initializer, delegates to do_initialize. Called with keyword
237
+ # arguments and either 1 or 2 paths; the hindmost path argument
238
+ # passed is delegated to _per_arrelement_path_; the preceding
239
+ # path argument (if present, "" by default) is delegated to
240
+ # _base_path_.
241
+ def initialize_impl(path,path2=nil)
242
+ super
243
+ if path2
244
+ do_initialize(path,path2)
245
+ else
246
+ do_initialize("",path)
247
+ end
248
+ end
249
+ # "Real" initializer.
250
+ def do_initialize(base_path,per_arrelement_path)
251
+ per_arrelement_path=per_arrelement_path[1..-1] if per_arrelement_path[0]==?/
252
+ @base_path = XML::XXPath.new(base_path)
253
+ @per_arrelement_path = XML::XXPath.new(per_arrelement_path)
254
+ @reader_path = XML::XXPath.new(base_path+"/"+per_arrelement_path)
255
+ end
256
+ def extract_attr_value(xml) # :nodoc:
257
+ result = []
258
+ default_when_xpath_err{@reader_path.all(xml)}.each do |elt|
259
+ result << @options[:unmarshaller].call(elt)
260
+ end
261
+ result
262
+ end
263
+ def set_attr_value(xml, value) # :nodoc:
264
+ base_elt = @base_path.first(xml,:ensure_created=>true)
265
+ value.each do |arr_elt|
266
+ @options[:marshaller].call(@per_arrelement_path.create_new(base_elt), arr_elt)
267
+ end
268
+ end
269
+ end
270
+
271
+
272
+ # Node factory function synopsis:
273
+ #
274
+ # hash_node :_attrname_, _per_hashelement_path_, _key_path_
275
+ # [, :default_value=>_obj_]
276
+ # [, :optional=>true]
277
+ # [, :class=>_c_]
278
+ # [, :marshaller=>_proc_]
279
+ # [, :unmarshaller=>_proc_]
280
+ #
281
+ # - or -
282
+ #
283
+ # hash_node :_attrname_, _base_path_, _per_hashelement_path_, _key_path_
284
+ # [keyword args the same]
285
+ #
286
+ # Node that maps a sequence of sub-nodes of the XML tree to an
287
+ # attribute containing a hash of Ruby objects, with each hash
288
+ # value mapping to a corresponding member of the sequence of
289
+ # sub-nodes. The (string-valued) hash key associated with a hash
290
+ # value _v_ is mapped to the text of a specific sub-node of _v_'s
291
+ # sub-node.
292
+ #
293
+ # Analogously to ArrayNode, _base_path_ and _per_arrelement_path_
294
+ # define the XPath expression that "yields" the sequence of XML
295
+ # nodes, each of which maps to a value in the hash table. Relative
296
+ # to such a node, key_path_ names the node whose text becomes the
297
+ # associated hash key.
298
+ class HashNode < SubObjectBaseNode
299
+ # Initializer, delegates to do_initialize. Called with keyword
300
+ # arguments and either 2 or 3 paths; the hindmost path argument
301
+ # passed is delegated to _key_path_, the preceding path argument
302
+ # is delegated to _per_arrelement_path_, the path preceding that
303
+ # argument (if present, "" by default) is delegated to
304
+ # _base_path_. The meaning of the keyword arguments is the same
305
+ # as for ObjectNode.
306
+ def initialize_impl(path1,path2,path3=nil)
307
+ super
308
+ if path3
309
+ do_initialize(path1,path2,path3)
310
+ else
311
+ do_initialize("",path1,path2)
312
+ end
313
+ end
314
+ # "Real" initializer.
315
+ def do_initialize(base_path,per_hashelement_path,key_path)
316
+ per_hashelement_path=per_hashelement_path[1..-1] if per_hashelement_path[0]==?/
317
+ @base_path = XML::XXPath.new(base_path)
318
+ @per_hashelement_path = XML::XXPath.new(per_hashelement_path)
319
+ @key_path = XML::XXPath.new(key_path)
320
+ @reader_path = XML::XXPath.new(base_path+"/"+per_hashelement_path)
321
+ end
322
+ def extract_attr_value(xml) # :nodoc:
323
+ result = {}
324
+ default_when_xpath_err{@reader_path.all(xml)}.each do |elt|
325
+ key = @key_path.first(elt).text
326
+ value = @options[:unmarshaller].call(elt)
327
+ result[key] = value
328
+ end
329
+ result
330
+ end
331
+ def set_attr_value(xml, value) # :nodoc:
332
+ base_elt = @base_path.first(xml,:ensure_created=>true)
333
+ value.each_pair do |k,v|
334
+ elt = @per_hashelement_path.create_new(base_elt)
335
+ @options[:marshaller].call(elt,v)
336
+ @key_path.first(elt,:ensure_created=>true).text = k
337
+ end
338
+ end
339
+ end
340
+
341
+ end
342
+
343
+ end
@@ -0,0 +1,8 @@
1
+ # xml-mapping -- bidirectional Ruby-XML mapper
2
+ # Copyright (C) 2004,2005 Olaf Klischat
3
+
4
+ module XML
5
+ module Mapping
6
+ VERSION = '0.8'
7
+ end
8
+ end
@@ -0,0 +1,354 @@
1
+ # xpath.rb -- XPath implementation for Ruby, including write access
2
+ # Copyright (C) 2004,2005 Olaf Klischat
3
+
4
+ require 'rexml/document'
5
+
6
+ module XML
7
+
8
+ class XXPathError < RuntimeError
9
+ end
10
+
11
+ # Instances of this class hold (in a pre-compiled form) an XPath
12
+ # pattern. You call instance methods like +each+, +first+, +all+,
13
+ # <tt>create_new</tt> on instances of this class to apply the
14
+ # pattern to REXML elements.
15
+ class XXPath
16
+
17
+ # create and compile a new XPath. _xpathstr_ is the string
18
+ # representation (XPath pattern) of the path
19
+ def initialize(xpathstr)
20
+ @xpathstr = xpathstr # for error messages
21
+
22
+ xpathstr=xpathstr[1..-1] if xpathstr[0]==?/
23
+
24
+ # TODO: avoid code duplications
25
+ # maybe: build & create the procs using eval
26
+
27
+ @creator_procs = [ proc{|node,create_new| node} ]
28
+ @reader_proc = proc {|nodes| nodes}
29
+ xpathstr.split('/').reverse.each do |part|
30
+ prev_creator = @creator_procs[-1]
31
+ prev_reader = @reader_proc
32
+ case part
33
+ when /^(.*?)\[@(.*?)='(.*?)'\]$/
34
+ name,attr_name,attr_value = [$1,$2,$3]
35
+ @creator_procs << curr_creator = proc {|node,create_new|
36
+ prev_creator.call(Accessors.create_subnode_by_name_and_attr(node,create_new,
37
+ name,attr_name,attr_value),
38
+ create_new)
39
+ }
40
+ @reader_proc = proc {|nodes|
41
+ next_nodes = Accessors.subnodes_by_name_and_attr(nodes,
42
+ name,attr_name,attr_value)
43
+ if (next_nodes == [])
44
+ throw :not_found, [nodes,curr_creator]
45
+ else
46
+ prev_reader.call(next_nodes)
47
+ end
48
+ }
49
+ when /^(.*?)\[(.*?)\]$/
50
+ name,index = [$1,$2.to_i]
51
+ @creator_procs << curr_creator = proc {|node,create_new|
52
+ prev_creator.call(Accessors.create_subnode_by_name_and_index(node,create_new,
53
+ name,index),
54
+ create_new)
55
+ }
56
+ @reader_proc = proc {|nodes|
57
+ next_nodes = Accessors.subnodes_by_name_and_index(nodes,
58
+ name,index)
59
+ if (next_nodes == [])
60
+ throw :not_found, [nodes,curr_creator]
61
+ else
62
+ prev_reader.call(next_nodes)
63
+ end
64
+ }
65
+ when /^@(.*)$/
66
+ name = $1
67
+ @creator_procs << curr_creator = proc {|node,create_new|
68
+ prev_creator.call(Accessors.create_subnode_by_attr_name(node,create_new,name),
69
+ create_new)
70
+ }
71
+ @reader_proc = proc {|nodes|
72
+ next_nodes = Accessors.subnodes_by_attr_name(nodes,name)
73
+ if (next_nodes == [])
74
+ throw :not_found, [nodes,curr_creator]
75
+ else
76
+ prev_reader.call(next_nodes)
77
+ end
78
+ }
79
+ when '*'
80
+ @creator_procs << curr_creator = proc {|node,create_new|
81
+ prev_creator.call(Accessors.create_subnode_by_all(node,create_new),
82
+ create_new)
83
+ }
84
+ @reader_proc = proc {|nodes|
85
+ next_nodes = Accessors.subnodes_by_all(nodes)
86
+ if (next_nodes == [])
87
+ throw :not_found, [nodes,curr_creator]
88
+ else
89
+ prev_reader.call(next_nodes)
90
+ end
91
+ }
92
+ else
93
+ name = part
94
+ @creator_procs << curr_creator = proc {|node,create_new|
95
+ prev_creator.call(Accessors.create_subnode_by_name(node,create_new,name),
96
+ create_new)
97
+ }
98
+ @reader_proc = proc {|nodes|
99
+ next_nodes = Accessors.subnodes_by_name(nodes,name)
100
+ if (next_nodes == [])
101
+ throw :not_found, [nodes,curr_creator]
102
+ else
103
+ prev_reader.call(next_nodes)
104
+ end
105
+ }
106
+ end
107
+ end
108
+ end
109
+
110
+
111
+ # loop over all sub-nodes of _node_ that match this XPath.
112
+ def each(node,options={},&block)
113
+ all(node,options).each(&block)
114
+ end
115
+
116
+ # the first sub-node of _node_ that matches this XPath. If nothing
117
+ # matches, raise XXPathError unless :allow_nil=>true was provided.
118
+ #
119
+ # If :ensure_created=>true is provided, first() ensures that a
120
+ # match exists in _node_, creating one if none existed before.
121
+ #
122
+ # <tt>path.first(node,:create_new=>true)</tt> is equivalent
123
+ # to <tt>path.create_new(node)</tt>.
124
+ def first(node,options={})
125
+ a=all(node,options)
126
+ if a.empty?
127
+ if options[:allow_nil]
128
+ nil
129
+ else
130
+ raise XXPathError, "path not found: #{@xpathstr}"
131
+ end
132
+ else
133
+ a[0]
134
+ end
135
+ end
136
+
137
+ # Return an Enumerable with all sub-nodes of _node_ that match
138
+ # this XPath. Returns an empty Enumerable if no match was found.
139
+ #
140
+ # If :ensure_created=>true is provided, all() ensures that a match
141
+ # exists in _node_, creating one (and returning it as the sole
142
+ # element of the returned enumerable) if none existed before.
143
+ def all(node,options={})
144
+ raise "options not a hash" unless Hash===options
145
+ if options[:create_new]
146
+ return [ @creator_procs[-1].call(node,true) ]
147
+ else
148
+ last_nodes,rest_creator = catch(:not_found) do
149
+ return @reader_proc.call([node])
150
+ end
151
+ if options[:ensure_created]
152
+ [ rest_creator.call(last_nodes[0],false) ]
153
+ else
154
+ []
155
+ end
156
+ end
157
+ end
158
+
159
+ # create a completely new match of this XPath in
160
+ # <i>base_node</i>. "Completely new" means that a new node will be
161
+ # created for each path element, even if a matching node already
162
+ # existed in <i>base_node</i>.
163
+ #
164
+ # <tt>path.create_new(node)</tt> is equivalent to
165
+ # <tt>path.first(node,:create_new=>true)</tt>.
166
+ def create_new(base_node)
167
+ first(base_node,:create_new=>true)
168
+ end
169
+
170
+
171
+ module Accessors #:nodoc:
172
+
173
+ # we need a boolean "unspecified?" attribute for XML nodes --
174
+ # paths like "*" oder (somewhen) "foo|bar" create "unspecified"
175
+ # nodes that the user must then "specify" by setting their text
176
+ # etc. (or manually setting unspecified=false)
177
+ #
178
+ # This is mixed into the REXML::Element and
179
+ # XML::XXPath::Accessors::Attribute classes.
180
+ module UnspecifiednessSupport
181
+
182
+ def unspecified?
183
+ @xml_xpath_unspecified ||= false
184
+ end
185
+
186
+ def unspecified=(x)
187
+ @xml_xpath_unspecified = x
188
+ end
189
+
190
+ def self.included(mod)
191
+ mod.module_eval <<-EOS
192
+ alias_method :_text_orig, :text
193
+ alias_method :_textis_orig, :text=
194
+ def text
195
+ # we're suffering from the "fragile base class"
196
+ # phenomenon here -- we don't know whether the
197
+ # implementation of the class we get mixed into always
198
+ # calls text (instead of just accessing @text or so)
199
+ if unspecified?
200
+ "[UNSPECIFIED]"
201
+ else
202
+ _text_orig
203
+ end
204
+ end
205
+ def text=(x)
206
+ _textis_orig(x)
207
+ self.unspecified=false
208
+ end
209
+
210
+ alias_method :_nameis_orig, :name=
211
+ def name=(x)
212
+ _nameis_orig(x)
213
+ self.unspecified=false
214
+ end
215
+ EOS
216
+ end
217
+
218
+ end
219
+
220
+ class REXML::Element #:nodoc:
221
+ include UnspecifiednessSupport
222
+ end
223
+
224
+ # attribute node, half-way compatible
225
+ # with REXML's Element.
226
+ # REXML doesn't provide one...
227
+ #
228
+ # The all/first calls return instances of this class if they
229
+ # matched an attribute node.
230
+ class Attribute
231
+ attr_reader :parent, :name
232
+ attr_writer :name
233
+
234
+ def initialize(parent,name)
235
+ @parent,@name = parent,name
236
+ end
237
+
238
+ def self.new(parent,name,create)
239
+ if parent.attributes[name]
240
+ super(parent,name)
241
+ else
242
+ if create
243
+ parent.attributes[name] = "[unset]"
244
+ super(parent,name)
245
+ else
246
+ nil
247
+ end
248
+ end
249
+ end
250
+
251
+ # the value of the attribute.
252
+ def text
253
+ parent.attributes[@name]
254
+ end
255
+
256
+ def text=(x)
257
+ parent.attributes[@name] = x
258
+ end
259
+
260
+ def ==(other)
261
+ other.kind_of?(Attribute) and other.parent==parent and other.name==name
262
+ end
263
+
264
+ include UnspecifiednessSupport
265
+ end
266
+
267
+ # read accessors
268
+
269
+ for things in %w{name name_and_attr name_and_index attr_name all} do
270
+ self.module_eval <<-EOS
271
+ def self.subnodes_by_#{things}(nodes, *args)
272
+ nodes.map{|node| subnodes_by_#{things}_singlesrc(node,*args)}.flatten
273
+ end
274
+ EOS
275
+ end
276
+
277
+ def self.subnodes_by_name_singlesrc(node,name)
278
+ node.elements.select{|elt| elt.name==name}
279
+ end
280
+
281
+ def self.subnodes_by_name_and_attr_singlesrc(node,name,attr_name,attr_value)
282
+ node.elements.select{|elt| elt.name==name and elt.attributes[attr_name]==attr_value}
283
+ end
284
+
285
+ def self.subnodes_by_name_and_index_singlesrc(node,name,index)
286
+ index-=1
287
+ byname=subnodes_by_name_singlesrc(node,name)
288
+ if index>=byname.size
289
+ []
290
+ else
291
+ [byname[index]]
292
+ end
293
+ end
294
+
295
+ def self.subnodes_by_attr_name_singlesrc(node,name)
296
+ attr=Attribute.new(node,name,false)
297
+ if attr then [attr] else [] end
298
+ end
299
+
300
+ def self.subnodes_by_all_singlesrc(node)
301
+ node.elements.to_a
302
+ end
303
+
304
+
305
+ # write accessors
306
+
307
+ # precondition: unless create_new, we know that a node with
308
+ # exactly the requested attributes doesn't exist yet (else we
309
+ # wouldn't have been called)
310
+ def self.create_subnode_by_name(node,create_new,name)
311
+ node.elements.add name
312
+ end
313
+
314
+ def self.create_subnode_by_name_and_attr(node,create_new,name,attr_name,attr_value)
315
+ if create_new
316
+ newnode = node.elements.add(name)
317
+ else
318
+ newnode = subnodes_by_name_singlesrc(node,name)[0]
319
+ if not(newnode) or newnode.attributes[attr_name]
320
+ newnode = node.elements.add(name)
321
+ end
322
+ end
323
+ newnode.attributes[attr_name]=attr_value
324
+ newnode
325
+ end
326
+
327
+ def self.create_subnode_by_name_and_index(node,create_new,name,index)
328
+ name_matches = subnodes_by_name_singlesrc(node,name)
329
+ if create_new and (name_matches.size >= index)
330
+ raise XXPathError, "XPath (#{@xpathstr}): #{name}[#{index}]: create_new and element already exists"
331
+ end
332
+ newnode = name_matches[0]
333
+ (index-name_matches.size).times do
334
+ newnode = node.elements.add name
335
+ end
336
+ newnode
337
+ end
338
+
339
+ def self.create_subnode_by_attr_name(node,create_new,name)
340
+ if create_new and node.attributes[name]
341
+ raise XXPathError, "XPath (#{@xpathstr}): @#{name}: create_new and attribute already exists"
342
+ end
343
+ Attribute.new(node,name,true)
344
+ end
345
+
346
+ def self.create_subnode_by_all(node,create_new)
347
+ node = node.elements.add
348
+ node.unspecified = true
349
+ node
350
+ end
351
+ end
352
+ end
353
+
354
+ end