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.
- data/LICENSE +56 -0
- data/README +386 -0
- data/README_XPATH +175 -0
- data/Rakefile +214 -0
- data/TODO.txt +32 -0
- data/doc/xpath_impl_notes.txt +119 -0
- data/examples/company.rb +34 -0
- data/examples/company.xml +26 -0
- data/examples/company_usage.intin.rb +19 -0
- data/examples/company_usage.intout +39 -0
- data/examples/order.rb +61 -0
- data/examples/order.xml +54 -0
- data/examples/order_signature_enhanced.rb +7 -0
- data/examples/order_signature_enhanced.xml +9 -0
- data/examples/order_signature_enhanced_usage.intin.rb +12 -0
- data/examples/order_signature_enhanced_usage.intout +16 -0
- data/examples/order_usage.intin.rb +73 -0
- data/examples/order_usage.intout +147 -0
- data/examples/time_augm.intin.rb +19 -0
- data/examples/time_augm.intout +23 -0
- data/examples/time_node.rb +27 -0
- data/examples/xpath_create_new.intin.rb +85 -0
- data/examples/xpath_create_new.intout +181 -0
- data/examples/xpath_docvsroot.intin.rb +30 -0
- data/examples/xpath_docvsroot.intout +34 -0
- data/examples/xpath_ensure_created.intin.rb +62 -0
- data/examples/xpath_ensure_created.intout +114 -0
- data/examples/xpath_pathological.intin.rb +42 -0
- data/examples/xpath_pathological.intout +56 -0
- data/examples/xpath_usage.intin.rb +51 -0
- data/examples/xpath_usage.intout +57 -0
- data/install.rb +40 -0
- data/lib/xml/mapping.rb +14 -0
- data/lib/xml/mapping/base.rb +563 -0
- data/lib/xml/mapping/standard_nodes.rb +343 -0
- data/lib/xml/mapping/version.rb +8 -0
- data/lib/xml/xxpath.rb +354 -0
- data/test/all_tests.rb +6 -0
- data/test/company.rb +54 -0
- data/test/documents_folders.rb +33 -0
- data/test/fixtures/bookmarks1.xml +24 -0
- data/test/fixtures/company1.xml +85 -0
- data/test/fixtures/documents_folders.xml +71 -0
- data/test/fixtures/documents_folders2.xml +30 -0
- data/test/multiple_mappings.rb +80 -0
- data/test/tests_init.rb +2 -0
- data/test/xml_mapping_adv_test.rb +84 -0
- data/test/xml_mapping_test.rb +182 -0
- data/test/xpath_test.rb +273 -0
- 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
|
data/lib/xml/xxpath.rb
ADDED
@@ -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
|