xml-mapping 0.8.1 → 0.9.1
Sign up to get free protection for your applications and to get access to all the features.
- data/ChangeLog +64 -3
- data/README +871 -173
- data/README_XPATH +40 -13
- data/Rakefile +37 -26
- data/TODO.txt +39 -8
- data/examples/README +5 -0
- data/examples/company_usage.intout +34 -22
- data/examples/documents_folders.rb +31 -0
- data/examples/documents_folders.xml +16 -0
- data/examples/documents_folders_usage.intin.rb +18 -0
- data/examples/documents_folders_usage.intout +46 -0
- data/examples/order_signature_enhanced_usage.intout +21 -11
- data/examples/order_usage.intin.rb +52 -5
- data/examples/order_usage.intout +154 -80
- data/examples/person.intin.rb +44 -0
- data/examples/person.intout +27 -0
- data/examples/person_mm.intin.rb +119 -0
- data/examples/person_mm.intout +114 -0
- data/examples/publication.intin.rb +44 -0
- data/examples/publication.intout +20 -0
- data/examples/reader.intin.rb +33 -0
- data/examples/reader.intout +19 -0
- data/examples/stringarray.rb +5 -0
- data/examples/stringarray.xml +10 -0
- data/examples/stringarray_usage.intin.rb +11 -0
- data/examples/stringarray_usage.intout +31 -0
- data/examples/time_augm.intout +19 -7
- data/examples/time_augm_loading.intin.rb +44 -0
- data/examples/time_augm_loading.intout +12 -0
- data/examples/time_node.intin.rb +79 -0
- data/examples/time_node.rb +3 -2
- data/examples/time_node_w_marshallers.intin.rb +48 -0
- data/examples/time_node_w_marshallers.intout +25 -0
- data/examples/time_node_w_marshallers.xml +9 -0
- data/examples/xpath_create_new.intout +132 -114
- data/examples/xpath_ensure_created.intout +86 -65
- data/examples/xpath_pathological.intout +16 -16
- data/examples/xpath_usage.intout +1 -1
- data/install.rb +1 -0
- data/lib/xml/mapping.rb +3 -1
- data/lib/xml/mapping/base.rb +442 -272
- data/lib/xml/mapping/core_classes_mapping.rb +32 -0
- data/lib/xml/mapping/standard_nodes.rb +176 -86
- data/lib/xml/mapping/version.rb +2 -2
- data/lib/xml/rexml_ext.rb +186 -0
- data/lib/xml/xxpath.rb +28 -265
- data/lib/xml/xxpath/steps.rb +345 -0
- data/lib/xml/xxpath_methods.rb +96 -0
- data/test/all_tests.rb +4 -1
- data/test/benchmark_fixtures.rb +14 -0
- data/test/{multiple_mappings.rb → bookmarks.rb} +0 -0
- data/test/company.rb +47 -0
- data/test/documents_folders.rb +11 -1
- data/test/examples_test.rb +29 -0
- data/test/fixtures/benchmark.xml +77 -0
- data/test/fixtures/company1.xml +9 -0
- data/test/fixtures/documents_folders.xml +0 -8
- data/test/fixtures/documents_folders2.xml +13 -19
- data/test/fixtures/triangle_m1.xml +17 -0
- data/test/fixtures/triangle_m2.xml +19 -0
- data/test/inheritance_test.rb +50 -0
- data/test/multiple_mappings_test.rb +155 -0
- data/test/rexml_xpath_benchmark.rb +29 -0
- data/test/triangle_mm.rb +57 -0
- data/test/xml_mapping_adv_test.rb +36 -1
- data/test/xml_mapping_test.rb +136 -7
- data/test/xpath_test.rb +154 -0
- data/test/xxpath_benchmark.rb +36 -0
- data/test/xxpath_benchmark.result1.txt +17 -0
- data/test/xxpath_methods_test.rb +61 -0
- metadata +139 -90
@@ -17,98 +17,119 @@ rootelt=d.root
|
|
17
17
|
XML::XXPath.new("/bar/baz[@key='work']").first(rootelt,:ensure_created=>true)
|
18
18
|
=> <baz key='work'> ... </>
|
19
19
|
d.write($stdout,2)
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
20
|
+
|
21
|
+
<foo>
|
22
|
+
<bar>
|
23
|
+
<baz key='work'>
|
24
|
+
Java
|
25
|
+
</baz>
|
26
|
+
<baz key='play'>
|
27
|
+
Ruby
|
28
|
+
</baz>
|
29
|
+
</bar>
|
30
|
+
</foo>### no change (path existed before)
|
28
31
|
|
29
32
|
|
30
33
|
XML::XXPath.new("/bar/baz[@key='42']").first(rootelt,:ensure_created=>true)
|
31
34
|
=> <baz key='42'/>
|
32
35
|
d.write($stdout,2)
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
36
|
+
|
37
|
+
<foo>
|
38
|
+
<bar>
|
39
|
+
<baz key='work'>
|
40
|
+
Java
|
41
|
+
</baz>
|
42
|
+
<baz key='play'>
|
43
|
+
Ruby
|
44
|
+
</baz>
|
37
45
|
<baz key='42'/>
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
### path was added
|
46
|
+
</bar>
|
47
|
+
</foo>### path was added
|
42
48
|
|
43
49
|
XML::XXPath.new("/bar/baz[@key='42']").first(rootelt,:ensure_created=>true)
|
44
50
|
=> <baz key='42'/>
|
45
51
|
d.write($stdout,2)
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
52
|
+
|
53
|
+
<foo>
|
54
|
+
<bar>
|
55
|
+
<baz key='work'>
|
56
|
+
Java
|
57
|
+
</baz>
|
58
|
+
<baz key='play'>
|
59
|
+
Ruby
|
60
|
+
</baz>
|
50
61
|
<baz key='42'/>
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
### no change this time
|
62
|
+
</bar>
|
63
|
+
</foo>### no change this time
|
55
64
|
|
56
65
|
XML::XXPath.new("/bar/baz[@key2='hello']").first(rootelt,:ensure_created=>true)
|
57
|
-
=> <baz
|
66
|
+
=> <baz key='work' key2='hello'> ... </>
|
58
67
|
d.write($stdout,2)
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
68
|
+
|
69
|
+
<foo>
|
70
|
+
<bar>
|
71
|
+
<baz key='work' key2='hello'>
|
72
|
+
Java
|
73
|
+
</baz>
|
74
|
+
<baz key='play'>
|
75
|
+
Ruby
|
76
|
+
</baz>
|
63
77
|
<baz key='42'/>
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
### this fit in the 1st "baz" element since
|
78
|
+
</bar>
|
79
|
+
</foo>### this fit in the 1st "baz" element since
|
68
80
|
### there was no "key2" attribute there before.
|
69
81
|
|
70
82
|
XML::XXPath.new("/bar/baz[2]").first(rootelt,:ensure_created=>true)
|
71
83
|
=> <baz key='play'> ... </>
|
72
84
|
d.write($stdout,2)
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
85
|
+
|
86
|
+
<foo>
|
87
|
+
<bar>
|
88
|
+
<baz key='work' key2='hello'>
|
89
|
+
Java
|
90
|
+
</baz>
|
91
|
+
<baz key='play'>
|
92
|
+
Ruby
|
93
|
+
</baz>
|
77
94
|
<baz key='42'/>
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
### no change
|
95
|
+
</bar>
|
96
|
+
</foo>### no change
|
82
97
|
|
83
98
|
XML::XXPath.new("/bar/baz[6]/@haha").first(rootelt,:ensure_created=>true)
|
84
|
-
=> #<XML::XXPath::Accessors::Attribute:
|
99
|
+
=> #<XML::XXPath::Accessors::Attribute:0x7f4876522658 @name="haha", @parent=<baz haha='[unset]'/>>
|
85
100
|
d.write($stdout,2)
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
101
|
+
|
102
|
+
<foo>
|
103
|
+
<bar>
|
104
|
+
<baz key='work' key2='hello'>
|
105
|
+
Java
|
106
|
+
</baz>
|
107
|
+
<baz key='play'>
|
108
|
+
Ruby
|
109
|
+
</baz>
|
90
110
|
<baz key='42'/>
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
### for there to be a 6th "baz" element, there must be 1st..5th "baz" elements
|
111
|
+
<baz/>
|
112
|
+
<baz/>
|
113
|
+
<baz haha='[unset]'/>
|
114
|
+
</bar>
|
115
|
+
</foo>### for there to be a 6th "baz" element, there must be 1st..5th "baz" elements
|
98
116
|
|
99
117
|
XML::XXPath.new("/bar/baz[6]/@haha").first(rootelt,:ensure_created=>true)
|
100
|
-
=> #<XML::XXPath::Accessors::Attribute:
|
118
|
+
=> #<XML::XXPath::Accessors::Attribute:0x7f4876516bc8 @name="haha", @parent=<baz haha='[unset]'/>>
|
101
119
|
d.write($stdout,2)
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
120
|
+
|
121
|
+
<foo>
|
122
|
+
<bar>
|
123
|
+
<baz key='work' key2='hello'>
|
124
|
+
Java
|
125
|
+
</baz>
|
126
|
+
<baz key='play'>
|
127
|
+
Ruby
|
128
|
+
</baz>
|
106
129
|
<baz key='42'/>
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
### no change this time
|
130
|
+
<baz/>
|
131
|
+
<baz/>
|
132
|
+
<baz haha='[unset]'/>
|
133
|
+
</bar>
|
134
|
+
</foo>### no change this time
|
114
135
|
|
@@ -24,14 +24,13 @@ newelt = XML::XXPath.new("bar/*").first(rootelt, :ensure_created=>true)
|
|
24
24
|
=> </>
|
25
25
|
|
26
26
|
d.write($stdout,2)
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
27
|
+
|
28
|
+
<foo>
|
29
|
+
<bar>
|
30
|
+
</>
|
31
|
+
</bar>
|
32
|
+
<bar/>
|
33
|
+
</foo>
|
35
34
|
### a new "unspecified" element was created
|
36
35
|
newelt.unspecified?
|
37
36
|
=> true
|
@@ -43,14 +42,15 @@ newelt.unspecified?
|
|
43
42
|
=> false
|
44
43
|
|
45
44
|
d.write($stdout,2)
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
45
|
+
|
46
|
+
<foo>
|
47
|
+
<bar>
|
48
|
+
<new-one>
|
49
|
+
hello!
|
50
|
+
</new-one>
|
51
|
+
</bar>
|
52
|
+
<bar/>
|
53
|
+
</foo>
|
54
54
|
### you could also set unspecified to false explicitly, as in:
|
55
55
|
newelt.unspecified=true
|
56
56
|
|
data/examples/xpath_usage.intout
CHANGED
@@ -44,7 +44,7 @@ path2.all(d)
|
|
44
44
|
## "first" raises XML::XXPathError in such cases...
|
45
45
|
path2.first(d)
|
46
46
|
XML::XXPathError: path not found: /foo/bar[2]/baz[4]
|
47
|
-
from ../lib/xml
|
47
|
+
from ../lib/xml/xxpath.rb:75:in `first'
|
48
48
|
|
49
49
|
##...unless we allow nil returns
|
50
50
|
path2.first(d,:allow_nil=>true)
|
data/install.rb
CHANGED
data/lib/xml/mapping.rb
CHANGED
@@ -1,14 +1,16 @@
|
|
1
1
|
# xml-mapping -- bidirectional Ruby-XML mapper
|
2
|
-
# Copyright (C) 2004
|
2
|
+
# Copyright (C) 2004-2006 Olaf Klischat
|
3
3
|
|
4
4
|
$:.unshift(File.dirname(__FILE__)+"/..")
|
5
5
|
|
6
6
|
require 'xml/mapping/base'
|
7
7
|
require 'xml/mapping/standard_nodes'
|
8
8
|
|
9
|
+
XML::Mapping.add_node_class XML::Mapping::Node
|
9
10
|
XML::Mapping.add_node_class XML::Mapping::TextNode
|
10
11
|
XML::Mapping.add_node_class XML::Mapping::NumericNode
|
11
12
|
XML::Mapping.add_node_class XML::Mapping::ObjectNode
|
12
13
|
XML::Mapping.add_node_class XML::Mapping::BooleanNode
|
13
14
|
XML::Mapping.add_node_class XML::Mapping::ArrayNode
|
14
15
|
XML::Mapping.add_node_class XML::Mapping::HashNode
|
16
|
+
XML::Mapping.add_node_class XML::Mapping::ChoiceNode
|
data/lib/xml/mapping/base.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
# xml-mapping -- bidirectional Ruby-XML mapper
|
2
|
-
# Copyright (C) 2004
|
2
|
+
# Copyright (C) 2004-2006 Olaf Klischat
|
3
3
|
|
4
4
|
require 'rexml/document'
|
5
5
|
require "xml/xxpath"
|
@@ -60,6 +60,14 @@ module XML
|
|
60
60
|
# Including XML::Mapping also adds all methods of
|
61
61
|
# XML::Mapping::ClassMethods to your class (as class methods).
|
62
62
|
#
|
63
|
+
# It is recommended that if your class does not have required
|
64
|
+
# +initialize+ method arguments. The XML loader attempts to create a
|
65
|
+
# new object using the +new+ method. If this fails because the
|
66
|
+
# initializer expects an argument, then the loader calls +allocate+
|
67
|
+
# instead. +allocate+ bypasses the initializer. If your class must
|
68
|
+
# have initializer arguments, then you should verify that bypassing
|
69
|
+
# the initializer is acceptable.
|
70
|
+
#
|
63
71
|
# As you may have noticed from the example, the node factory methods
|
64
72
|
# generally use XPath expressions to specify locations in the mapped
|
65
73
|
# XML document. To make this work, XML::Mapping relies on
|
@@ -70,45 +78,96 @@ module XML
|
|
70
78
|
# elements/documents in memory.
|
71
79
|
module Mapping
|
72
80
|
|
73
|
-
#
|
81
|
+
# defined mapping classes for a given root elt name and mapping
|
82
|
+
# name (nested map from root element name to mapping name to array
|
83
|
+
# of classes)
|
84
|
+
#
|
85
|
+
# can't really use a class variable for this because it must be
|
74
86
|
# shared by all class methods mixed into classes by including
|
75
87
|
# Mapping. See
|
76
88
|
# http://user.cs.tu-berlin.de/~klischat/mydocs/ruby/mixin_class_methods_global_state.txt.html
|
77
89
|
# for a more detailed discussion.
|
78
|
-
|
79
|
-
|
90
|
+
Classes_by_rootelt_names = {} #:nodoc:
|
91
|
+
class << Classes_by_rootelt_names
|
92
|
+
def create_classes_for rootelt_name, mapping
|
93
|
+
(self[rootelt_name] ||= {})[mapping] ||= []
|
94
|
+
end
|
95
|
+
def classes_for rootelt_name, mapping
|
96
|
+
(self[rootelt_name] || {})[mapping] || []
|
97
|
+
end
|
98
|
+
def remove_class rootelt_name, mapping, clazz
|
99
|
+
classes_for(rootelt_name, mapping).delete clazz
|
100
|
+
end
|
101
|
+
def ensure_exists rootelt_name, mapping, clazz
|
102
|
+
clazzes = create_classes_for(rootelt_name, mapping)
|
103
|
+
clazzes << clazz unless clazzes.include? clazz
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
80
107
|
|
81
108
|
def self.append_features(base) #:nodoc:
|
82
109
|
super
|
83
110
|
base.extend(ClassMethods)
|
84
|
-
|
111
|
+
Classes_by_rootelt_names.create_classes_for(base.default_root_element_name, :_default) << base
|
112
|
+
base.initializing_xml_mapping
|
85
113
|
end
|
86
114
|
|
87
|
-
|
88
|
-
#
|
89
|
-
#
|
115
|
+
# Finds a mapping class corresponding to the given XML root
|
116
|
+
# element name and mapping name. There may be more than one such class --
|
117
|
+
# in that case, the most recently defined one is returned
|
118
|
+
#
|
119
|
+
# This is the inverse operation to
|
90
120
|
# <class>.root_element_name (see
|
91
121
|
# XML::Mapping::ClassMethods.root_element_name).
|
92
|
-
def self.class_for_root_elt_name(name)
|
122
|
+
def self.class_for_root_elt_name(name, options={:mapping=>:_default})
|
93
123
|
# TODO: implement Hash read-only instead of this
|
94
124
|
# interface
|
95
|
-
|
96
|
-
Classes_w_default_rootelt_names[name]
|
125
|
+
Classes_by_rootelt_names.classes_for(name, options[:mapping])[-1]
|
97
126
|
end
|
98
127
|
|
128
|
+
# Finds a mapping class and mapping name corresponding to the
|
129
|
+
# given XML root element name. There may be more than one
|
130
|
+
# (class,mapping) tuple for a given root element name -- in that
|
131
|
+
# case, one of them is selected arbitrarily.
|
132
|
+
#
|
133
|
+
# returns [class,mapping]
|
134
|
+
def self.class_and_mapping_for_root_elt_name(name)
|
135
|
+
(Classes_by_rootelt_names[name] || {}).each_pair{|mapping,classes| return [classes[0],mapping] }
|
136
|
+
nil
|
137
|
+
end
|
99
138
|
|
100
|
-
|
101
|
-
|
102
|
-
|
139
|
+
# Xml-mapping-specific initializer.
|
140
|
+
#
|
141
|
+
# This will be called when a new instance is being initialized
|
142
|
+
# from an XML source, as well as after calling _class_._new_(args)
|
143
|
+
# (for the latter case to work, you'll have to make sure you call
|
144
|
+
# the inherited _initialize_ method)
|
145
|
+
#
|
146
|
+
# The :mapping keyword argument gives the mapping the instance is
|
147
|
+
# being initialized with. This is non-nil only when the instance
|
148
|
+
# is being initialized from an XML source (:mapping will contain
|
149
|
+
# the :mapping argument passed (explicitly or implicitly) to the
|
150
|
+
# load_from_... method).
|
151
|
+
#
|
152
|
+
# When the instance is being initialized because _class_._new_ was
|
153
|
+
# called, the :mapping argument is set to nil to show that the
|
154
|
+
# object is being initialized with respect to no specific mapping.
|
155
|
+
#
|
156
|
+
# The default implementation of this method calls obj_initializing
|
157
|
+
# on all nodes. You may overwrite this method to do your own
|
158
|
+
# initialization stuff; make sure to call +super+ in that case.
|
159
|
+
def initialize_xml_mapping(options={:mapping=>nil})
|
160
|
+
self.class.all_xml_mapping_nodes(:mapping=>options[:mapping]).each do |node|
|
161
|
+
node.obj_initializing(self,options[:mapping])
|
103
162
|
end
|
104
163
|
end
|
105
164
|
|
106
|
-
#
|
107
|
-
|
108
|
-
#
|
109
|
-
#
|
110
|
-
# inherit this behaviour.
|
165
|
+
# Initializer. Called (by Class#new) after _self_ was created
|
166
|
+
# using _new_.
|
167
|
+
#
|
168
|
+
# XML::Mapping's implementation calls #initialize_xml_mapping.
|
111
169
|
def initialize(*args)
|
170
|
+
super(*args)
|
112
171
|
initialize_xml_mapping
|
113
172
|
end
|
114
173
|
|
@@ -119,19 +178,21 @@ module XML
|
|
119
178
|
# object's class are processed (i.e. have their
|
120
179
|
# #xml_to_obj method called) in the order of their definition
|
121
180
|
# inside the class, then #post_load is called.
|
122
|
-
def fill_from_xml(xml)
|
123
|
-
|
124
|
-
|
181
|
+
def fill_from_xml(xml, options={:mapping=>:_default})
|
182
|
+
raise(MappingError, "undefined mapping: #{options[:mapping].inspect}") \
|
183
|
+
unless self.class.xml_mapping_nodes_hash.has_key?(options[:mapping])
|
184
|
+
pre_load xml, :mapping=>options[:mapping]
|
185
|
+
self.class.all_xml_mapping_nodes(:mapping=>options[:mapping]).each do |node|
|
125
186
|
node.xml_to_obj self, xml
|
126
187
|
end
|
127
|
-
post_load
|
188
|
+
post_load :mapping=>options[:mapping]
|
128
189
|
end
|
129
190
|
|
130
191
|
# This method is called immediately before _self_ is filled from
|
131
192
|
# an xml source. _xml_ is the source REXML::Element.
|
132
193
|
#
|
133
194
|
# The default implementation of this method is empty.
|
134
|
-
def pre_load(xml)
|
195
|
+
def pre_load(xml, options={:mapping=>:_default})
|
135
196
|
end
|
136
197
|
|
137
198
|
|
@@ -143,7 +204,7 @@ module XML
|
|
143
204
|
# exception to abandon the whole loading process.
|
144
205
|
#
|
145
206
|
# The default implementation of this method is empty.
|
146
|
-
def post_load
|
207
|
+
def post_load(options={:mapping=>:_default})
|
147
208
|
end
|
148
209
|
|
149
210
|
|
@@ -152,8 +213,8 @@ module XML
|
|
152
213
|
# (i.e. have their
|
153
214
|
# #obj_to_xml method called) in the order of their definition
|
154
215
|
# inside the class.
|
155
|
-
def fill_into_xml(xml)
|
156
|
-
self.class.all_xml_mapping_nodes.each do |node|
|
216
|
+
def fill_into_xml(xml, options={:mapping=>:_default})
|
217
|
+
self.class.all_xml_mapping_nodes(:mapping=>options[:mapping]).each do |node|
|
157
218
|
node.obj_to_xml self,xml
|
158
219
|
end
|
159
220
|
end
|
@@ -163,10 +224,10 @@ module XML
|
|
163
224
|
#
|
164
225
|
# This method calls #pre_save, then #fill_into_xml, then
|
165
226
|
# #post_save.
|
166
|
-
def save_to_xml
|
167
|
-
xml = pre_save
|
168
|
-
fill_into_xml
|
169
|
-
post_save
|
227
|
+
def save_to_xml(options={:mapping=>:_default})
|
228
|
+
xml = pre_save :mapping=>options[:mapping]
|
229
|
+
fill_into_xml xml, :mapping=>options[:mapping]
|
230
|
+
post_save xml, :mapping=>options[:mapping]
|
170
231
|
xml
|
171
232
|
end
|
172
233
|
|
@@ -181,47 +242,271 @@ module XML
|
|
181
242
|
# class name, with capital letters converted to lowercase and
|
182
243
|
# preceded by a dash, e.g. "MySampleClass" becomes
|
183
244
|
# "my-sample-class".
|
184
|
-
def pre_save
|
185
|
-
REXML::Element.new(self.class.root_element_name)
|
245
|
+
def pre_save(options={:mapping=>:_default})
|
246
|
+
REXML::Element.new(self.class.root_element_name(:mapping=>options[:mapping]))
|
186
247
|
end
|
187
248
|
|
188
249
|
# This method is called immediately after _self_'s state has been
|
189
250
|
# filled into an XML element.
|
190
251
|
#
|
191
252
|
# The default implementation does nothing.
|
192
|
-
def post_save(xml)
|
253
|
+
def post_save(xml, options={:mapping=>:_default})
|
193
254
|
end
|
194
255
|
|
195
256
|
|
196
257
|
# Save _self_'s state as XML into the file named _filename_.
|
197
258
|
# The XML is obtained by calling #save_to_xml.
|
198
|
-
def save_to_file(filename)
|
199
|
-
xml = save_to_xml
|
259
|
+
def save_to_file(filename, options={:mapping=>:_default})
|
260
|
+
xml = save_to_xml :mapping=>options[:mapping]
|
200
261
|
File.open(filename,"w") do |f|
|
201
|
-
|
262
|
+
REXML::Formatters::Transitive.new(2,false).write(xml,f)
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
|
267
|
+
# The instance methods of this module are automatically added as
|
268
|
+
# class methods to a class that includes XML::Mapping.
|
269
|
+
module ClassMethods
|
270
|
+
#ClassMethods = Module.new do # this is the alternative -- but see above for peculiarities
|
271
|
+
|
272
|
+
# all nodes of this class, in the order of their definition,
|
273
|
+
# hashed by mapping (hash mapping => array of nodes)
|
274
|
+
def xml_mapping_nodes_hash #:nodoc:
|
275
|
+
@xml_mapping_nodes ||= {}
|
276
|
+
end
|
277
|
+
|
278
|
+
# called on a class when it is being made a mapping class
|
279
|
+
# (i.e. immediately after XML::Mapping was included in it)
|
280
|
+
def initializing_xml_mapping #:nodoc:
|
281
|
+
@default_mapping = :_default
|
282
|
+
end
|
283
|
+
|
284
|
+
# Make _mapping_ the mapping to be used by default in future
|
285
|
+
# node declarations in this class. The default can be
|
286
|
+
# overwritten on a per-node basis by passing a :mapping option
|
287
|
+
# parameter to the node factory method
|
288
|
+
#
|
289
|
+
# The initial default mapping in a mapping class is :_default
|
290
|
+
def use_mapping mapping
|
291
|
+
@default_mapping = mapping
|
292
|
+
xml_mapping_nodes_hash[mapping] ||= [] # create empty mapping node list if
|
293
|
+
# there wasn't one before so future calls
|
294
|
+
# to load/save_xml etc. w/ this mapping don't raise
|
295
|
+
end
|
296
|
+
|
297
|
+
# return the current default mapping (:_default initially, or
|
298
|
+
# the value set with the latest call to use_mapping)
|
299
|
+
def default_mapping
|
300
|
+
@default_mapping
|
301
|
+
end
|
302
|
+
|
303
|
+
# Add getter and setter methods for a new attribute named _name_
|
304
|
+
# to this class. This is a convenience method intended to be
|
305
|
+
# called from Node class initializers.
|
306
|
+
def add_accessor(name)
|
307
|
+
name = name.id2name if name.kind_of? Symbol
|
308
|
+
unless self.instance_methods.include?(name)
|
309
|
+
self.module_eval <<-EOS
|
310
|
+
attr_reader :#{name}
|
311
|
+
EOS
|
312
|
+
end
|
313
|
+
unless self.instance_methods.include?("#{name}=")
|
314
|
+
self.module_eval <<-EOS
|
315
|
+
attr_writer :#{name}
|
316
|
+
EOS
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
# Create a new instance of this class from the XML contained in
|
321
|
+
# the file named _filename_. Calls load_from_xml internally.
|
322
|
+
def load_from_file(filename, options={:mapping=>:_default})
|
323
|
+
xml = REXML::Document.new(File.new(filename))
|
324
|
+
load_from_xml xml.root, :mapping=>options[:mapping]
|
325
|
+
end
|
326
|
+
|
327
|
+
# Create a new instance of this class from the XML contained in
|
328
|
+
# _xml_ (a REXML::Element).
|
329
|
+
#
|
330
|
+
# Allocates a new object, then calls fill_from_xml(_xml_) on
|
331
|
+
# it.
|
332
|
+
def load_from_xml(xml, options={:mapping=>:_default})
|
333
|
+
raise(MappingError, "undefined mapping: #{options[:mapping].inspect}") \
|
334
|
+
unless xml_mapping_nodes_hash.has_key?(options[:mapping])
|
335
|
+
# create the new object. It is recommended that the class
|
336
|
+
# have a no-argument initializer, so try new first. If that
|
337
|
+
# doesn't work, try allocate, which bypasses the initializer.
|
338
|
+
begin
|
339
|
+
obj = self.new
|
340
|
+
rescue ArgumentError # TODO: this may hide real errors.
|
341
|
+
# how to statically check whether
|
342
|
+
# self self.new accepts an empty
|
343
|
+
# argument list?
|
344
|
+
obj = self.allocate
|
345
|
+
end
|
346
|
+
obj.initialize_xml_mapping :mapping=>options[:mapping]
|
347
|
+
obj.fill_from_xml xml, :mapping=>options[:mapping]
|
348
|
+
obj
|
349
|
+
end
|
350
|
+
|
351
|
+
|
352
|
+
# array of all nodes defined in this class, in the order of
|
353
|
+
# their definition. Option :create specifies whether or not an
|
354
|
+
# empty array should be created and returned if there was none
|
355
|
+
# before (if not, an exception is raised). :mapping specifies
|
356
|
+
# the mapping the returned nodes must have been defined in; nil
|
357
|
+
# means return all nodes regardless of their mapping
|
358
|
+
def xml_mapping_nodes(options={:mapping=>nil,:create=>true})
|
359
|
+
unless options[:mapping]
|
360
|
+
return xml_mapping_nodes_hash.values.inject([]){|a1,a2|a1+a2}
|
361
|
+
end
|
362
|
+
options[:create] = true if options[:create].nil?
|
363
|
+
if options[:create]
|
364
|
+
xml_mapping_nodes_hash[options[:mapping]] ||= []
|
365
|
+
else
|
366
|
+
xml_mapping_nodes_hash[options[:mapping]] ||
|
367
|
+
raise(MappingError, "undefined mapping: #{options[:mapping].inspect}")
|
368
|
+
end
|
369
|
+
end
|
370
|
+
|
371
|
+
|
372
|
+
# enumeration of all nodes in effect when
|
373
|
+
# marshalling/unmarshalling this class, that is, nodes defined
|
374
|
+
# for this class as well as for its superclasses. The nodes are
|
375
|
+
# returned in the order of their definition, starting with the
|
376
|
+
# topmost superclass that has nodes defined. keyword arguments
|
377
|
+
# are the same as for #xml_mapping_nodes.
|
378
|
+
def all_xml_mapping_nodes(options={:mapping=>nil,:create=>true})
|
379
|
+
# TODO: we could return a dynamic Enumerable here, or cache
|
380
|
+
# the array...
|
381
|
+
result = []
|
382
|
+
if superclass and superclass.respond_to?(:all_xml_mapping_nodes)
|
383
|
+
result += superclass.all_xml_mapping_nodes options
|
384
|
+
end
|
385
|
+
result += xml_mapping_nodes options
|
202
386
|
end
|
387
|
+
|
388
|
+
|
389
|
+
# The "root element name" of this class (combined getter/setter
|
390
|
+
# method).
|
391
|
+
#
|
392
|
+
# The root element name is the name of the root element of the
|
393
|
+
# XML tree returned by <this class>.#save_to_xml (or, more
|
394
|
+
# specifically, <this class>.#pre_save). By default, this method
|
395
|
+
# returns the #default_root_element_name; you may call this
|
396
|
+
# method with an argument to set the root element name to
|
397
|
+
# something other than the default. The option argument :mapping
|
398
|
+
# specifies the mapping the root element is/will be defined in,
|
399
|
+
# it defaults to the current default mapping (:_default
|
400
|
+
# initially, or the value set with the latest call to
|
401
|
+
# use_mapping)
|
402
|
+
def root_element_name(name=nil, options={:mapping=>@default_mapping})
|
403
|
+
if Hash===name # ugly...
|
404
|
+
options=name; name=nil
|
405
|
+
end
|
406
|
+
@root_element_names ||= {}
|
407
|
+
if name
|
408
|
+
Classes_by_rootelt_names.remove_class root_element_name, options[:mapping], self
|
409
|
+
@root_element_names[options[:mapping]] = name
|
410
|
+
Classes_by_rootelt_names.create_classes_for(name, options[:mapping]) << self
|
411
|
+
end
|
412
|
+
@root_element_names[options[:mapping]] || default_root_element_name
|
413
|
+
end
|
414
|
+
|
415
|
+
# The default root element name for this class. Equals the class
|
416
|
+
# name, with all parent module names stripped, and with capital
|
417
|
+
# letters converted to lowercase and preceded by a dash;
|
418
|
+
# e.g. "Foo::Bar::MySampleClass" becomes "my-sample-class".
|
419
|
+
def default_root_element_name
|
420
|
+
self.name.split('::')[-1].gsub(/^(.)/){$1.downcase}.gsub(/(.)([A-Z])/){$1+"-"+$2.downcase}
|
421
|
+
end
|
422
|
+
|
203
423
|
end
|
204
424
|
|
205
425
|
|
426
|
+
|
427
|
+
# "polymorphic" load function. Turns the XML tree _xml_ into an
|
428
|
+
# object, which is returned. The class of the object and the
|
429
|
+
# mapping to be used for unmarshalling are automatically
|
430
|
+
# determined from the root element name of _xml_ using
|
431
|
+
# XML::Mapping.class_for_root_elt_name. If :mapping is non-nil,
|
432
|
+
# only root element names defined in that mapping will be
|
433
|
+
# considered (default is to consider all classes)
|
434
|
+
def self.load_object_from_xml(xml,options={:mapping=>nil})
|
435
|
+
if mapping = options[:mapping]
|
436
|
+
c = class_for_root_elt_name xml.name, :mapping=>mapping
|
437
|
+
else
|
438
|
+
c,mapping = class_and_mapping_for_root_elt_name(xml.name)
|
439
|
+
end
|
440
|
+
unless c
|
441
|
+
raise MappingError, "no mapping class for root element name #{xml.name}, mapping #{mapping.inspect}"
|
442
|
+
end
|
443
|
+
c.load_from_xml xml, :mapping=>mapping
|
444
|
+
end
|
445
|
+
|
446
|
+
# Like load_object_from_xml, but loads from the XML file named by
|
447
|
+
# _filename_.
|
448
|
+
def self.load_object_from_file(filename,options={:mapping=>nil})
|
449
|
+
xml = REXML::Document.new(File.new(filename))
|
450
|
+
load_object_from_xml xml.root, options
|
451
|
+
end
|
452
|
+
|
453
|
+
|
454
|
+
# Registers the new node class _c_ (must be a descendant of Node)
|
455
|
+
# with the xml-mapping framework.
|
456
|
+
#
|
457
|
+
# A new "factory method" will automatically be added to
|
458
|
+
# ClassMethods (and therefore to all classes that include
|
459
|
+
# XML::Mapping from now on); so you can call it from the body of
|
460
|
+
# your mapping class definition in order to create nodes of type
|
461
|
+
# _c_. The name of the factory method is derived by "underscoring"
|
462
|
+
# the (unqualified) name of _c_;
|
463
|
+
# e.g. _c_==<tt>Foo::Bar::MyNiftyNode</tt> will result in the
|
464
|
+
# creation of a factory method named +my_nifty_node+. The
|
465
|
+
# generated factory method creates and returns a new instance of
|
466
|
+
# _c_. The list of argument to _c_.new consists of _self_
|
467
|
+
# (i.e. the mapping class the factory method was called from)
|
468
|
+
# followed by the arguments passed to the factory method. You
|
469
|
+
# should always use the factory methods to create instances of
|
470
|
+
# node classes; you should never need to call a node class's
|
471
|
+
# constructor directly.
|
472
|
+
#
|
473
|
+
# For a demonstration, see the calls to +text_node+, +array_node+
|
474
|
+
# etc. in the examples along with the corresponding node classes
|
475
|
+
# TextNode, ArrayNode etc. (these predefined node classes are in
|
476
|
+
# no way "special"; they're added using add_node_class in
|
477
|
+
# mapping.rb just like any custom node classes would be).
|
478
|
+
def self.add_node_class(c)
|
479
|
+
meth_name = c.name.split('::')[-1].gsub(/^(.)/){$1.downcase}.gsub(/(.)([A-Z])/){$1+"_"+$2.downcase}
|
480
|
+
ClassMethods.module_eval <<-EOS
|
481
|
+
def #{meth_name}(*args)
|
482
|
+
#{c.name}.new(self,*args)
|
483
|
+
end
|
484
|
+
EOS
|
485
|
+
end
|
486
|
+
|
487
|
+
|
488
|
+
###### core node classes
|
489
|
+
|
206
490
|
# Abstract base class for all node types. As mentioned in the
|
207
491
|
# documentation for XML::Mapping, node types must be registered
|
208
|
-
# using add_node_class, and a corresponding "node
|
209
|
-
# (e.g. "text_node") will then be added as a class
|
210
|
-
# mapping classes. The node factory method is
|
211
|
-
# of the mapping classes as demonstrated in
|
212
|
-
# creates an instance of its corresponding node
|
213
|
-
# parameters to the node factory method,
|
214
|
-
# mapping class, will be passed to the
|
215
|
-
# type) and adds it to its owning mapping
|
216
|
-
# node object per node definition per
|
217
|
-
# object will handle all XML
|
218
|
-
# node, for all instances of
|
219
|
-
# the
|
220
|
-
# instance (fill_into_xml
|
221
|
-
# will call obj_to_xml
|
222
|
-
#
|
223
|
-
# REXML element the data is to be
|
224
|
-
# as well as the object the data
|
492
|
+
# using XML::Mapping.add_node_class, and a corresponding "node
|
493
|
+
# factory method" (e.g. "text_node") will then be added as a class
|
494
|
+
# method to your mapping classes. The node factory method is
|
495
|
+
# called from the body of the mapping classes as demonstrated in
|
496
|
+
# the examples. It creates an instance of its corresponding node
|
497
|
+
# type (the list of parameters to the node factory method,
|
498
|
+
# preceded by the owning mapping class, will be passed to the
|
499
|
+
# constructor of the node type) and adds it to its owning mapping
|
500
|
+
# class, so there is one node object per node definition per
|
501
|
+
# mapping class. That node object will handle all XML
|
502
|
+
# marshalling/unmarshalling for this node, for all instances of
|
503
|
+
# the mapping class. For this purpose, the marshalling and
|
504
|
+
# unmarshalling methods of a mapping class instance (fill_into_xml
|
505
|
+
# and fill_from_xml, respectively) will call obj_to_xml
|
506
|
+
# resp. xml_to_obj on all nodes of the mapping class, in the order
|
507
|
+
# of their definition, passing the REXML element the data is to be
|
508
|
+
# marshalled to/unmarshalled from as well as the object the data
|
509
|
+
# is to be read from/filled into.
|
225
510
|
#
|
226
511
|
# Node types that map some XML data to a single attribute of their
|
227
512
|
# mapping class (that should be most of them) shouldn't be
|
@@ -230,14 +515,55 @@ module XML
|
|
230
515
|
class Node
|
231
516
|
# Intializer, to be called from descendant classes. _owner_ is
|
232
517
|
# the mapping class this node is being defined in. It'll be
|
233
|
-
# stored in _@owner_.
|
234
|
-
|
518
|
+
# stored in _@owner_. @options will be set to a (possibly empty)
|
519
|
+
# hash containing the option arguments passed to
|
520
|
+
# _initialize_. Options :mapping, :reader and :writer will be
|
521
|
+
# handled, subclasses may handle additional options. See the
|
522
|
+
# section on defining nodes in the README for details.
|
523
|
+
def initialize(owner,*args)
|
235
524
|
@owner = owner
|
236
|
-
|
525
|
+
if Hash===args[-1]
|
526
|
+
@options = args[-1]
|
527
|
+
args = args[0..-2]
|
528
|
+
else
|
529
|
+
@options={}
|
530
|
+
end
|
531
|
+
@mapping = @options[:mapping] || owner.default_mapping
|
532
|
+
owner.xml_mapping_nodes(:mapping=>@mapping) << self
|
533
|
+
XML::Mapping::Classes_by_rootelt_names.ensure_exists owner.root_element_name, @mapping, owner
|
534
|
+
if @options[:reader]
|
535
|
+
# override xml_to_obj in this instance with invocation of
|
536
|
+
# @options[:reader]
|
537
|
+
class << self
|
538
|
+
alias_method :default_xml_to_obj, :xml_to_obj
|
539
|
+
def xml_to_obj(obj,xml)
|
540
|
+
begin
|
541
|
+
@options[:reader].call(obj,xml)
|
542
|
+
rescue ArgumentError
|
543
|
+
@options[:reader].call(obj,xml,self.method(:default_xml_to_obj))
|
544
|
+
end
|
545
|
+
end
|
546
|
+
end
|
547
|
+
end
|
548
|
+
if @options[:writer]
|
549
|
+
# override obj_to_xml in this instance with invocation of
|
550
|
+
# @options[:writer]
|
551
|
+
class << self
|
552
|
+
alias_method :default_obj_to_xml, :obj_to_xml
|
553
|
+
def obj_to_xml(obj,xml)
|
554
|
+
begin
|
555
|
+
@options[:writer].call(obj,xml)
|
556
|
+
rescue ArgumentError
|
557
|
+
@options[:writer].call(obj,xml,self.method(:default_obj_to_xml))
|
558
|
+
end
|
559
|
+
end
|
560
|
+
end
|
561
|
+
end
|
562
|
+
args
|
237
563
|
end
|
238
564
|
# This is called by the XML unmarshalling machinery when the
|
239
565
|
# state of an instance of this node's @owner is to be read from
|
240
|
-
# an XML
|
566
|
+
# an XML tree. _obj_ is the instance, _xml_ is the tree (a
|
241
567
|
# REXML::Element). The node must read "its" data from _xml_
|
242
568
|
# (using XML::XXPath or any other means) and store it to the
|
243
569
|
# corresponding parts (attributes etc.) of _obj_'s state.
|
@@ -246,32 +572,44 @@ module XML
|
|
246
572
|
end
|
247
573
|
# This is called by the XML unmarshalling machinery when the
|
248
574
|
# state of an instance of this node's @owner is to be stored
|
249
|
-
# into an XML
|
250
|
-
#
|
251
|
-
#
|
575
|
+
# into an XML tree. _obj_ is the instance, _xml_ is the tree (a
|
576
|
+
# REXML::Element). The node must extract "its" data from _obj_
|
577
|
+
# and store it to the corresponding parts (sub-elements,
|
252
578
|
# attributes etc.) of _xml_ (using XML::XXPath or any other
|
253
579
|
# means).
|
254
580
|
def obj_to_xml(obj,xml)
|
255
581
|
raise "abstract method called"
|
256
582
|
end
|
257
|
-
# Called when a new instance
|
258
|
-
#
|
259
|
-
#
|
260
|
-
#
|
261
|
-
|
583
|
+
# Called when a new instance of the mapping class this node
|
584
|
+
# belongs to is being initialized. _obj_ is the
|
585
|
+
# instance. _mapping_ is the mapping the initialization is
|
586
|
+
# happening with, if any: If the instance is being initialized
|
587
|
+
# as part of e.g. <tt>Class.load_from_file(name,
|
588
|
+
# :mapping=>:some_mapping</tt> or any other call that specifies
|
589
|
+
# a mapping, that mapping will be passed to this method. If the
|
590
|
+
# instance is being initialized normally with
|
591
|
+
# <tt>Class.new</tt>, _mapping_ is nil here.
|
592
|
+
#
|
593
|
+
# You may set up initial values for the attributes this node is
|
594
|
+
# responsible for here. Default implementation is empty.
|
595
|
+
def obj_initializing(obj,mapping)
|
596
|
+
end
|
597
|
+
# tell whether this node's data is present in _obj_ (when this
|
598
|
+
# method is called, _obj_ will be an instance of the mapping
|
599
|
+
# class this node was defined in). This method is currently used
|
600
|
+
# only by ChoiceNode when writing data back to XML. See
|
601
|
+
# ChoiceNode#obj_to_xml.
|
602
|
+
def is_present_in? obj
|
603
|
+
true
|
262
604
|
end
|
263
605
|
end
|
264
606
|
|
265
607
|
|
266
608
|
# Base class for node types that map some XML data to a single
|
267
|
-
# attribute of their mapping class.
|
268
|
-
# general "options" hash parameter which may be used to influence
|
269
|
-
# the creation of nodes in numerous ways, e.g. by providing
|
270
|
-
# default attribute values when there is no source data in the
|
271
|
-
# mapped XML.
|
609
|
+
# attribute of their mapping class.
|
272
610
|
#
|
273
|
-
# All node types that come with xml-mapping
|
274
|
-
# SingleAttributeNode.
|
611
|
+
# All node types that come with xml-mapping except one
|
612
|
+
# (ChoiceNode) inherit from SingleAttributeNode.
|
275
613
|
class SingleAttributeNode < Node
|
276
614
|
# Initializer. _owner_ is the owning mapping class (gets passed
|
277
615
|
# to the superclass initializer and therefore put into
|
@@ -282,10 +620,8 @@ module XML
|
|
282
620
|
# attribute of name attrname) is added to the mapping class
|
283
621
|
# (using attr_accessor).
|
284
622
|
#
|
285
|
-
#
|
286
|
-
#
|
287
|
-
# @options. Two entries -- :optional and :default_value -- in
|
288
|
-
# the options hash are already processed in SingleAttributeNode:
|
623
|
+
# In the initializer, two option arguments -- :optional and
|
624
|
+
# :default_value -- are processed in SingleAttributeNode:
|
289
625
|
#
|
290
626
|
# Supplying :default_value=>_obj_ makes _obj_ the _default
|
291
627
|
# value_ for this attribute. When unmarshalling (loading) an
|
@@ -296,43 +632,24 @@ module XML
|
|
296
632
|
#
|
297
633
|
# Providing just :optional=>true is equivalent to providing
|
298
634
|
# :default_value=>nil.
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
# initialize.
|
303
|
-
#
|
304
|
-
# For example (TextNode is a subclass of SingleAttributeNote):
|
305
|
-
#
|
306
|
-
# class Address
|
307
|
-
# include XML::Mapping
|
308
|
-
# text_node :city, "city", :optional=>true, :default_value=>"Berlin"
|
309
|
-
# end
|
310
|
-
#
|
311
|
-
# Here +Address+ is the _owner_, <tt>:city</tt> is the
|
312
|
-
# _attrname_,
|
313
|
-
# <tt>{:optional=>true,:default_value=>"Berlin"}</tt> is the
|
314
|
-
# @options, and ["city"] is the argument list that'll be passed
|
315
|
-
# to TextNode.initialize_impl. "city" is of course the XPath
|
316
|
-
# expression locating the XML sub-element this text node refers
|
317
|
-
# to; TextNode.initialize_impl stores it into @path.
|
318
|
-
def initialize(owner,attrname,*args)
|
319
|
-
super(owner)
|
320
|
-
@attrname = attrname
|
321
|
-
owner.add_accessor attrname
|
322
|
-
if Hash===args[-1]
|
323
|
-
@options = args[-1]
|
324
|
-
args = args[0..-2]
|
325
|
-
else
|
326
|
-
@options={}
|
327
|
-
end
|
635
|
+
def initialize(*args)
|
636
|
+
@attrname,*args = super(*args)
|
637
|
+
@owner.add_accessor @attrname
|
328
638
|
if @options[:optional] and not(@options.has_key?(:default_value))
|
329
639
|
@options[:default_value] = nil
|
330
640
|
end
|
331
641
|
initialize_impl(*args)
|
642
|
+
args
|
332
643
|
end
|
333
|
-
#
|
644
|
+
# this method was retained for compatibility with xml-mapping 0.8.
|
645
|
+
#
|
646
|
+
# It used to be the initializer to be implemented by subclasses. The
|
647
|
+
# arguments (args) are those still unprocessed by
|
648
|
+
# SingleAttributeNode's initializer.
|
649
|
+
#
|
650
|
+
# In xml-mapping 0.9 and up, you should just override initialize() and
|
651
|
+
# call super.initialize. The returned array is the same args array.
|
334
652
|
def initialize_impl(*args)
|
335
|
-
raise "abstract method called"
|
336
653
|
end
|
337
654
|
|
338
655
|
# Exception that may be used by implementations of
|
@@ -356,15 +673,14 @@ module XML
|
|
356
673
|
obj.send :"#{@attrname}=", @options[:default_value]
|
357
674
|
end
|
358
675
|
end
|
676
|
+
true
|
359
677
|
end
|
360
678
|
|
361
|
-
# (to be overridden by subclasses) Extract and return the
|
362
|
-
# attribute
|
363
|
-
# implementation
|
364
|
-
#
|
365
|
-
#
|
366
|
-
# _xml_, it should raise NoAttrValueSet in order to initiate
|
367
|
-
# proper handling of possibly supplied :optional and
|
679
|
+
# (to be overridden by subclasses) Extract and return the value
|
680
|
+
# of the attribute this node is responsible for (@attrname) from
|
681
|
+
# _xml_. If the implementation decides that the attribute value
|
682
|
+
# is "unset" in _xml_, it should raise NoAttrValueSet in order
|
683
|
+
# to initiate proper handling of possibly supplied :optional and
|
368
684
|
# :default_value options (you may use #default_when_xpath_err
|
369
685
|
# for this purpose).
|
370
686
|
def extract_attr_value(xml)
|
@@ -382,14 +698,17 @@ module XML
|
|
382
698
|
end
|
383
699
|
set_attr_value(xml, value)
|
384
700
|
end
|
701
|
+
true
|
385
702
|
end
|
386
|
-
# (to be overridden by subclasses) Write _value_
|
387
|
-
#
|
703
|
+
# (to be overridden by subclasses) Write _value_, which is the
|
704
|
+
# current value of the attribute this node is responsible for
|
705
|
+
# (@attrname), into (the correct sub-nodes, attributes,
|
706
|
+
# whatever) of _xml_.
|
388
707
|
def set_attr_value(xml, value)
|
389
708
|
raise "abstract method called"
|
390
709
|
end
|
391
|
-
def obj_initializing(obj) # :nodoc:
|
392
|
-
if @options.has_key?
|
710
|
+
def obj_initializing(obj,mapping) # :nodoc:
|
711
|
+
if @options.has_key?(:default_value) and (mapping==nil || mapping==@mapping)
|
393
712
|
begin
|
394
713
|
obj.send :"#{@attrname}=", @options[:default_value].clone
|
395
714
|
rescue
|
@@ -410,160 +729,11 @@ module XML
|
|
410
729
|
raise NoAttrValueSet, "Attribute #{@attrname} not set (XXPathError: #{err})"
|
411
730
|
end
|
412
731
|
end
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
# with the xml-mapping framework.
|
418
|
-
#
|
419
|
-
# A new "factory method" will automatically be added to
|
420
|
-
# ClassMethods (and therefore to all classes that include
|
421
|
-
# XML::Mapping from now on); so you can call it from the body of
|
422
|
-
# your mapping class definition in order to create nodes of type
|
423
|
-
# _c_. The name of the factory method is derived by "underscoring"
|
424
|
-
# the (unqualified) name of _c_;
|
425
|
-
# e.g. _c_==<tt>Foo::Bar::MyNiftyNode</tt> will result in the
|
426
|
-
# creation of a factory method named +my_nifty_node+. The
|
427
|
-
# generated factory method creates and returns a new instance of
|
428
|
-
# _c_. The list of argument to _c_.new consists of _self_
|
429
|
-
# (i.e. the mapping class the factory method was called from)
|
430
|
-
# followed by the arguments passed to the factory method. You
|
431
|
-
# should always use the factory methods to create instances of
|
432
|
-
# node classes; you should never need to call a node class's
|
433
|
-
# constructor directly.
|
434
|
-
#
|
435
|
-
# For a demonstration, see the calls to +text_node+, +array_node+
|
436
|
-
# etc. in the examples along with the corresponding node classes
|
437
|
-
# TextNode, ArrayNode etc. (these predefined node classes are in
|
438
|
-
# no way "special"; they're added using add_node_class in
|
439
|
-
# mapping.rb just like any custom node classes would be).
|
440
|
-
def self.add_node_class(c)
|
441
|
-
meth_name = c.name.split('::')[-1].gsub(/^(.)/){$1.downcase}.gsub(/(.)([A-Z])/){$1+"_"+$2.downcase}
|
442
|
-
ClassMethods.module_eval <<-EOS
|
443
|
-
def #{meth_name}(*args)
|
444
|
-
#{c.name}.new(self,*args)
|
445
|
-
end
|
446
|
-
EOS
|
447
|
-
end
|
448
|
-
|
449
|
-
|
450
|
-
# The instance methods of this module are automatically added as
|
451
|
-
# class methods to a class that includes XML::Mapping.
|
452
|
-
module ClassMethods
|
453
|
-
#ClassMethods = Module.new do # this is the alterbative -- but see above for peculiarities
|
454
|
-
|
455
|
-
# Add getter and setter methods for a new attribute named _name_
|
456
|
-
# to this class. This is a convenience method intended to be
|
457
|
-
# called from Node class initializers.
|
458
|
-
def add_accessor(name)
|
459
|
-
name = name.id2name if name.kind_of? Symbol
|
460
|
-
unless self.instance_methods.include?(name)
|
461
|
-
self.module_eval <<-EOS
|
462
|
-
attr_reader :#{name}
|
463
|
-
EOS
|
464
|
-
end
|
465
|
-
unless self.instance_methods.include?("#{name}=")
|
466
|
-
self.module_eval <<-EOS
|
467
|
-
attr_writer :#{name}
|
468
|
-
EOS
|
469
|
-
end
|
470
|
-
end
|
471
|
-
|
472
|
-
# Create a new instance of this class from the XML contained in
|
473
|
-
# the file named _filename_. Calls load_from_xml internally.
|
474
|
-
def load_from_file(filename)
|
475
|
-
xml = REXML::Document.new(File.new(filename))
|
476
|
-
load_from_xml(xml.root)
|
477
|
-
end
|
478
|
-
|
479
|
-
# Create a new instance of this class from the XML contained in
|
480
|
-
# _xml_ (a REXML::Element).
|
481
|
-
#
|
482
|
-
# Allocates a new object, then calls fill_from_xml(_xml_) on
|
483
|
-
# it.
|
484
|
-
def load_from_xml(xml)
|
485
|
-
obj = self.allocate
|
486
|
-
obj.initialize_xml_mapping
|
487
|
-
obj.fill_from_xml(xml)
|
488
|
-
obj
|
489
|
-
end
|
490
|
-
|
491
|
-
|
492
|
-
# array of all nodes types defined in this class, in the order
|
493
|
-
# of their definition
|
494
|
-
def xml_mapping_nodes
|
495
|
-
@xml_mapping_nodes ||= []
|
496
|
-
end
|
497
|
-
|
498
|
-
|
499
|
-
# enumeration of all nodes types in effect when
|
500
|
-
# marshalling/unmarshalling this class, that is, node types
|
501
|
-
# defined for this class as well as for its superclasses. The
|
502
|
-
# node types are returned in the order of their definition,
|
503
|
-
# starting with the topmost superclass that has node types
|
504
|
-
# defined.
|
505
|
-
def all_xml_mapping_nodes
|
506
|
-
# TODO: we could return a dynamic Enumerable here, or cache
|
507
|
-
# the array...
|
508
|
-
result = []
|
509
|
-
if superclass and superclass.respond_to?(:all_xml_mapping_nodes)
|
510
|
-
result += superclass.all_xml_mapping_nodes
|
511
|
-
end
|
512
|
-
result += xml_mapping_nodes
|
513
|
-
end
|
514
|
-
|
515
|
-
|
516
|
-
# The "root element name" of this class (combined getter/setter
|
517
|
-
# method).
|
518
|
-
#
|
519
|
-
# The root element name is the name of the root element of the
|
520
|
-
# XML tree returned by <this class>.#save_to_xml (or, more
|
521
|
-
# specifically, <this class>.#pre_save). By default, this method
|
522
|
-
# returns the #default_root_element_name; you may call this
|
523
|
-
# method with an argument to set the root element name to
|
524
|
-
# something other than the default.
|
525
|
-
def root_element_name(name=nil)
|
526
|
-
if name
|
527
|
-
Classes_w_nondefault_rootelt_names.delete(root_element_name)
|
528
|
-
Classes_w_default_rootelt_names.delete(root_element_name)
|
529
|
-
Classes_w_default_rootelt_names.delete(name)
|
530
|
-
|
531
|
-
@root_element_name = name
|
532
|
-
|
533
|
-
Classes_w_nondefault_rootelt_names[name]=self
|
534
|
-
end
|
535
|
-
@root_element_name || default_root_element_name
|
536
|
-
end
|
537
|
-
|
538
|
-
|
539
|
-
# The default root element name for this class. Equals the class
|
540
|
-
# name, with all parent module names stripped, and with capital
|
541
|
-
# letters converted to lowercase and preceded by a dash;
|
542
|
-
# e.g. "Foo::Bar::MySampleClass" becomes "my-sample-class".
|
543
|
-
def default_root_element_name
|
544
|
-
self.name.split('::')[-1].gsub(/^(.)/){$1.downcase}.gsub(/(.)([A-Z])/){$1+"-"+$2.downcase}
|
732
|
+
# (overridden) returns true if and only if the value of this
|
733
|
+
# node's attribute in _obj_ is non-nil.
|
734
|
+
def is_present_in? obj
|
735
|
+
nil != obj.send(:"#{@attrname}")
|
545
736
|
end
|
546
|
-
|
547
|
-
end
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
# "polymorphic" load function. Turns the XML tree _xml_ into an
|
552
|
-
# object, which is returned. The class of the object is
|
553
|
-
# automatically determined from the root element name of _xml_
|
554
|
-
# using XML::Mapping::class_for_root_elt_name.
|
555
|
-
def self.load_object_from_xml(xml)
|
556
|
-
unless c = class_for_root_elt_name(xml.name)
|
557
|
-
raise MappingError, "no mapping class for root element name #{xml.name}"
|
558
|
-
end
|
559
|
-
c.load_from_xml(xml)
|
560
|
-
end
|
561
|
-
|
562
|
-
# Like load_object_from_xml, but loads from the XML file named by
|
563
|
-
# _filename_.
|
564
|
-
def self.load_object_from_file(filename)
|
565
|
-
xml = REXML::Document.new(File.new(filename))
|
566
|
-
load_object_from_xml(xml.root)
|
567
737
|
end
|
568
738
|
|
569
739
|
end
|