handsoap 1.1.0

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