xml-mapping 0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. data/LICENSE +56 -0
  2. data/README +386 -0
  3. data/README_XPATH +175 -0
  4. data/Rakefile +214 -0
  5. data/TODO.txt +32 -0
  6. data/doc/xpath_impl_notes.txt +119 -0
  7. data/examples/company.rb +34 -0
  8. data/examples/company.xml +26 -0
  9. data/examples/company_usage.intin.rb +19 -0
  10. data/examples/company_usage.intout +39 -0
  11. data/examples/order.rb +61 -0
  12. data/examples/order.xml +54 -0
  13. data/examples/order_signature_enhanced.rb +7 -0
  14. data/examples/order_signature_enhanced.xml +9 -0
  15. data/examples/order_signature_enhanced_usage.intin.rb +12 -0
  16. data/examples/order_signature_enhanced_usage.intout +16 -0
  17. data/examples/order_usage.intin.rb +73 -0
  18. data/examples/order_usage.intout +147 -0
  19. data/examples/time_augm.intin.rb +19 -0
  20. data/examples/time_augm.intout +23 -0
  21. data/examples/time_node.rb +27 -0
  22. data/examples/xpath_create_new.intin.rb +85 -0
  23. data/examples/xpath_create_new.intout +181 -0
  24. data/examples/xpath_docvsroot.intin.rb +30 -0
  25. data/examples/xpath_docvsroot.intout +34 -0
  26. data/examples/xpath_ensure_created.intin.rb +62 -0
  27. data/examples/xpath_ensure_created.intout +114 -0
  28. data/examples/xpath_pathological.intin.rb +42 -0
  29. data/examples/xpath_pathological.intout +56 -0
  30. data/examples/xpath_usage.intin.rb +51 -0
  31. data/examples/xpath_usage.intout +57 -0
  32. data/install.rb +40 -0
  33. data/lib/xml/mapping.rb +14 -0
  34. data/lib/xml/mapping/base.rb +563 -0
  35. data/lib/xml/mapping/standard_nodes.rb +343 -0
  36. data/lib/xml/mapping/version.rb +8 -0
  37. data/lib/xml/xxpath.rb +354 -0
  38. data/test/all_tests.rb +6 -0
  39. data/test/company.rb +54 -0
  40. data/test/documents_folders.rb +33 -0
  41. data/test/fixtures/bookmarks1.xml +24 -0
  42. data/test/fixtures/company1.xml +85 -0
  43. data/test/fixtures/documents_folders.xml +71 -0
  44. data/test/fixtures/documents_folders2.xml +30 -0
  45. data/test/multiple_mappings.rb +80 -0
  46. data/test/tests_init.rb +2 -0
  47. data/test/xml_mapping_adv_test.rb +84 -0
  48. data/test/xml_mapping_test.rb +182 -0
  49. data/test/xpath_test.rb +273 -0
  50. metadata +96 -0
