pgericson-handsoap 1.1.7

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.
@@ -0,0 +1,221 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ module Handsoap
4
+
5
+ # XmlMason is a simple XML builder.
6
+ module XmlMason
7
+
8
+ XML_ESCAPE = { '&' => '&amp;', '"' => '&quot;', '>' => '&gt;', '<' => '&lt;' }
9
+
10
+ def self.xml_escape(s)
11
+ s.to_s.gsub(/[&"><]/) { |special| XML_ESCAPE[special] }
12
+ end
13
+
14
+ class Node
15
+ def initialize
16
+ @namespaces = {}
17
+ end
18
+ def add(node_name, value = nil, *flags) # :yields: Handsoap::XmlMason::Element
19
+ prefix, name = parse_ns(node_name)
20
+ node = append_child Element.new(self, prefix, name, value, flags)
21
+ if block_given?
22
+ yield node
23
+ end
24
+ end
25
+ # Registers a prefix for a namespace.
26
+ #
27
+ # You must register a namespace, before you can refer it.
28
+ def alias(prefix, namespaces)
29
+ @namespaces[prefix] = namespaces
30
+ end
31
+ # Finds the first element whos +node_name+ equals +name+
32
+ #
33
+ # Doesn't regard namespaces/prefixes.
34
+ def find(name)
35
+ raise NotImplementedError.new
36
+ end
37
+ # Finds all elements whos +node_name+ equals +name+
38
+ #
39
+ # Doesn't regard namespaces/prefixes.
40
+ def find_all(name)
41
+ raise NotImplementedError.new
42
+ end
43
+ def parse_ns(name)
44
+ matches = name.match /^([^:]+):(.*)$/
45
+ if matches
46
+ [matches[1] == '*' ? @prefix : matches[1], matches[2]]
47
+ else
48
+ [nil, name]
49
+ end
50
+ end
51
+ private :parse_ns
52
+ end
53
+
54
+ class Document < Node
55
+ def initialize # :yields: Document
56
+ super
57
+ @document_element = nil
58
+ @xml_header = true
59
+ if block_given?
60
+ yield self
61
+ end
62
+ end
63
+ def xml_header=(xml_header)
64
+ @xml_header = !! xml_header
65
+ end
66
+ def append_child(node)
67
+ if not @document_element.nil?
68
+ raise "There can only be one element at the top level."
69
+ end
70
+ @document_element = node
71
+ end
72
+ def find(name)
73
+ @document_element.find(name)
74
+ end
75
+ def find_all(name)
76
+ @document_element.find_all(name)
77
+ end
78
+ def get_namespace(prefix)
79
+ @namespaces[prefix] || raise("No alias registered for prefix '#{prefix}'")
80
+ end
81
+ def defines_namespace?(prefix)
82
+ false
83
+ end
84
+ def to_s
85
+ if @document_element.nil?
86
+ raise "No document element added."
87
+ end
88
+ (@xml_header ? "<?xml version='1.0' ?>\n" : "") + @document_element.to_s
89
+ end
90
+ end
91
+
92
+ class TextNode
93
+ def initialize(text)
94
+ @text = text
95
+ end
96
+ def to_s(indentation = '')
97
+ XmlMason.xml_escape(@text)
98
+ end
99
+ end
100
+
101
+ class RawContent < TextNode
102
+ def to_s(indentation = '')
103
+ @text
104
+ end
105
+ end
106
+
107
+ class Element < Node
108
+ def initialize(parent, prefix, node_name, value = nil, flags = []) # :yields: Handsoap::XmlMason::Element
109
+ super()
110
+ # if prefix.to_s == ""
111
+ # raise "missing prefix"
112
+ # end
113
+ @parent = parent
114
+ @prefix = prefix
115
+ @node_name = node_name
116
+ @children = []
117
+ @attributes = {}
118
+ if not value.nil?
119
+ set_value value.to_s, *flags
120
+ end
121
+ if block_given?
122
+ yield self
123
+ end
124
+ end
125
+ # Returns the document that this element belongs to, or self if this is the document.
126
+ def document
127
+ @parent.respond_to?(:document) ? @parent.document : @parent
128
+ end
129
+ # Returns the qname (prefix:nodename)
130
+ def full_name
131
+ @prefix.nil? ? @node_name : (@prefix + ":" + @node_name)
132
+ end
133
+ # Adds a child node.
134
+ #
135
+ # You usually won't need to call this method, but will rather use +add+
136
+ def append_child(node)
137
+ if value_node?
138
+ raise "Element already has a text value. Can't add nodes"
139
+ end
140
+ @children << node
141
+ return node
142
+ end
143
+ # Sets the inner text of this element.
144
+ #
145
+ # By default the string is escaped, but you can pass the flag :raw to inject XML.
146
+ #
147
+ # You usually won't need to call this method, but will rather use +add+
148
+ def set_value(value, *flags)
149
+ if @children.length > 0
150
+ raise "Element already has children. Can't set value"
151
+ end
152
+ if flags && flags.include?(:raw)
153
+ @children = [RawContent.new(value)]
154
+ else
155
+ @children = [TextNode.new(value)]
156
+ end
157
+ end
158
+ # Sets the value of an attribute.
159
+ def set_attr(name, value)
160
+ full_name = parse_ns(name).join(":")
161
+ @attributes[name] = value
162
+ end
163
+ def find(name)
164
+ name = name.to_s if name.kind_of? Symbol
165
+ if @node_name == name || full_name == name
166
+ return self
167
+ end
168
+ @children.each do |node|
169
+ if node.respond_to? :find
170
+ tmp = node.find(name)
171
+ if tmp
172
+ return tmp
173
+ end
174
+ end
175
+ end
176
+ return nil
177
+ end
178
+ def find_all(name)
179
+ name = name.to_s if name.kind_of? Symbol
180
+ result = []
181
+ if @node_name == name || full_name == name
182
+ result << self
183
+ end
184
+ @children.each do |node|
185
+ if node.respond_to? :find
186
+ result = result.concat(node.find_all(name))
187
+ end
188
+ end
189
+ return result
190
+ end
191
+ def value_node?
192
+ @children.length == 1 && @children[0].kind_of?(TextNode)
193
+ end
194
+ def get_namespace(prefix)
195
+ @namespaces[prefix] || @parent.get_namespace(prefix)
196
+ end
197
+ def defines_namespace?(prefix)
198
+ @attributes.keys.include?("xmlns:#{prefix}") || @parent.defines_namespace?(prefix)
199
+ end
200
+ def to_s(indentation = '')
201
+ # todo resolve attribute prefixes aswell
202
+ if @prefix && (not defines_namespace?(@prefix))
203
+ set_attr "xmlns:#{@prefix}", get_namespace(@prefix)
204
+ end
205
+ name = XmlMason.xml_escape(full_name)
206
+ attr = (@attributes.any? ? (" " + @attributes.map { |key, value| XmlMason.xml_escape(key) + '="' + XmlMason.xml_escape(value) + '"' }.join(" ")) : "")
207
+ if @children.any?
208
+ if value_node?
209
+ children = @children[0].to_s(indentation + " ")
210
+ else
211
+ children = @children.map { |node| "\n" + node.to_s(indentation + " ") }.join("") + "\n" + indentation
212
+ end
213
+ indentation + "<" + name + attr + ">" + children + "</" + name + ">"
214
+ else
215
+ indentation + "<" + name + attr + " />"
216
+ end
217
+ end
218
+ end
219
+ end
220
+
221
+ end
@@ -0,0 +1,337 @@
1
+ # -*- coding: utf-8 -*-
2
+ module Handsoap
3
+ #
4
+ # A simple frontend for parsing XML document with Xpath.
5
+ #
6
+ # This provides a unified interface for multiple xpath-capable dom-parsers,
7
+ # allowing seamless switching between the underlying implementations.
8
+ #
9
+ # A document is loaded using the function Handsoap::XmlQueryFront.parse_string, passing
10
+ # the xml source string and a driver, which can (currently) be one of:
11
+ #
12
+ # :rexml
13
+ # :nokogiri
14
+ # :libxml
15
+ #
16
+ # The resulting object is a wrapper, of the type Handsoap::XmlQueryFront::XmlElement.
17
+ #
18
+ module XmlQueryFront
19
+
20
+ # This error is raised if the document didn't parse
21
+ class ParseError < RuntimeError; end
22
+
23
+ # Loads requirements for a driver.
24
+ #
25
+ # This function is implicitly called by +parse_string+.
26
+ def self.load_driver!(driver)
27
+ if driver == :rexml
28
+ require 'rexml/document'
29
+ elsif driver == :nokogiri
30
+ require 'nokogiri'
31
+ begin
32
+ gem('nokogiri') # work around bug in rubygems for Ruby 1.9
33
+
34
+ if Gem.loaded_specs['nokogiri'].version < Gem::Version.new('1.3.0')
35
+ raise "Incompatible version of Nokogiri. Please upgrade gem."
36
+ end
37
+ rescue NoMethodError
38
+ end
39
+ elsif driver == :libxml
40
+ require 'libxml'
41
+ else
42
+ raise "Unknown driver #{driver}"
43
+ end
44
+ return driver
45
+ end
46
+
47
+ # Returns a wrapped XML parser, using the requested driver.
48
+ #
49
+ # +driver+ can be one of the following:
50
+ # :rexml
51
+ # :nokogiri
52
+ # :libxml
53
+ def self.parse_string(xml_string, driver)
54
+ load_driver!(driver)
55
+ if driver == :rexml
56
+ doc = REXML::Document.new(xml_string)
57
+ raise ParseError.new if doc.root.nil?
58
+ XmlQueryFront::REXMLDriver.new(doc)
59
+ elsif driver == :nokogiri
60
+ doc = Nokogiri::XML(xml_string)
61
+ raise ParseError.new unless (doc && doc.root && doc.errors.empty?)
62
+ XmlQueryFront::NokogiriDriver.new(doc)
63
+ elsif driver == :libxml
64
+ begin
65
+ LibXML::XML::Error.set_handler &LibXML::XML::Error::QUIET_HANDLER
66
+ doc = XmlQueryFront::LibXMLDriver.new(LibXML::XML::Parser.string(xml_string).parse)
67
+ rescue ArgumentError, LibXML::XML::Error => ex
68
+ raise ParseError.new
69
+ end
70
+ end
71
+ end
72
+
73
+ # NodeSelection is a wrapper around Array, that implicitly delegates XmlElement methods to the first element.
74
+ #
75
+ # It makes mapping code prettier, since you often need to access the first element of a selection.
76
+ class NodeSelection < Array
77
+ def to_i
78
+ self.first.to_i if self.any?
79
+ end
80
+ def to_f
81
+ self.first.to_f if self.any?
82
+ end
83
+ def to_boolean
84
+ self.first.to_boolean if self.any?
85
+ end
86
+ def to_date
87
+ self.first.to_date if self.any?
88
+ end
89
+ def to_s
90
+ self.first.to_s if self.any?
91
+ end
92
+ def node_name
93
+ self.first.node_name if self.any?
94
+ end
95
+ def xpath(expression, ns = nil)
96
+ self.first.xpath(expression, ns)
97
+ end
98
+ def /(expression)
99
+ self.first.xpath(expression)
100
+ end
101
+ def to_xml
102
+ self.first.to_xml if self.any?
103
+ end
104
+ def to_raw
105
+ self.first.to_raw if self.any?
106
+ end
107
+ end
108
+
109
+ # Wraps the underlying (native) xml driver, and provides a uniform interface.
110
+ module XmlElement
111
+ def initialize(element, namespaces = {})
112
+ @element = element
113
+ @namespaces = namespaces
114
+ end
115
+ # Registers a prefix to refer to a namespace.
116
+ #
117
+ # You can either register a nemspace with this function or pass it explicitly to the +xpath+ method.
118
+ def add_namespace(prefix, uri)
119
+ @namespaces[prefix] = uri
120
+ end
121
+ # Checks that an xpath-query doesn't refer to any undefined prefixes in +ns+
122
+ def assert_prefixes!(expression, ns)
123
+ expression.scan(/([a-zA-Z_][a-zA-Z0-9_.-]*):[^:]+/).map{|m| m[0] }.each do |prefix|
124
+ raise "Undefined prefix '#{prefix}' in #{ns.inspect}" if ns[prefix].nil?
125
+ end
126
+ end
127
+ # Returns the value of the element as an integer.
128
+ #
129
+ # See +to_s+
130
+ def to_i
131
+ t = self.to_s
132
+ return if t.nil?
133
+ t.to_i
134
+ end
135
+ # Returns the value of the element as a float.
136
+ #
137
+ # See +to_s+
138
+ def to_f
139
+ t = self.to_s
140
+ return if t.nil?
141
+ t.to_f
142
+ end
143
+ # Returns the value of the element as an boolean.
144
+ #
145
+ # See +to_s+
146
+ def to_boolean
147
+ t = self.to_s
148
+ return if t.nil?
149
+ t.downcase == 'true'
150
+ end
151
+ # Returns the value of the element as a ruby Time object.
152
+ #
153
+ # See +to_s+
154
+ def to_date
155
+ t = self.to_s
156
+ return if t.nil?
157
+ Time.iso8601(t)
158
+ end
159
+ # Returns the inner text content of this element, or the value (if it's an attr or textnode).
160
+ #
161
+ # The output is a UTF-8 encoded string, without xml-entities.
162
+ def to_s
163
+ raise NotImplementedError.new
164
+ end
165
+ # Returns the underlying native element.
166
+ #
167
+ # You shouldn't need to use this, since doing so would void portability.
168
+ def native_element
169
+ @element
170
+ end
171
+ # Returns the node name of the current element.
172
+ def node_name
173
+ raise NotImplementedError.new
174
+ end
175
+ # Queries the document with XPath, relative to the current element.
176
+ #
177
+ # +ns+ Should be a Hash of prefix => namespace
178
+ #
179
+ # Returns a +NodeSelection+
180
+ #
181
+ # See add_namespace
182
+ def xpath(expression, ns = nil)
183
+ raise NotImplementedError.new
184
+ end
185
+ # Returns a +NodeSelection+
186
+ def children
187
+ raise NotImplementedError.new
188
+ end
189
+ # Returns the outer XML for this element.
190
+ def to_xml
191
+ raise NotImplementedError.new
192
+ end
193
+ # Returns the outer XML for this element, preserving the original formatting.
194
+ def to_raw
195
+ raise NotImplementedError.new
196
+ end
197
+ # alias of +xpath+
198
+ def /(expression)
199
+ self.xpath(expression)
200
+ end
201
+ # Returns the attribute value of the underlying element.
202
+ #
203
+ # Shortcut for:
204
+ #
205
+ # (node/"@attribute_name").to_s
206
+ def [](attribute_name)
207
+ raise NotImplementedError.new
208
+ end
209
+ end
210
+
211
+ # Driver for +libxml+.
212
+ #
213
+ # http://libxml.rubyforge.org/
214
+ class LibXMLDriver
215
+ include XmlElement
216
+ def node_name
217
+ @element.name
218
+ end
219
+ def xpath(expression, ns = nil)
220
+ ns = {} if ns.nil?
221
+ ns = @namespaces.merge(ns)
222
+ assert_prefixes!(expression, ns)
223
+ NodeSelection.new(@element.find(expression, ns.map{|k,v| "#{k}:#{v}" }).to_a.map{|node| LibXMLDriver.new(node, ns) })
224
+ end
225
+ def children
226
+ NodeSelection.new(@element.children.map{|node| LibXMLDriver.new(node) })
227
+ end
228
+ def [](attribute_name)
229
+ raise ArgumentError.new unless attribute_name.kind_of? String
230
+ @element[attribute_name]
231
+ end
232
+ def to_xml
233
+ @element.to_s(:indent => true)
234
+ end
235
+ def to_raw
236
+ @element.to_s(:indent => false)
237
+ end
238
+ def to_s
239
+ if @element.kind_of? LibXML::XML::Attr
240
+ @element.value
241
+ else
242
+ @element.content
243
+ end
244
+ end
245
+ end
246
+
247
+ # Driver for +REXML+
248
+ #
249
+ # http://www.germane-software.com/software/rexml/
250
+ class REXMLDriver
251
+ include XmlElement
252
+ def node_name
253
+ if @element.respond_to? :name
254
+ @element.name
255
+ else
256
+ @element.class.name.gsub(/.*::([^:]+)$/, "\\1").downcase
257
+ end
258
+ end
259
+ def xpath(expression, ns = nil)
260
+ ns = {} if ns.nil?
261
+ ns = @namespaces.merge(ns)
262
+ assert_prefixes!(expression, ns)
263
+ NodeSelection.new(REXML::XPath.match(@element, expression, ns).map{|node| REXMLDriver.new(node, ns) })
264
+ end
265
+ def children
266
+ NodeSelection.new(@element.children.map{|node| REXMLDriver.new(node) })
267
+ end
268
+ def [](attribute_name)
269
+ raise ArgumentError.new unless attribute_name.kind_of? String
270
+ @element.attributes[attribute_name]
271
+ end
272
+ def to_xml
273
+ require 'rexml/formatters/pretty'
274
+ formatter = REXML::Formatters::Pretty.new
275
+ out = String.new
276
+ formatter.write(@element, out)
277
+ # patch for REXML's broken formatting
278
+ out.gsub(/>\n\s+([^<]+)\n\s+<\//, ">\\1</")
279
+ end
280
+ def to_raw
281
+ @element.to_s
282
+ end
283
+ def to_s
284
+ if @element.respond_to? :text
285
+ @element.text
286
+ else
287
+ @element.value
288
+ end
289
+ end
290
+ end
291
+
292
+ # Driver for +Nokogiri+
293
+ #
294
+ # http://nokogiri.rubyforge.org/nokogiri/
295
+ class NokogiriDriver
296
+ include XmlElement
297
+ def node_name
298
+ @element.name
299
+ end
300
+ def xpath(expression, ns = nil)
301
+ ns = {} if ns.nil?
302
+ ns = @namespaces.merge(ns)
303
+ assert_prefixes!(expression, ns)
304
+ NodeSelection.new(@element.xpath(expression, ns).map{|node| NokogiriDriver.new(node, ns) })
305
+ end
306
+ def children
307
+ NodeSelection.new(@element.children.map{|node| NokogiriDriver.new(node) })
308
+ end
309
+ def [](attribute_name)
310
+ raise ArgumentError.new unless attribute_name.kind_of? String
311
+ @element[attribute_name]
312
+ end
313
+ def to_xml
314
+ @element.serialize(:encoding => 'UTF-8')
315
+ end
316
+ def to_raw
317
+ @element.serialize(:encoding => 'UTF-8', :save_with => Nokogiri::XML::Node::SaveOptions::AS_XML)
318
+ end
319
+ def to_s
320
+ if @element.kind_of?(Nokogiri::XML::Text) || @element.kind_of?(Nokogiri::XML::CDATA)
321
+ element = @element
322
+ elsif @element.kind_of?(Nokogiri::XML::Attr)
323
+ return @element.value
324
+ else
325
+ element = @element.children.first
326
+ end
327
+ return if element.nil?
328
+ # This looks messy because it is .. Nokogiri's interface is in a flux
329
+ if element.kind_of?(Nokogiri::XML::CDATA)
330
+ element.serialize(:encoding => 'UTF-8').gsub(/^<!\[CDATA\[/, "").gsub(/\]\]>$/, "")
331
+ else
332
+ element.serialize(:encoding => 'UTF-8').gsub('&lt;', '<').gsub('&gt;', '>').gsub('&quot;', '"').gsub('&apos;', "'").gsub('&amp;', '&')
333
+ end
334
+ end
335
+ end
336
+ end
337
+ end