xml-mapping 0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/LICENSE +56 -0
- data/README +386 -0
- data/README_XPATH +175 -0
- data/Rakefile +214 -0
- data/TODO.txt +32 -0
- data/doc/xpath_impl_notes.txt +119 -0
- data/examples/company.rb +34 -0
- data/examples/company.xml +26 -0
- data/examples/company_usage.intin.rb +19 -0
- data/examples/company_usage.intout +39 -0
- data/examples/order.rb +61 -0
- data/examples/order.xml +54 -0
- data/examples/order_signature_enhanced.rb +7 -0
- data/examples/order_signature_enhanced.xml +9 -0
- data/examples/order_signature_enhanced_usage.intin.rb +12 -0
- data/examples/order_signature_enhanced_usage.intout +16 -0
- data/examples/order_usage.intin.rb +73 -0
- data/examples/order_usage.intout +147 -0
- data/examples/time_augm.intin.rb +19 -0
- data/examples/time_augm.intout +23 -0
- data/examples/time_node.rb +27 -0
- data/examples/xpath_create_new.intin.rb +85 -0
- data/examples/xpath_create_new.intout +181 -0
- data/examples/xpath_docvsroot.intin.rb +30 -0
- data/examples/xpath_docvsroot.intout +34 -0
- data/examples/xpath_ensure_created.intin.rb +62 -0
- data/examples/xpath_ensure_created.intout +114 -0
- data/examples/xpath_pathological.intin.rb +42 -0
- data/examples/xpath_pathological.intout +56 -0
- data/examples/xpath_usage.intin.rb +51 -0
- data/examples/xpath_usage.intout +57 -0
- data/install.rb +40 -0
- data/lib/xml/mapping.rb +14 -0
- data/lib/xml/mapping/base.rb +563 -0
- data/lib/xml/mapping/standard_nodes.rb +343 -0
- data/lib/xml/mapping/version.rb +8 -0
- data/lib/xml/xxpath.rb +354 -0
- data/test/all_tests.rb +6 -0
- data/test/company.rb +54 -0
- data/test/documents_folders.rb +33 -0
- data/test/fixtures/bookmarks1.xml +24 -0
- data/test/fixtures/company1.xml +85 -0
- data/test/fixtures/documents_folders.xml +71 -0
- data/test/fixtures/documents_folders2.xml +30 -0
- data/test/multiple_mappings.rb +80 -0
- data/test/tests_init.rb +2 -0
- data/test/xml_mapping_adv_test.rb +84 -0
- data/test/xml_mapping_test.rb +182 -0
- data/test/xpath_test.rb +273 -0
- metadata +96 -0
@@ -0,0 +1,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"]
|
data/install.rb
ADDED
@@ -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
|
+
}
|
data/lib/xml/mapping.rb
ADDED
@@ -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
|