@@ -0,0 +1,114 @@
1
+ require 'xml/xxpath'
2
+
3
+ d=REXML::Document.new <<EOS
4
+ <foo>
5
+ <bar>
6
+ <baz key="work">Java</baz>
7
+ <baz key="play">Ruby</baz>
8
+ </bar>
9
+ </foo>
10
+ EOS
11
+
12
+
13
+ rootelt=d.root
14
+
15
+ #### ensuring that a specific path exists inside the document
16
+
17
+ XML::XXPath.new("/bar/baz[@key='work']").first(rootelt,:ensure_created=>true)
18
+ => <baz key='work'> ... </>
19
+ d.write($stdout,2)
20
+ <foo>
21
+ <bar>
22
+ <baz key='work'>Java</baz>
23
+ <baz key='play'>Ruby</baz>
24
+ </bar>
25
+ </foo>
26
+
27
+ ### no change (path existed before)
28
+
29
+
30
+ XML::XXPath.new("/bar/baz[@key='42']").first(rootelt,:ensure_created=>true)
31
+ => <baz key='42'/>
32
+ d.write($stdout,2)
33
+ <foo>
34
+ <bar>
35
+ <baz key='work'>Java</baz>
36
+ <baz key='play'>Ruby</baz>
37
+ <baz key='42'/>
38
+ </bar>
39
+ </foo>
40
+
41
+ ### path was added
42
+
43
+ XML::XXPath.new("/bar/baz[@key='42']").first(rootelt,:ensure_created=>true)
44
+ => <baz key='42'/>
45
+ d.write($stdout,2)
46
+ <foo>
47
+ <bar>
48
+ <baz key='work'>Java</baz>
49
+ <baz key='play'>Ruby</baz>
50
+ <baz key='42'/>
51
+ </bar>
52
+ </foo>
53
+
54
+ ### no change this time
55
+
56
+ XML::XXPath.new("/bar/baz[@key2='hello']").first(rootelt,:ensure_created=>true)
57
+ => <baz key2='hello' key='work'> ... </>
58
+ d.write($stdout,2)
59
+ <foo>
60
+ <bar>
61
+ <baz key2='hello' key='work'>Java</baz>
62
+ <baz key='play'>Ruby</baz>
63
+ <baz key='42'/>
64
+ </bar>
65
+ </foo>
66
+
67
+ ### this fit in the 1st "baz" element since
68
+ ### there was no "key2" attribute there before.
69
+
70
+ XML::XXPath.new("/bar/baz[2]").first(rootelt,:ensure_created=>true)
71
+ => <baz key='play'> ... </>
72
+ d.write($stdout,2)
73
+ <foo>
74
+ <bar>
75
+ <baz key2='hello' key='work'>Java</baz>
76
+ <baz key='play'>Ruby</baz>
77
+ <baz key='42'/>
78
+ </bar>
79
+ </foo>
80
+
81
+ ### no change
82
+
83
+ XML::XXPath.new("/bar/baz[6]/@haha").first(rootelt,:ensure_created=>true)
84
+ => #<XML::XXPath::Accessors::Attribute:0x404d5ce0 @name="haha", @parent=<baz haha='[unset]'/>>
85
+ d.write($stdout,2)
86
+ <foo>
87
+ <bar>
88
+ <baz key2='hello' key='work'>Java</baz>
89
+ <baz key='play'>Ruby</baz>
90
+ <baz key='42'/>
91
+ <baz/>
92
+ <baz/>
93
+ <baz haha='[unset]'/>
94
+ </bar>
95
+ </foo>
96
+
97
+ ### for there to be a 6th "baz" element, there must be 1st..5th "baz" elements
98
+
99
+ XML::XXPath.new("/bar/baz[6]/@haha").first(rootelt,:ensure_created=>true)
100
+ => #<XML::XXPath::Accessors::Attribute:0x404d086c @name="haha", @parent=<baz haha='[unset]'/>>
101
+ d.write($stdout,2)
102
+ <foo>
103
+ <bar>
104
+ <baz key2='hello' key='work'>Java</baz>
105
+ <baz key='play'>Ruby</baz>
106
+ <baz key='42'/>
107
+ <baz/>
108
+ <baz/>
109
+ <baz haha='[unset]'/>
110
+ </bar>
111
+ </foo>
112
+
113
+ ### no change this time
114
+
@@ -0,0 +1,42 @@
1
+ #:invisible:
2
+ $:.unshift "../lib" #<=
3
+ #:visible:
4
+ require 'xml/xxpath'
5
+
6
+ d=REXML::Document.new <<EOS
7
+ <foo>
8
+ <bar/>
9
+ <bar/>
10
+ </foo>
11
+ EOS
12
+
13
+
14
+ rootelt=d.root
15
+
16
+
17
+ XML::XXPath.new("*").all(rootelt)#<=
18
+ ### ok
19
+
20
+ XML::XXPath.new("bar/*").first(rootelt, :allow_nil=>true)#<=
21
+ ### ok, nothing there
22
+
23
+ ### the same call with :ensure_created=>true
24
+ newelt = XML::XXPath.new("bar/*").first(rootelt, :ensure_created=>true)#<=
25
+
26
+ #:invisible_retval:
27
+ d.write($stdout,2)#<=
28
+
29
+ #:visible_retval:
30
+ ### a new "unspecified" element was created
31
+ newelt.unspecified?#<=
32
+
33
+ ### we must modify it to "specify" it
34
+ newelt.name="new-one"
35
+ newelt.text="hello!"
36
+ newelt.unspecified?#<=
37
+
38
+ #:invisible_retval:
39
+ d.write($stdout,2)#<=
40
+
41
+ ### you could also set unspecified to false explicitly, as in:
42
+ newelt.unspecified=true
@@ -0,0 +1,56 @@
1
+ require 'xml/xxpath'
2
+
3
+ d=REXML::Document.new <<EOS
4
+ <foo>
5
+ <bar/>
6
+ <bar/>
7
+ </foo>
8
+ EOS
9
+
10
+
11
+ rootelt=d.root
12
+
13
+
14
+ XML::XXPath.new("*").all(rootelt)
15
+ => [<bar/>, <bar/>]
16
+ ### ok
17
+
18
+ XML::XXPath.new("bar/*").first(rootelt, :allow_nil=>true)
19
+ => nil
20
+ ### ok, nothing there
21
+
22
+ ### the same call with :ensure_created=>true
23
+ newelt = XML::XXPath.new("bar/*").first(rootelt, :ensure_created=>true)
24
+ => </>
25
+
26
+ d.write($stdout,2)
27
+ <foo>
28
+ <bar>
29
+ </>
30
+ </bar>
31
+ <bar/>
32
+ </foo>
33
+
34
+
35
+ ### a new "unspecified" element was created
36
+ newelt.unspecified?
37
+ => true
38
+
39
+ ### we must modify it to "specify" it
40
+ newelt.name="new-one"
41
+ newelt.text="hello!"
42
+ newelt.unspecified?
43
+ => false
44
+
45
+ d.write($stdout,2)
46
+ <foo>
47
+ <bar>
48
+ <new-one>hello!</new-one>
49
+ </bar>
50
+ <bar/>
51
+ </foo>
52
+
53
+
54
+ ### you could also set unspecified to false explicitly, as in:
55
+ newelt.unspecified=true
56
+
@@ -0,0 +1,51 @@
1
+ #:invisible:
2
+ $:.unshift "../lib" #<=
3
+ #:visible:
4
+ require 'xml/xxpath'
5
+
6
+ d=REXML::Document.new <<EOS
7
+ <foo>
8
+ <bar>
9
+ <baz key="work">Java</baz>
10
+ <baz key="play">Ruby</baz>
11
+ </bar>
12
+ <bar>
13
+ <baz key="ab">hello</baz>
14
+ <baz key="play">scrabble</baz>
15
+ <baz key="xy">goodbye</baz>
16
+ </bar>
17
+ <more>
18
+ <baz key="play">poker</baz>
19
+ </more>
20
+ </foo>
21
+ EOS
22
+
23
+
24
+ ####read access
25
+ path=XML::XXPath.new("/foo/bar[2]/baz")
26
+
27
+ ## path.all(document) gives all elements matching path in document
28
+ path.all(d)#<=
29
+
30
+ ## loop over them
31
+ path.each(d){|elt| puts elt.text}#<=
32
+
33
+ ## the first of those
34
+ path.first(d)#<=
35
+
36
+ ## no match here (only three "baz" elements)
37
+ path2=XML::XXPath.new("/foo/bar[2]/baz[4]")
38
+ path2.all(d)#<=
39
+
40
+ #:handle_exceptions:
41
+ ## "first" raises XML::XXPathError in such cases...
42
+ path2.first(d)#<=
43
+ #:no_exceptions:
44
+
45
+ ##...unless we allow nil returns
46
+ path2.first(d,:allow_nil=>true)#<=
47
+
48
+ ##attribute nodes can also be returned
49
+ keysPath=XML::XXPath.new("/foo/*/*/@key")
50
+
51
+ keysPath.all(d).map{|attr|attr.text}#<=
@@ -0,0 +1,57 @@
1
+ require 'xml/xxpath'
2
+
3
+ d=REXML::Document.new <<EOS
4
+ <foo>
5
+ <bar>
6
+ <baz key="work">Java</baz>
7
+ <baz key="play">Ruby</baz>
8
+ </bar>
9
+ <bar>
10
+ <baz key="ab">hello</baz>
11
+ <baz key="play">scrabble</baz>
12
+ <baz key="xy">goodbye</baz>
13
+ </bar>
14
+ <more>
15
+ <baz key="play">poker</baz>
16
+ </more>
17
+ </foo>
18
+ EOS
19
+
20
+
21
+ ####read access
22
+ path=XML::XXPath.new("/foo/bar[2]/baz")
23
+
24
+ ## path.all(document) gives all elements matching path in document
25
+ path.all(d)
26
+ => [<baz key='ab'> ... </>, <baz key='play'> ... </>, <baz key='xy'> ... </>]
27
+
28
+ ## loop over them
29
+ path.each(d){|elt| puts elt.text}
30
+ hello
31
+ scrabble
32
+ goodbye
33
+ => [<baz key='ab'> ... </>, <baz key='play'> ... </>, <baz key='xy'> ... </>]
34
+
35
+ ## the first of those
36
+ path.first(d)
37
+ => <baz key='ab'> ... </>
38
+
39
+ ## no match here (only three "baz" elements)
40
+ path2=XML::XXPath.new("/foo/bar[2]/baz[4]")
41
+ path2.all(d)
42
+ => []
43
+
44
+ ## "first" raises XML::XXPathError in such cases...
45
+ path2.first(d)
46
+ XML::XXPathError: path not found: /foo/bar[2]/baz[4]
47
+ from ../lib/xml/../xml/xxpath.rb:130:in `first'
48
+
49
+ ##...unless we allow nil returns
50
+ path2.first(d,:allow_nil=>true)
51
+ => nil
52
+
53
+ ##attribute nodes can also be returned
54
+ keysPath=XML::XXPath.new("/foo/*/*/@key")
55
+
56
+ keysPath.all(d).map{|attr|attr.text}
57
+ => ["work", "play", "ab", "play", "xy", "play"]
@@ -0,0 +1,40 @@
1
+ require 'rbconfig'
2
+ require 'find'
3
+ require 'ftools'
4
+
5
+ include Config
6
+
7
+ # this was adapted from active_record's install.rb
8
+
9
+ $sitedir = CONFIG["sitelibdir"]
10
+ unless $sitedir
11
+ version = CONFIG["MAJOR"] + "." + CONFIG["MINOR"]
12
+ $libdir = File.join(CONFIG["libdir"], "ruby", version)
13
+ $sitedir = $:.find {|x| x =~ /site_ruby/ }
14
+ if !$sitedir
15
+ $sitedir = File.join($libdir, "site_ruby")
16
+ elsif $sitedir !~ Regexp.quote(version)
17
+ $sitedir = File.join($sitedir, version)
18
+ end
19
+ end
20
+
21
+
22
+ # deprecated files that should be removed
23
+ # deprecated = %w{ }
24
+
25
+ # files to install in library path
26
+ files = %w-
27
+ xml/mapping.rb
28
+ xml/xxpath.rb
29
+ xml/mapping/base.rb
30
+ xml/mapping/standard_nodes.rb
31
+ xml/mapping/version.rb
32
+ -
33
+
34
+ # the acual gruntwork
35
+ Dir.chdir("lib")
36
+ # File::safe_unlink *deprecated.collect{|f| File.join($sitedir, f.split(/\//))}
37
+ files.each {|f|
38
+ File::makedirs(File.join($sitedir, *f.split(/\//)[0..-2]))
39
+ File::install(f, File.join($sitedir, *f.split(/\//)), 0644, true)
40
+ }
@@ -0,0 +1,14 @@
1
+ # xml-mapping -- bidirectional Ruby-XML mapper
2
+ # Copyright (C) 2004,2005 Olaf Klischat
3
+
4
+ $:.unshift(File.dirname(__FILE__)+"/..")
5
+
6
+ require 'xml/mapping/base'
7
+ require 'xml/mapping/standard_nodes'
8
+
9
+ XML::Mapping.add_node_class XML::Mapping::TextNode
10
+ XML::Mapping.add_node_class XML::Mapping::NumericNode
11
+ XML::Mapping.add_node_class XML::Mapping::ObjectNode
12
+ XML::Mapping.add_node_class XML::Mapping::BooleanNode
13
+ XML::Mapping.add_node_class XML::Mapping::ArrayNode
14
+ XML::Mapping.add_node_class XML::Mapping::HashNode
@@ -0,0 +1,563 @@
1
+ # xml-mapping -- bidirectional Ruby-XML mapper
2
+ # Copyright (C) 2004,2005 Olaf Klischat
3
+
4
+ require 'rexml/document'
5
+ require "xml/xxpath"
6
+
7
+ module XML
8
+
9
+ class MappingError < RuntimeError
10
+ end
11
+
12
+ # This is the central interface module of the xml-mapping library.
13
+ #
14
+ # Including this module in your classes adds XML mapping
15
+ # capabilities to them.
16
+ #
17
+ # == Example
18
+ #
19
+ # === Input document:
20
+ #
21
+ # :include: company.xml
22
+ #
23
+ # === mapping class declaration:
24
+ #
25
+ # :include: company.rb
26
+ #
27
+ # === usage:
28
+ #
29
+ # :include: company_usage.intout
30
+ #
31
+ # So you have to include XML::Mapping into your class to turn it
32
+ # into a "mapping class", that is, to add XML mapping capabilities
33
+ # to it. An instance of the mapping classes is then bidirectionally
34
+ # mapped to an XML node (i.e. an element), where the state (simple
35
+ # attributes, sub-objects, arrays, hashes etc.) of that instance is
36
+ # mapped to sub-nodes of that node. In addition to the class and
37
+ # instance methods defined in XML::Mapping, your mapping class will
38
+ # get class methods like 'text_node', 'array_node' and so on; I call
39
+ # them "node factory methods". More precisely, there is one node
40
+ # factory method for each registered <em>node type</em>. Node types
41
+ # are classes derived from XML::Mapping::Node; they're registered
42
+ # with the xml-mapping library via XML::Mapping.add_node_class. The
43
+ # node types TextNode, BooleanNode, NumericNode, ObjectNode,
44
+ # ArrayNode, and HashNode are automatically registered by
45
+ # xml/mapping.rb; you can easily write your own ones. The name of a
46
+ # node factory method is inferred by 'underscoring' the name of the
47
+ # corresponding node type; e.g. 'TextNode' becomes 'text_node'. Each
48
+ # node factory method creates an instance of the corresponding node
49
+ # type and adds it to the mapping class (not its instances). The
50
+ # arguments to a node factory method are automatically turned into
51
+ # arguments to the corresponding node type's initializer. So, in
52
+ # order to learn more about the meaning of a node factory method's
53
+ # parameters, you read the documentation of the corresponding node
54
+ # type. All predefined node types expect as their first argument a
55
+ # symbol that names an r/w attribute which will be added to the
56
+ # mapping class. The mapping class is a normal Ruby class; you can
57
+ # add constructors, methods and attributes to it, derive from it,
58
+ # derive it from another class, include additional modules etc.
59
+ #
60
+ # Including XML::Mapping also adds all methods of
61
+ # XML::Mapping::ClassMethods to your class (as class methods).
62
+ #
63
+ # As you may have noticed from the example, the node factory methods
64
+ # generally use XPath expressions to specify locations in the mapped
65
+ # XML document. To make this work, XML::Mapping relies on
66
+ # XML::XXPath, which implements a subset of XPath, but also provides
67
+ # write access, which is needed by the node types to support writing
68
+ # data back to XML. Both XML::Mapping and XML::XXPath use REXML
69
+ # (http://www.germane-software.com/software/rexml/) to represent XML
70
+ # elements/documents in memory.
71
+ module Mapping
72
+
73
+ # can't really use class variables for these because they must be
74
+ # shared by all class methods mixed into classes by including
75
+ # Mapping. See
76
+ # http://user.cs.tu-berlin.de/~klischat/mydocs/ruby/mixin_class_methods_global_state.txt.html
77
+ # for a more detailed discussion.
78
+ Classes_w_default_rootelt_names = {} #:nodoc:
79
+ Classes_w_nondefault_rootelt_names = {} #:nodoc:
80
+
81
+ def self.append_features(base) #:nodoc:
82
+ super
83
+ base.extend(ClassMethods)
84
+ Classes_w_default_rootelt_names[base.default_root_element_name] = base
85
+ end
86
+
87
+
88
+ # Finds the mapping class corresponding to the given XML root
89
+ # element name. This is the inverse operation to
90
+ # <class>.root_element_name (see
91
+ # XML::Mapping::ClassMethods.root_element_name).
92
+ def self.class_for_root_elt_name(name)
93
+ # TODO: implement Hash read-only instead of this
94
+ # interface
95
+ Classes_w_nondefault_rootelt_names[name] ||
96
+ Classes_w_default_rootelt_names[name]
97
+ end
98
+
99
+
100
+ def initialize_xml_mapping #:nodoc:
101
+ self.class.all_xml_mapping_nodes.each do |node|
102
+ node.obj_initializing(self)
103
+ end
104
+ end
105
+
106
+ # private :initialize_xml_mapping
107
+
108
+ # Initializer. Calls obj_initializing(self) on all nodes. You
109
+ # should call this using +super+ in your mapping classes to
110
+ # inherit this behaviour.
111
+ def initialize(*args)
112
+ initialize_xml_mapping
113
+ end
114
+
115
+ # "fill" the contents of _xml_ into _self_. _xml_ is a
116
+ # REXML::Element.
117
+ #
118
+ # First, pre_load(_xml_) is called, then all the nodes for this
119
+ # object's class are processed (i.e. have their
120
+ # #xml_to_obj method called) in the order of their definition
121
+ # inside the class, then #post_load is called.
122
+ def fill_from_xml(xml)
123
+ pre_load(xml)
124
+ self.class.all_xml_mapping_nodes.each do |node|
125
+ node.xml_to_obj self, xml
126
+ end
127
+ post_load
128
+ end
129
+
130
+ # This method is called immediately before _self_ is filled from
131
+ # an xml source. _xml_ is the source REXML::Element.
132
+ #
133
+ # The default implementation of this method is empty.
134
+ def pre_load(xml)
135
+ end
136
+
137
+
138
+ # This method is called immediately after _self_ has been filled
139
+ # from an xml source. If you have things to do after the object
140
+ # has been succefully loaded from the xml (reorganising the loaded
141
+ # data in some way, setting up additional views on the data etc.),
142
+ # this is the place where you put them. You can also raise an
143
+ # exception to abandon the whole loading process.
144
+ #
145
+ # The default implementation of this method is empty.
146
+ def post_load
147
+ end
148
+
149
+
150
+ # Fill _self_'s state into the xml node (REXML::Element)
151
+ # _xml_. All the nodes for this object's class are processed
152
+ # (i.e. have their
153
+ # #obj_to_xml method called) in the order of their definition
154
+ # inside the class.
155
+ def fill_into_xml(xml)
156
+ self.class.all_xml_mapping_nodes.each do |node|
157
+ node.obj_to_xml self,xml
158
+ end
159
+ end
160
+
161
+ # Fill _self_'s state into a new xml node, return that
162
+ # node.
163
+ #
164
+ # This method calls #pre_save, then #fill_into_xml, then
165
+ # #post_save.
166
+ def save_to_xml
167
+ xml = pre_save
168
+ fill_into_xml(xml)
169
+ post_save(xml)
170
+ xml
171
+ end
172
+
173
+ # This method is called when _self_ is to be converted to an XML
174
+ # tree. It *must* create and return an XML element (as a
175
+ # REXML::Element); that element will then be passed to
176
+ # #fill_into_xml.
177
+ #
178
+ # The default implementation of this method creates a new empty
179
+ # element whose name is the #root_element_name of _self_'s class
180
+ # (see ClassMethods.root_element_name). By default, this is the
181
+ # class name, with capital letters converted to lowercase and
182
+ # preceded by a dash, e.g. "MySampleClass" becomes
183
+ # "my-sample-class".
184
+ def pre_save
185
+ REXML::Element.new(self.class.root_element_name)
186
+ end
187
+
188
+ # This method is called immediately after _self_'s state has been
189
+ # filled into an XML element.
190
+ #
191
+ # The default implementation does nothing.
192
+ def post_save(xml)
193
+ end
194
+
195
+
196
+ # Save _self_'s state as XML into the file named _filename_.
197
+ # The XML is obtained by calling #save_to_xml.
198
+ def save_to_file(filename)
199
+ xml = save_to_xml
200
+ File.open(filename,"w") do |f|
201
+ xml.write(f,2)
202
+ end
203
+ end
204
+
205
+
206
+ # Abstract base class for all node types. As mentioned in the
207
+ # documentation for XML::Mapping, node types must be registered
208
+ # using add_node_class, and a corresponding "node factory method"
209
+ # (e.g. "text_node") will then be added as a class method to your
210
+ # mapping classes. The node factory method is called from the body
211
+ # of the mapping classes as demonstrated in the examples. It
212
+ # creates an instance of its corresponding node type (the list of
213
+ # parameters to the node factory method, preceded by the owning
214
+ # mapping class, will be passed to the constructor of the node
215
+ # type) and adds it to its owning mapping class, so there is one
216
+ # node object per node definition per mapping class. That node
217
+ # object will handle all XML marshalling/unmarshalling for this
218
+ # node, for all instances of the mapping class. For this purpose,
219
+ # the marshalling and unmarshalling methods of a mapping class
220
+ # instance (fill_into_xml and fill_from_xml, respectively)
221
+ # will call obj_to_xml resp. xml_to_obj on all nodes of the
222
+ # mapping class, in the order of their definition, passing the
223
+ # REXML element the data is to be marshalled to/unmarshalled from
224
+ # as well as the object the data is to be read from/filled into.
225
+ #
226
+ # Node types that map some XML data to a single attribute of their
227
+ # mapping class (that should be most of them) shouldn't be
228
+ # directly derived from this class, but rather from
229
+ # SingleAttributeNode.
230
+ class Node
231
+ # Intializer, to be called from descendant classes. _owner_ is
232
+ # the mapping class this node is being defined in. It'll be
233
+ # stored in _@owner_.
234
+ def initialize(owner)
235
+ @owner = owner
236
+ owner.xml_mapping_nodes << self
237
+ end
238
+ # This is called by the XML unmarshalling machinery when the
239
+ # state of an instance of this node's @owner is to be read from
240
+ # an XML node. _obj_ is the instance, _xml_ is the element (a
241
+ # REXML::Element). The node must read "its" data from _xml_
242
+ # (using XML::XXPath or any other means) and store it to the
243
+ # corresponding parts (attributes etc.) of _obj_'s state.
244
+ def xml_to_obj(obj,xml)
245
+ raise "abstract method called"
246
+ end
247
+ # This is called by the XML unmarshalling machinery when the
248
+ # state of an instance of this node's @owner is to be stored
249
+ # into an XML node. _obj_ is the instance, _xml_ is the element
250
+ # (a REXML::Element). The node must extract "its" data from
251
+ # _obj_ and store it to the corresponding parts (sub-elements,
252
+ # attributes etc.) of _xml_ (using XML::XXPath or any other
253
+ # means).
254
+ def obj_to_xml(obj,xml)
255
+ raise "abstract method called"
256
+ end
257
+ # Called when a new instance is being initialized. _obj_ is the
258
+ # instance. You may set up initial values for the attributes
259
+ # this node is responsible for here. Default implementation is
260
+ # empty.
261
+ def obj_initializing(obj)
262
+ end
263
+ end
264
+
265
+
266
+ # Base class for node types that map some XML data to a single
267
+ # attribute of their mapping class. This class also introduces a
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.
272
+ #
273
+ # All node types that come with xml-mapping inherit from
274
+ # SingleAttributeNode.
275
+ class SingleAttributeNode < Node
276
+ # Initializer. _owner_ is the owning mapping class (gets passed
277
+ # to the superclass initializer and therefore put into
278
+ # @owner). The second parameter (and hence the first parameter
279
+ # to the node factory method), _attrname_, is a symbol that
280
+ # names the mapping class attribute this node should map to. It
281
+ # gets stored into @attrname, and the attribute (an r/w
282
+ # attribute of name attrname) is added to the mapping class
283
+ # (using attr_accessor).
284
+ #
285
+ # If the last argument is a hash, it is assumed to be the
286
+ # abovementioned "options hash", and is stored into
287
+ # @options. Two entries -- :optional and :default_value -- in
288
+ # the options hash are already processed in SingleAttributeNode:
289
+ #
290
+ # Supplying :default_value=>_obj_ makes _obj_ the _default
291
+ # value_ for this attribute. When unmarshalling (loading) an
292
+ # object from an XML source, the attribute will be set to this
293
+ # value if nothing was provided in the XML; when marshalling
294
+ # (saving), the attribute won't be saved if it is set to the
295
+ # default value.
296
+ #
297
+ # Providing just :optional=>true is equivalent to providing
298
+ # :default_value=>nil.
299
+ #
300
+ # The remaining arguments are passed to initialize_impl, which
301
+ # is the initializer subclasses should overwrite instead of
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
328
+ if @options[:optional] and not(@options.has_key?(:default_value))
329
+ @options[:default_value] = nil
330
+ end
331
+ initialize_impl(*args)
332
+ end
333
+ # Initializer to be implemented by subclasses.
334
+ def initialize_impl(*args)
335
+ raise "abstract method called"
336
+ end
337
+
338
+ # Exception that may be used by implementations of
339
+ # #extract_attr_value to announce that the attribute value is
340
+ # not set in the XML and, consequently, the default value should
341
+ # be set in the object being created, or an Exception be raised
342
+ # if no default value was specified.
343
+ class NoAttrValueSet < XXPathError
344
+ end
345
+
346
+ def xml_to_obj(obj,xml) # :nodoc:
347
+ begin
348
+ obj.send :"#{@attrname}=", extract_attr_value(xml)
349
+ rescue NoAttrValueSet => err
350
+ unless @options.has_key? :default_value
351
+ raise XML::MappingError, "no value, and no default value: #{err}"
352
+ end
353
+ obj.send :"#{@attrname}=", @options[:default_value]
354
+ end
355
+ end
356
+
357
+ # (to be overridden by subclasses) Extract and return the
358
+ # attribute's value from _xml_. In the example above, TextNode's
359
+ # implementation would return the current value of the
360
+ # sub-element named by @path (i.e., "city"). If the
361
+ # implementation decides that the attribute value is "unset" in
362
+ # _xml_, it should raise NoAttrValueSet in order to initiate
363
+ # proper handling of possibly supplied :optional and
364
+ # :default_value options (you may use #default_when_xpath_err
365
+ # for this purpose).
366
+ def extract_attr_value(xml)
367
+ raise "abstract method called"
368
+ end
369
+ def obj_to_xml(obj,xml) # :nodoc:
370
+ value = obj.send(:"#{@attrname}")
371
+ if @options.has_key? :default_value
372
+ unless value == @options[:default_value]
373
+ set_attr_value(xml, value)
374
+ end
375
+ else
376
+ if value == nil
377
+ raise XML::MappingError, "no value, and no default value, for attribute: #{@attrname}"
378
+ end
379
+ set_attr_value(xml, value)
380
+ end
381
+ end
382
+ # (to be overridden by subclasses) Write _value_ into the
383
+ # correct sub-nodes of _xml_.
384
+ def set_attr_value(xml, value)
385
+ raise "abstract method called"
386
+ end
387
+ def obj_initializing(obj) # :nodoc:
388
+ if @options.has_key? :default_value
389
+ obj.send :"#{@attrname}=", @options[:default_value]
390
+ end
391
+ end
392
+ # utility method to be used by implementations of
393
+ # #extract_attr_value. Calls the supplied block, catching
394
+ # XML::XXPathError and mapping it to NoAttrValueSet. This is for
395
+ # the common case that an implementation considers an attribute
396
+ # value not to be present in the XML if some specific sub-path
397
+ # does not exist.
398
+ def default_when_xpath_err # :yields:
399
+ begin
400
+ yield
401
+ rescue XML::XXPathError => err
402
+ raise NoAttrValueSet, "Attribute #{@attrname} not set (XXPathError: #{err})"
403
+ end
404
+ end
405
+ end
406
+
407
+
408
+ # Registers the new node class _c_ (must be a descendant of Node)
409
+ # with the xml-mapping framework.
410
+ #
411
+ # A new "factory method" will automatically be added to
412
+ # ClassMethods (and therefore to all classes that include
413
+ # XML::Mapping from now on); so you can call it from the body of
414
+ # your mapping class definition in order to create nodes of type
415
+ # _c_. The name of the factory method is derived by "underscoring"
416
+ # the (unqualified) name of _c_;
417
+ # e.g. _c_==<tt>Foo::Bar::MyNiftyNode</tt> will result in the
418
+ # creation of a factory method named +my_nifty_node+. The
419
+ # generated factory method creates and returns a new instance of
420
+ # _c_. The list of argument to _c_.new consists of _self_
421
+ # (i.e. the mapping class the factory method was called from)
422
+ # followed by the arguments passed to the factory method. You
423
+ # should always use the factory methods to create instances of
424
+ # node classes; you should never need to call a node class's
425
+ # constructor directly.
426
+ #
427
+ # For a demonstration, see the calls to +text_node+, +array_node+
428
+ # etc. in the examples along with the corresponding node classes
429
+ # TextNode, ArrayNode etc. (these predefined node classes are in
430
+ # no way "special"; they're added using add_node_class in
431
+ # mapping.rb just like any custom node classes would be).
432
+ def self.add_node_class(c)
433
+ meth_name = c.name.split('::')[-1].gsub(/^(.)/){$1.downcase}.gsub(/(.)([A-Z])/){$1+"_"+$2.downcase}
434
+ ClassMethods.module_eval <<-EOS
435
+ def #{meth_name}(*args)
436
+ #{c.name}.new(self,*args)
437
+ end
438
+ EOS
439
+ end
440
+
441
+
442
+ # The instance methods of this module are automatically added as
443
+ # class methods to a class that includes XML::Mapping.
444
+ module ClassMethods
445
+ #ClassMethods = Module.new do # this is the alterbative -- but see above for peculiarities
446
+
447
+ # Add getter and setter methods for a new attribute named _name_
448
+ # to this class. This is a convenience method intended to be
449
+ # called from Node class initializers.
450
+ def add_accessor(name)
451
+ name = name.id2name if name.kind_of? Symbol
452
+ unless self.instance_methods.include?(name)
453
+ self.module_eval <<-EOS
454
+ attr_reader :#{name}
455
+ EOS
456
+ end
457
+ unless self.instance_methods.include?("#{name}=")
458
+ self.module_eval <<-EOS
459
+ attr_writer :#{name}
460
+ EOS
461
+ end
462
+ end
463
+
464
+ # Create a new instance of this class from the XML contained in
465
+ # the file named _filename_. Calls load_from_xml internally.
466
+ def load_from_file(filename)
467
+ xml = REXML::Document.new(File.new(filename))
468
+ load_from_xml(xml.root)
469
+ end
470
+
471
+ # Create a new instance of this class from the XML contained in
472
+ # _xml_ (a REXML::Element).
473
+ #
474
+ # Allocates a new object, then calls fill_from_xml(_xml_) on
475
+ # it.
476
+ def load_from_xml(xml)
477
+ obj = self.allocate
478
+ obj.initialize_xml_mapping
479
+ obj.fill_from_xml(xml)
480
+ obj
481
+ end
482
+
483
+
484
+ # array of all nodes types defined in this class, in the order
485
+ # of their definition
486
+ def xml_mapping_nodes
487
+ @xml_mapping_nodes ||= []
488
+ end
489
+
490
+
491
+ # enumeration of all nodes types in effect when
492
+ # marshalling/unmarshalling this class, that is, node types
493
+ # defined for this class as well as for its superclasses. The
494
+ # node types are returned in the order of their definition,
495
+ # starting with the topmost superclass that has node types
496
+ # defined.
497
+ def all_xml_mapping_nodes
498
+ # TODO: we could return a dynamic Enumerable here, or cache
499
+ # the array...
500
+ result = []
501
+ if superclass and superclass.respond_to?(:all_xml_mapping_nodes)
502
+ result += superclass.all_xml_mapping_nodes
503
+ end
504
+ result += xml_mapping_nodes
505
+ end
506
+
507
+
508
+ # The "root element name" of this class (combined getter/setter
509
+ # method).
510
+ #
511
+ # The root element name is the name of the root element of the
512
+ # XML tree returned by <this class>.#save_to_xml (or, more
513
+ # specifically, <this class>.#pre_save). By default, this method
514
+ # returns the #default_root_element_name; you may call this
515
+ # method with an argument to set the root element name to
516
+ # something other than the default.
517
+ def root_element_name(name=nil)
518
+ if name
519
+ Classes_w_nondefault_rootelt_names.delete(root_element_name)
520
+ Classes_w_default_rootelt_names.delete(root_element_name)
521
+ Classes_w_default_rootelt_names.delete(name)
522
+
523
+ @root_element_name = name
524
+
525
+ Classes_w_nondefault_rootelt_names[name]=self
526
+ end
527
+ @root_element_name || default_root_element_name
528
+ end
529
+
530
+
531
+ # The default root element name for this class. Equals the class
532
+ # name, with all parent module names stripped, and with capital
533
+ # letters converted to lowercase and preceded by a dash;
534
+ # e.g. "Foo::Bar::MySampleClass" becomes "my-sample-class".
535
+ def default_root_element_name
536
+ self.name.split('::')[-1].gsub(/^(.)/){$1.downcase}.gsub(/(.)([A-Z])/){$1+"-"+$2.downcase}
537
+ end
538
+
539
+ end
540
+
541
+
542
+
543
+ # "polymorphic" load function. Turns the XML tree _xml_ into an
544
+ # object, which is returned. The class of the object is
545
+ # automatically determined from the root element name of _xml_
546
+ # using XML::Mapping::class_for_root_elt_name.
547
+ def self.load_object_from_xml(xml)
548
+ unless c = class_for_root_elt_name(xml.name)
549
+ raise MappingError, "no mapping class for root element name #{xml.name}"
550
+ end
551
+ c.load_from_xml(xml)
552
+ end
553
+
554
+ # Like load_object_from_xml, but loads from the XML file named by
555
+ # _filename_.
556
+ def self.load_object_from_file(filename)
557
+ xml = REXML::Document.new(File.new(filename))
558
+ load_object_from_xml(xml.root)
559
+ end
560
+
561
+ end
562
+
563
+ end