tilia-xml 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/.rubocop.yml +32 -0
- data/.simplecov +4 -0
- data/.travis.yml +3 -0
- data/CHANGELOG.sabre.md +167 -0
- data/CONTRIBUTING.md +25 -0
- data/Gemfile +15 -0
- data/Gemfile.lock +56 -0
- data/LICENSE +27 -0
- data/LICENSE.sabre +27 -0
- data/README.md +30 -0
- data/Rakefile +17 -0
- data/lib/tilia/xml/context_stack_trait.rb +99 -0
- data/lib/tilia/xml/element/base.rb +73 -0
- data/lib/tilia/xml/element/cdata.rb +53 -0
- data/lib/tilia/xml/element/elements.rb +109 -0
- data/lib/tilia/xml/element/key_value.rb +110 -0
- data/lib/tilia/xml/element/uri.rb +98 -0
- data/lib/tilia/xml/element/xml_fragment.rb +128 -0
- data/lib/tilia/xml/element.rb +22 -0
- data/lib/tilia/xml/lib_xml_exception.rb +9 -0
- data/lib/tilia/xml/parse_exception.rb +7 -0
- data/lib/tilia/xml/reader.rb +240 -0
- data/lib/tilia/xml/service.rb +151 -0
- data/lib/tilia/xml/version.rb +9 -0
- data/lib/tilia/xml/writer.rb +261 -0
- data/lib/tilia/xml/xml_deserializable.rb +29 -0
- data/lib/tilia/xml/xml_serializable.rb +27 -0
- data/lib/tilia/xml.rb +23 -0
- data/test/test_helper.rb +4 -0
- data/test/xml/context_stack_test.rb +40 -0
- data/test/xml/element/cdata_test.rb +37 -0
- data/test/xml/element/eater.rb +60 -0
- data/test/xml/element/elements_test.rb +113 -0
- data/test/xml/element/key_value_test.rb +187 -0
- data/test/xml/element/mock.rb +52 -0
- data/test/xml/element/uri_test.rb +55 -0
- data/test/xml/element/xml_fragment_test.rb +121 -0
- data/test/xml/infite_loop_test.rb +47 -0
- data/test/xml/reader_test.rb +407 -0
- data/test/xml/service_test.rb +156 -0
- data/test/xml/writer_test.rb +260 -0
- data/tilia-xml.gemspec +15 -0
- metadata +132 -0
@@ -0,0 +1,240 @@
|
|
1
|
+
require 'libxml'
|
2
|
+
require 'stringio'
|
3
|
+
LibXML::XML::Error.set_handler(&LibXML::XML::Error::QUIET_HANDLER)
|
4
|
+
|
5
|
+
module Tilia
|
6
|
+
module Xml
|
7
|
+
# The Reader class expands upon PHP's built-in XMLReader.
|
8
|
+
#
|
9
|
+
# The intended usage, is to assign certain XML elements to PHP classes. These
|
10
|
+
# need to be registered using the element_map public property.
|
11
|
+
#
|
12
|
+
# After this is done, a single call to parse() will parse the entire document,
|
13
|
+
# and delegate sub-sections of the document to element classes.
|
14
|
+
class Reader
|
15
|
+
include Tilia::Xml::ContextStackTrait
|
16
|
+
|
17
|
+
# Returns the current nodename in clark-notation.
|
18
|
+
#
|
19
|
+
# For example: "{http://www.w3.org/2005/Atom}feed".
|
20
|
+
# Or if no namespace is defined: "{}feed".
|
21
|
+
#
|
22
|
+
# This method returns null if we're not currently on an element.
|
23
|
+
#
|
24
|
+
# @return [String, nil]
|
25
|
+
def clark
|
26
|
+
return nil unless local_name
|
27
|
+
|
28
|
+
"{#{namespace_uri}}#{local_name}"
|
29
|
+
end
|
30
|
+
|
31
|
+
# Reads the entire document.
|
32
|
+
#
|
33
|
+
# This function returns an array with the following three elements:
|
34
|
+
# * name - The root element name.
|
35
|
+
# * value - The value for the root element.
|
36
|
+
# * attributes - An array of attributes.
|
37
|
+
#
|
38
|
+
# This function will also disable the standard libxml error handler (which
|
39
|
+
# usually just results in PHP errors), and throw exceptions instead.
|
40
|
+
#
|
41
|
+
# @return [Hash]
|
42
|
+
def parse
|
43
|
+
begin
|
44
|
+
nil while node_type != ::LibXML::XML::Reader::TYPE_ELEMENT && read # noop
|
45
|
+
|
46
|
+
result = parse_current_element
|
47
|
+
rescue ::LibXML::XML::Error => e
|
48
|
+
raise Tilia::Xml::LibXmlException, e.to_s
|
49
|
+
end
|
50
|
+
|
51
|
+
result
|
52
|
+
end
|
53
|
+
|
54
|
+
# parse_get_elements parses everything in the current sub-tree,
|
55
|
+
# and returns a an array of elements.
|
56
|
+
#
|
57
|
+
# Each element has a 'name', 'value' and 'attributes' key.
|
58
|
+
#
|
59
|
+
# If the the element didn't contain sub-elements, an empty array is always
|
60
|
+
# returned. If there was any text inside the element, it will be
|
61
|
+
# discarded.
|
62
|
+
#
|
63
|
+
# If the element_map argument is specified, the existing element_map will
|
64
|
+
# be overridden while parsing the tree, and restored after this process.
|
65
|
+
#
|
66
|
+
# @param [Hash] element_map
|
67
|
+
# @return [Array]
|
68
|
+
def parse_get_elements(element_map = nil)
|
69
|
+
result = parse_inner_tree(element_map)
|
70
|
+
|
71
|
+
return [] unless result.is_a?(Array)
|
72
|
+
result
|
73
|
+
end
|
74
|
+
|
75
|
+
# Parses all elements below the current element.
|
76
|
+
#
|
77
|
+
# This method will return a string if this was a text-node, or an array if
|
78
|
+
# there were sub-elements.
|
79
|
+
#
|
80
|
+
# If there's both text and sub-elements, the text will be discarded.
|
81
|
+
#
|
82
|
+
# If the element_map argument is specified, the existing element_map will
|
83
|
+
# be overridden while parsing the tree, and restored after this process.
|
84
|
+
#
|
85
|
+
# @param [Hash] element_map
|
86
|
+
# @return [Array, String]
|
87
|
+
def parse_inner_tree(element_map = nil)
|
88
|
+
text = nil
|
89
|
+
elements = []
|
90
|
+
|
91
|
+
if node_type == ::LibXML::XML::Reader::TYPE_ELEMENT && self.empty_element?
|
92
|
+
# Easy!
|
93
|
+
self.next
|
94
|
+
return nil
|
95
|
+
end
|
96
|
+
|
97
|
+
unless element_map.nil?
|
98
|
+
push_context
|
99
|
+
@element_map = element_map
|
100
|
+
end
|
101
|
+
|
102
|
+
return false unless read
|
103
|
+
|
104
|
+
loop do
|
105
|
+
# RUBY: Skip is_valid block
|
106
|
+
|
107
|
+
case node_type
|
108
|
+
when ::LibXML::XML::Reader::TYPE_ELEMENT
|
109
|
+
elements << parse_current_element
|
110
|
+
when ::LibXML::XML::Reader::TYPE_TEXT,
|
111
|
+
::LibXML::XML::Reader::TYPE_CDATA
|
112
|
+
text ||= ''
|
113
|
+
text += value
|
114
|
+
read
|
115
|
+
when ::LibXML::XML::Reader::TYPE_END_ELEMENT
|
116
|
+
# Ensuring we are moving the cursor after the end element.
|
117
|
+
read
|
118
|
+
break
|
119
|
+
when ::LibXML::XML::Reader::TYPE_NONE
|
120
|
+
fail Tilia::Xml::ParseException, 'We hit the end of the document prematurely. This likely means that some parser "eats" too many elements. Do not attempt to continue parsing.'
|
121
|
+
else
|
122
|
+
# Advance to the next element
|
123
|
+
read
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
pop_context unless element_map.nil?
|
128
|
+
|
129
|
+
elements.any? ? elements : text
|
130
|
+
end
|
131
|
+
|
132
|
+
# Reads all text below the current element, and returns this as a string.
|
133
|
+
#
|
134
|
+
# @return [String]
|
135
|
+
def read_text
|
136
|
+
result = ''
|
137
|
+
previous_depth = depth
|
138
|
+
|
139
|
+
while read && depth != previous_depth
|
140
|
+
result += value if [
|
141
|
+
::LibXML::XML::Reader::TYPE_TEXT,
|
142
|
+
::LibXML::XML::Reader::TYPE_CDATA,
|
143
|
+
::LibXML::XML::Reader::TYPE_WHITESPACE
|
144
|
+
].include? node_type
|
145
|
+
end
|
146
|
+
|
147
|
+
result
|
148
|
+
end
|
149
|
+
|
150
|
+
# Parses the current XML element.
|
151
|
+
#
|
152
|
+
# This method returns arn array with 3 properties:
|
153
|
+
# * name - A clark-notation XML element name.
|
154
|
+
# * value - The parsed value.
|
155
|
+
# * attributes - A key-value list of attributes.
|
156
|
+
#
|
157
|
+
# @return [Hash]
|
158
|
+
def parse_current_element
|
159
|
+
name = clark
|
160
|
+
|
161
|
+
attributes = {}
|
162
|
+
|
163
|
+
attributes = parse_attributes if self.has_attributes?
|
164
|
+
|
165
|
+
if @element_map.key? name
|
166
|
+
deserializer = @element_map[name]
|
167
|
+
|
168
|
+
if deserializer.is_a?(Class) && deserializer.include?(XmlDeserializable)
|
169
|
+
value = deserializer.xml_deserialize(self)
|
170
|
+
elsif deserializer.is_a? Proc
|
171
|
+
value = deserializer.call(self)
|
172
|
+
else
|
173
|
+
# Omit php stuff for error creation
|
174
|
+
fail "Could not use this type as a deserializer: #{deserializer.inspect}"
|
175
|
+
end
|
176
|
+
else
|
177
|
+
value = Element::Base.xml_deserialize(self)
|
178
|
+
end
|
179
|
+
{
|
180
|
+
'name' => name,
|
181
|
+
'value' => value,
|
182
|
+
'attributes' => attributes
|
183
|
+
}
|
184
|
+
end
|
185
|
+
|
186
|
+
# Grabs all the attributes from the current element, and returns them as a
|
187
|
+
# key-value array.
|
188
|
+
#
|
189
|
+
# If the attributes are part of the same namespace, they will simply be
|
190
|
+
# short keys. If they are defined on a different namespace, the attribute
|
191
|
+
# name will be retured in clark-notation.
|
192
|
+
#
|
193
|
+
# @return [Hash]
|
194
|
+
def parse_attributes
|
195
|
+
attributes = {}
|
196
|
+
|
197
|
+
while move_to_next_attribute != 0
|
198
|
+
if namespace_uri
|
199
|
+
# Ignoring 'xmlns', it doesn't make any sense.
|
200
|
+
next if namespace_uri == 'http://www.w3.org/2000/xmlns/'
|
201
|
+
|
202
|
+
name = clark
|
203
|
+
attributes[name] = value
|
204
|
+
else
|
205
|
+
attributes[local_name] = value
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
move_to_element
|
210
|
+
|
211
|
+
attributes
|
212
|
+
end
|
213
|
+
|
214
|
+
# TODO: document this
|
215
|
+
def initialize
|
216
|
+
initialize_context_stack_attributes
|
217
|
+
end
|
218
|
+
|
219
|
+
# TODO: documentation
|
220
|
+
def xml(input)
|
221
|
+
fail 'XML document already loaded' if @reader
|
222
|
+
|
223
|
+
if input.is_a? String
|
224
|
+
@reader = ::LibXML::XML::Reader.string(input)
|
225
|
+
elsif input.is_a? File
|
226
|
+
@reader = ::LibXML::XML::Reader.file(input)
|
227
|
+
elsif input.is_a? StringIO
|
228
|
+
@reader = ::LibXML::XML::Reader.io(input)
|
229
|
+
else
|
230
|
+
fail 'Unable to load XML document'
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
# TODO: documentation
|
235
|
+
def method_missing(name, *args)
|
236
|
+
@reader.send(name, *args)
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
module Tilia
|
2
|
+
module Xml
|
3
|
+
# XML parsing and writing service.
|
4
|
+
#
|
5
|
+
# You are encouraged to make a instance of this for your application and
|
6
|
+
# potentially extend it, as a central API point for dealing with xml and
|
7
|
+
# configuring the reader and writer.
|
8
|
+
class Service
|
9
|
+
# This is the element map. It contains a list of XML elements (in clark
|
10
|
+
# notation) as keys and PHP class names as values.
|
11
|
+
#
|
12
|
+
# The PHP class names must implement Sabre\Xml\Element.
|
13
|
+
#
|
14
|
+
# Values may also be a callable. In that case the function will be called
|
15
|
+
# directly.
|
16
|
+
#
|
17
|
+
# @return [Hash]
|
18
|
+
attr_accessor :element_map
|
19
|
+
|
20
|
+
# This is a list of namespaces that you want to give default prefixes.
|
21
|
+
#
|
22
|
+
# You must make sure you create this entire list before starting to write.
|
23
|
+
# They should be registered on the root element.
|
24
|
+
#
|
25
|
+
# @return [Hash]
|
26
|
+
attr_accessor :namespace_map
|
27
|
+
|
28
|
+
# Returns a fresh XML Reader
|
29
|
+
#
|
30
|
+
# @return [Reader]
|
31
|
+
def reader
|
32
|
+
reader = Reader.new
|
33
|
+
reader.element_map = @element_map
|
34
|
+
reader
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns a fresh xml writer
|
38
|
+
#
|
39
|
+
# @return [Writer]
|
40
|
+
def writer
|
41
|
+
writer = Writer.new
|
42
|
+
writer.namespace_map = @namespace_map
|
43
|
+
writer
|
44
|
+
end
|
45
|
+
|
46
|
+
# Parses a document in full.
|
47
|
+
#
|
48
|
+
# Input may be specified as a string or readable stream resource.
|
49
|
+
# The returned value is the value of the root document.
|
50
|
+
#
|
51
|
+
# Specifying the context_uri allows the parser to figure out what the URI
|
52
|
+
# of the document was. This allows relative URIs within the document to be
|
53
|
+
# expanded easily.
|
54
|
+
#
|
55
|
+
# The root_element_name is specified by reference and will be populated
|
56
|
+
# with the root element name of the document.
|
57
|
+
#
|
58
|
+
# @param [String, File, StringIO] input
|
59
|
+
# @param [String, nil] context_uri
|
60
|
+
# @param [String, nil] root_element_name
|
61
|
+
# @raise [ParseException]
|
62
|
+
# @return [Array, Object, String]
|
63
|
+
def parse(input, context_uri = nil, root_element_name = nil)
|
64
|
+
# Skip php short commings
|
65
|
+
reader = self.reader
|
66
|
+
reader.context_uri = context_uri
|
67
|
+
reader.xml(input)
|
68
|
+
|
69
|
+
result = reader.parse
|
70
|
+
root_element_name.replace(result['name'])
|
71
|
+
result['value']
|
72
|
+
end
|
73
|
+
|
74
|
+
# Parses a document in full, and specify what the expected root element
|
75
|
+
# name is.
|
76
|
+
#
|
77
|
+
# This function works similar to parse, but the difference is that the
|
78
|
+
# user can specify what the expected name of the root element should be,
|
79
|
+
# in clark notation.
|
80
|
+
#
|
81
|
+
# This is useful in cases where you expected a specific document to be
|
82
|
+
# passed, and reduces the amount of if statements.
|
83
|
+
#
|
84
|
+
# @param [String] root_element_name
|
85
|
+
# @param [String, File, StringIO] input
|
86
|
+
# @param [String, nil] context_uri
|
87
|
+
# @return [void]
|
88
|
+
def expect(root_element_name, input, context_uri = nil)
|
89
|
+
# Skip php short commings
|
90
|
+
reader = self.reader
|
91
|
+
reader.context_uri = context_uri
|
92
|
+
reader.xml(input)
|
93
|
+
|
94
|
+
result = reader.parse
|
95
|
+
if root_element_name != result['name']
|
96
|
+
fail Tilia::Xml::ParseException, "Expected #{root_element_name} but received #{result['name']} as the root element"
|
97
|
+
end
|
98
|
+
result['value']
|
99
|
+
end
|
100
|
+
|
101
|
+
# Generates an XML document in one go.
|
102
|
+
#
|
103
|
+
# The $rootElement must be specified in clark notation.
|
104
|
+
# The value must be a string, an array or an object implementing
|
105
|
+
# XmlSerializable. Basically, anything that's supported by the Writer
|
106
|
+
# object.
|
107
|
+
#
|
108
|
+
# context_uri can be used to specify a sort of 'root' of the PHP application,
|
109
|
+
# in case the xml document is used as a http response.
|
110
|
+
#
|
111
|
+
# This allows an implementor to easily create URI's relative to the root
|
112
|
+
# of the domain.
|
113
|
+
#
|
114
|
+
# @param [String] root_element_name
|
115
|
+
# @param [String, Array, XmlSerializable] value
|
116
|
+
# @param [String, nil] context_uri
|
117
|
+
# @return [void]
|
118
|
+
def write(root_element_name, value, context_uri = nil)
|
119
|
+
writer = self.writer
|
120
|
+
writer.open_memory
|
121
|
+
writer.context_uri = context_uri
|
122
|
+
writer.set_indent(true)
|
123
|
+
writer.start_document
|
124
|
+
writer.write_element(root_element_name, value)
|
125
|
+
writer.output_memory
|
126
|
+
end
|
127
|
+
|
128
|
+
# Parses a clark-notation string, and returns the namespace and element
|
129
|
+
# name components.
|
130
|
+
#
|
131
|
+
# If the string was invalid, it will throw an InvalidArgumentException.
|
132
|
+
#
|
133
|
+
# @param [String] str
|
134
|
+
# @raise [InvalidArgumentException]
|
135
|
+
# @return [Array]
|
136
|
+
def self.parse_clark_notation(str)
|
137
|
+
if str =~ /^{([^}]*)}(.*)/
|
138
|
+
[Regexp.last_match[1], Regexp.last_match[2]]
|
139
|
+
else
|
140
|
+
fail ArgumentError, "'#{str}' is not a valid clark-notation formatted string"
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# TODO: document
|
145
|
+
def initialize
|
146
|
+
@element_map = {}
|
147
|
+
@namespace_map = {}
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,261 @@
|
|
1
|
+
require 'libxml'
|
2
|
+
module Tilia
|
3
|
+
module Xml
|
4
|
+
# The XML Writer class.
|
5
|
+
#
|
6
|
+
# This class works exactly as PHP's built-in XMLWriter, with a few additions.
|
7
|
+
#
|
8
|
+
# Namespaces can be registered beforehand, globally. When the first element is
|
9
|
+
# written, namespaces will automatically be declared.
|
10
|
+
#
|
11
|
+
# The write_attribute, startElement and write_element can now take a
|
12
|
+
# clark-notation element name (example: {http://www.w3.org/2005/Atom}link).
|
13
|
+
#
|
14
|
+
# If, when writing the namespace is a known one a prefix will automatically be
|
15
|
+
# selected, otherwise a random prefix will be generated.
|
16
|
+
#
|
17
|
+
# Instead of standard string values, the writer can take Element classes (as
|
18
|
+
# defined by this library) to delegate the serialization.
|
19
|
+
#
|
20
|
+
# The write() method can take array structures to quickly write out simple xml
|
21
|
+
# trees.
|
22
|
+
class Writer
|
23
|
+
include ContextStackTrait
|
24
|
+
|
25
|
+
protected
|
26
|
+
|
27
|
+
# Any namespace that the writer is asked to write, will be added here.
|
28
|
+
#
|
29
|
+
# Any of these elements will get a new namespace definition *every single
|
30
|
+
# time* they are used, but this array allows the writer to make sure that
|
31
|
+
# the prefixes are consistent anyway.
|
32
|
+
#
|
33
|
+
# @return [Hash]
|
34
|
+
attr_accessor :adhoc_namespaces
|
35
|
+
|
36
|
+
# When the first element is written, this flag is set to true.
|
37
|
+
#
|
38
|
+
# This ensures that the namespaces in the namespaces map are only written
|
39
|
+
# once.
|
40
|
+
#
|
41
|
+
# @return [Boolean]
|
42
|
+
attr_accessor :namespaces_written
|
43
|
+
|
44
|
+
public
|
45
|
+
|
46
|
+
# Writes a value to the output stream.
|
47
|
+
#
|
48
|
+
# The following values are supported:
|
49
|
+
# 1. Scalar values will be written as-is, as text.
|
50
|
+
# 2. Null values will be skipped (resulting in a short xml tag).
|
51
|
+
# 3. If a value is an instance of an Element class, writing will be
|
52
|
+
# delegated to the object.
|
53
|
+
# 4. If a value is an array, two formats are supported.
|
54
|
+
#
|
55
|
+
# Array format 1:
|
56
|
+
# [
|
57
|
+
# "{namespace}name1" => "..",
|
58
|
+
# "{namespace}name2" => "..",
|
59
|
+
# ]
|
60
|
+
#
|
61
|
+
# One element will be created for each key in this array. The values of
|
62
|
+
# this array support any format this method supports (this method is
|
63
|
+
# called recursively).
|
64
|
+
#
|
65
|
+
# Array format 2:
|
66
|
+
#
|
67
|
+
# [
|
68
|
+
# [
|
69
|
+
# "name" => "{namespace}name1"
|
70
|
+
# "value" => "..",
|
71
|
+
# "attributes" => [
|
72
|
+
# "attr" => "attribute value",
|
73
|
+
# ]
|
74
|
+
# ],
|
75
|
+
# [
|
76
|
+
# "name" => "{namespace}name1"
|
77
|
+
# "value" => "..",
|
78
|
+
# "attributes" => [
|
79
|
+
# "attr" => "attribute value",
|
80
|
+
# ]
|
81
|
+
# ]
|
82
|
+
# ]
|
83
|
+
#
|
84
|
+
# @param value
|
85
|
+
# @return [void]
|
86
|
+
def write(value)
|
87
|
+
if value.is_a?(Numeric) || value.is_a?(String)
|
88
|
+
write_string(value.to_s)
|
89
|
+
elsif value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
90
|
+
write_string(value.to_s)
|
91
|
+
elsif value.is_a? XmlSerializable
|
92
|
+
value.xml_serialize(self)
|
93
|
+
elsif value.nil?
|
94
|
+
# noop
|
95
|
+
elsif value.is_a?(Hash) || value.is_a?(Array)
|
96
|
+
# Code for ruby implementation
|
97
|
+
if value.is_a?(Array)
|
98
|
+
hash = {}
|
99
|
+
value.each_with_index do |v, i|
|
100
|
+
hash[i] = v
|
101
|
+
end
|
102
|
+
value = hash
|
103
|
+
end
|
104
|
+
|
105
|
+
value.each do |name, item|
|
106
|
+
if name.is_a? Fixnum
|
107
|
+
# This item has a numeric index. We expect to be an array with a name and a value.
|
108
|
+
unless item.is_a?(Hash) && item.key?('name') && item.key?('value')
|
109
|
+
fail ArgumentError, 'When passing an array to ->write with numeric indices, every item must be an array containing the "name" and "value" key'
|
110
|
+
end
|
111
|
+
|
112
|
+
attributes = item.key?('attributes') ? item['attributes'] : []
|
113
|
+
name = item['name']
|
114
|
+
item = item['value']
|
115
|
+
elsif item.is_a?(Hash) && item.key?('value')
|
116
|
+
# This item has a text index. We expect to be an array with a value and optional attributes.
|
117
|
+
attributes = item.key?('attributes') ? item['attributes'] : []
|
118
|
+
item = item['value']
|
119
|
+
else
|
120
|
+
# If it's an array with text-indices, we expect every item's
|
121
|
+
# key to be an xml element name in clark notation.
|
122
|
+
# No attributes can be passed.
|
123
|
+
attributes = []
|
124
|
+
end
|
125
|
+
|
126
|
+
start_element(name)
|
127
|
+
write_attributes(attributes)
|
128
|
+
write(item)
|
129
|
+
end_element
|
130
|
+
end
|
131
|
+
else
|
132
|
+
fail ArgumentError, "The writer cannot serialize objects of type: #{value.class}"
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# Starts an element.
|
137
|
+
#
|
138
|
+
# @param [String] name
|
139
|
+
# @return [Boolean]
|
140
|
+
def start_element(name)
|
141
|
+
if name[0] == '{'
|
142
|
+
(namespace, local_name) = Service.parse_clark_notation(name)
|
143
|
+
|
144
|
+
if @namespace_map.key? namespace
|
145
|
+
result = start_element_ns(@namespace_map[namespace], local_name, nil)
|
146
|
+
else
|
147
|
+
# An empty namespace means it's the global namespace. This is
|
148
|
+
# allowed, but it mustn't get a prefix.
|
149
|
+
if namespace == ''
|
150
|
+
result = start_element(local_name)
|
151
|
+
write_attribute('xmlns', '')
|
152
|
+
else
|
153
|
+
unless @adhoc_namespaces.key? namespace
|
154
|
+
@adhoc_namespaces[namespace] = 'x' + (@adhoc_namespaces.size + 1).to_s
|
155
|
+
end
|
156
|
+
result = start_element_ns(@adhoc_namespaces[namespace], local_name, namespace)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
else
|
160
|
+
result = @writer.start_element(name)
|
161
|
+
end
|
162
|
+
|
163
|
+
unless @namespaces_written
|
164
|
+
@namespace_map.each do |ns, prefix|
|
165
|
+
write_attribute((prefix ? 'xmlns:' + prefix : 'xmlns'), ns)
|
166
|
+
end
|
167
|
+
@namespaces_written = true
|
168
|
+
end
|
169
|
+
|
170
|
+
result
|
171
|
+
end
|
172
|
+
|
173
|
+
# Write a full element tag.
|
174
|
+
#
|
175
|
+
# This method automatically closes the element as well.
|
176
|
+
#
|
177
|
+
# @param [String] name
|
178
|
+
# @param [String] content
|
179
|
+
# @return [Boolean]
|
180
|
+
def write_element(name, content = nil)
|
181
|
+
start_element(name)
|
182
|
+
write(content) unless content.nil?
|
183
|
+
end_element
|
184
|
+
end
|
185
|
+
|
186
|
+
# Writes a list of attributes.
|
187
|
+
#
|
188
|
+
# Attributes are specified as a key->value array.
|
189
|
+
#
|
190
|
+
# The key is an attribute name. If the key is a 'localName', the current
|
191
|
+
# xml namespace is assumed. If it's a 'clark notation key', this namespace
|
192
|
+
# will be used instead.
|
193
|
+
#
|
194
|
+
# @param [Hash] attributes
|
195
|
+
# @return [void]
|
196
|
+
def write_attributes(attributes)
|
197
|
+
attributes.each do |name, value|
|
198
|
+
write_attribute(name, value)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
# Writes a new attribute.
|
203
|
+
#
|
204
|
+
# The name may be specified in clark-notation.
|
205
|
+
#
|
206
|
+
# Returns true when successful.
|
207
|
+
#
|
208
|
+
# @param [String] name
|
209
|
+
# @param [String] value
|
210
|
+
# @return [Boolean]
|
211
|
+
def write_attribute(name, value)
|
212
|
+
if name[0] == '{'
|
213
|
+
(namespace, local_name) = Service.parse_clark_notation(name)
|
214
|
+
if @namespace_map.key? namespace
|
215
|
+
# It's an attribute with a namespace we know
|
216
|
+
write_attribute(
|
217
|
+
@namespace_map[namespace] + ':' + local_name,
|
218
|
+
value
|
219
|
+
)
|
220
|
+
else
|
221
|
+
# We don't know the namespace, we must add it in-line
|
222
|
+
@adhoc_namespaces[namespace] = 'x' + (@adhoc_namespaces.size + 1).to_s unless @adhoc_namespaces.key?(namespace)
|
223
|
+
|
224
|
+
write_attribute_ns(
|
225
|
+
@adhoc_namespaces[namespace],
|
226
|
+
local_name,
|
227
|
+
namespace,
|
228
|
+
value
|
229
|
+
)
|
230
|
+
end
|
231
|
+
else
|
232
|
+
@writer.write_attribute(name, value)
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
# TODO: document this
|
237
|
+
def initialize
|
238
|
+
@adhoc_namespaces = {}
|
239
|
+
@namespaces_written = false
|
240
|
+
initialize_context_stack_attributes
|
241
|
+
end
|
242
|
+
|
243
|
+
# TODO: documentation
|
244
|
+
def open_memory
|
245
|
+
fail 'XML document already created' if @writer
|
246
|
+
|
247
|
+
@writer = ::LibXML::XML::Writer.string
|
248
|
+
end
|
249
|
+
|
250
|
+
# TODO: documentation
|
251
|
+
def output_memory
|
252
|
+
@writer.result
|
253
|
+
end
|
254
|
+
|
255
|
+
# TODO: documentation
|
256
|
+
def method_missing(name, *args)
|
257
|
+
@writer.send(name, *args)
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Tilia
|
2
|
+
module Xml
|
3
|
+
# Implementing the XmlDeserializable interface allows you to use a class as a
|
4
|
+
# deserializer for a specific element.
|
5
|
+
module XmlDeserializable
|
6
|
+
# The deserialize method is called during xml parsing.
|
7
|
+
#
|
8
|
+
# This method is called statictly, this is because in theory this method
|
9
|
+
# may be used as a type of constructor, or factory method.
|
10
|
+
#
|
11
|
+
# Often you want to return an instance of the current class, but you are
|
12
|
+
# free to return other data as well.
|
13
|
+
#
|
14
|
+
# You are responsible for advancing the reader to the next element. Not
|
15
|
+
# doing anything will result in a never-ending loop.
|
16
|
+
#
|
17
|
+
# If you just want to skip parsing for this element altogether, you can
|
18
|
+
# just call $reader->next();
|
19
|
+
#
|
20
|
+
# $reader->parseInnerTree() will parse the entire sub-tree, and advance to
|
21
|
+
# the next element.
|
22
|
+
#
|
23
|
+
# @param [Reader] _reader
|
24
|
+
# @return mixed
|
25
|
+
def xml_deserialize(_reader)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|