xml-mapping 0.8

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