xml-mapping 0.8
Sign up to get free protection for your applications and to get access to all the features.
- 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
|