handsoap 1.1.0

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,320 @@
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 the outer XML for this element.
186
+ def to_xml
187
+ raise NotImplementedError.new
188
+ end
189
+ # Returns the outer XML for this element, preserving the original formatting.
190
+ def to_raw
191
+ raise NotImplementedError.new
192
+ end
193
+ # alias of +xpath+
194
+ def /(expression)
195
+ self.xpath(expression)
196
+ end
197
+ # Returns the attribute value of the underlying element.
198
+ #
199
+ # Shortcut for:
200
+ #
201
+ # (node/"@attribute_name").to_s
202
+ def [](attribute_name)
203
+ raise NotImplementedError.new
204
+ end
205
+ end
206
+
207
+ # Driver for +libxml+.
208
+ #
209
+ # http://libxml.rubyforge.org/
210
+ class LibXMLDriver
211
+ include XmlElement
212
+ def node_name
213
+ @element.name
214
+ end
215
+ def xpath(expression, ns = nil)
216
+ ns = {} if ns.nil?
217
+ ns = @namespaces.merge(ns)
218
+ assert_prefixes!(expression, ns)
219
+ NodeSelection.new(@element.find(expression, ns.map{|k,v| "#{k}:#{v}" }).to_a.map{|node| LibXMLDriver.new(node, ns) })
220
+ end
221
+ def [](attribute_name)
222
+ raise ArgumentError.new unless attribute_name.kind_of? String
223
+ @element[attribute_name]
224
+ end
225
+ def to_xml
226
+ @element.to_s(:indent => true)
227
+ end
228
+ def to_raw
229
+ @element.to_s(:indent => false)
230
+ end
231
+ def to_s
232
+ if @element.kind_of? LibXML::XML::Attr
233
+ @element.value
234
+ else
235
+ @element.content
236
+ end
237
+ end
238
+ end
239
+
240
+ # Driver for +REXML+
241
+ #
242
+ # http://www.germane-software.com/software/rexml/
243
+ class REXMLDriver
244
+ include XmlElement
245
+ def node_name
246
+ @element.name
247
+ end
248
+ def xpath(expression, ns = nil)
249
+ ns = {} if ns.nil?
250
+ ns = @namespaces.merge(ns)
251
+ assert_prefixes!(expression, ns)
252
+ NodeSelection.new(REXML::XPath.match(@element, expression, ns).map{|node| REXMLDriver.new(node, ns) })
253
+ end
254
+ def [](attribute_name)
255
+ raise ArgumentError.new unless attribute_name.kind_of? String
256
+ @element.attributes[attribute_name]
257
+ end
258
+ def to_xml
259
+ require 'rexml/formatters/pretty'
260
+ formatter = REXML::Formatters::Pretty.new
261
+ out = String.new
262
+ formatter.write(@element, out)
263
+ # patch for REXML's broken formatting
264
+ out.gsub(/>\n\s+([^<]+)\n\s+<\//, ">\\1</")
265
+ end
266
+ def to_raw
267
+ @element.to_s
268
+ end
269
+ def to_s
270
+ if @element.kind_of? REXML::Attribute
271
+ @element.value
272
+ else
273
+ @element.text
274
+ end
275
+ end
276
+ end
277
+
278
+ # Driver for +Nokogiri+
279
+ #
280
+ # http://nokogiri.rubyforge.org/nokogiri/
281
+ class NokogiriDriver
282
+ include XmlElement
283
+ def node_name
284
+ @element.name
285
+ end
286
+ def xpath(expression, ns = nil)
287
+ ns = {} if ns.nil?
288
+ ns = @namespaces.merge(ns)
289
+ assert_prefixes!(expression, ns)
290
+ NodeSelection.new(@element.xpath(expression, ns).map{|node| NokogiriDriver.new(node, ns) })
291
+ end
292
+ def [](attribute_name)
293
+ raise ArgumentError.new unless attribute_name.kind_of? String
294
+ @element[attribute_name]
295
+ end
296
+ def to_xml
297
+ @element.serialize(:encoding => 'UTF-8')
298
+ end
299
+ def to_raw
300
+ @element.serialize(:encoding => 'UTF-8', :save_with => Nokogiri::XML::Node::SaveOptions::AS_XML)
301
+ end
302
+ def to_s
303
+ if @element.kind_of?(Nokogiri::XML::Text) || @element.kind_of?(Nokogiri::XML::CDATA)
304
+ element = @element
305
+ elsif @element.kind_of?(Nokogiri::XML::Attr)
306
+ return @element.value
307
+ else
308
+ element = @element.children.first
309
+ end
310
+ return if element.nil?
311
+ # This looks messy because it is .. Nokogiri's interface is in a flux
312
+ if element.kind_of?(Nokogiri::XML::CDATA)
313
+ element.serialize(:encoding => 'UTF-8').gsub(/^<!\[CDATA\[/, "").gsub(/\]\]>$/, "")
314
+ else
315
+ element.serialize(:encoding => 'UTF-8').gsub('&lt;', '<').gsub('&gt;', '>').gsub('&quot;', '"').gsub('&apos;', "'").gsub('&amp;', '&')
316
+ end
317
+ end
318
+ end
319
+ end
320
+ end