# Copyright (C) 2001, 2002, 2003 by Michael Neumann (mneumann@ntecs.de) # # $Id$ # require "date" require "xmlrpc/base64" require "xmlrpc/datetime" module NQXML class Node def removeChild(node) @children.delete(node) end def childNodes @children end def hasChildNodes not @children.empty? end def [] (index) @children[index] end def nodeType if @entity.instance_of? NQXML::Text then :TEXT elsif @entity.instance_of? NQXML::Comment then :COMMENT #elsif @entity.instance_of? NQXML::Element then :ELEMENT elsif @entity.instance_of? NQXML::Tag then :ELEMENT else :ELSE end end def nodeValue #TODO: error when wrong Entity-type @entity.text end def nodeName #TODO: error when wrong Entity-type @entity.name end end # class Node end # module NQXML module XMLRPC # :nodoc: # Raised when the remote procedure returns a fault-structure, which has two # accessor-methods +faultCode+ an Integer, and +faultString+ a String. class FaultException < StandardError attr_reader :faultCode, :faultString # Creates a new XMLRPC::FaultException instance. # # +faultString+ is passed to StandardError as the +msg+ of the Exception. def initialize(faultCode, faultString) @faultCode = faultCode @faultString = faultString super(@faultString) end # The +faultCode+ and +faultString+ of the exception in a Hash. def to_h {"faultCode" => @faultCode, "faultString" => @faultString} end end # Helper class used to convert types. module Convert # Converts a String to an Integer # # See also String.to_i def self.int(str) str.to_i end # Converts a String to +true+ or +false+ # # Raises an exception if +str+ is not +0+ or +1+ def self.boolean(str) case str when "0" then false when "1" then true else raise "RPC-value of type boolean is wrong" end end # Converts a String to a Float # # See also String.to_f def self.double(str) str.to_f end # Converts a the given +str+ to a +dateTime.iso8601+ formatted date. # # Raises an exception if the String isn't in +dateTime.iso8601+ format. # # See also, XMLRPC::DateTime def self.dateTime(str) case str when /^(-?\d\d\d\d)-?(\d\d)-?(\d\d)T(\d\d):(\d\d):(\d\d)(?:Z|([+-])(\d\d):?(\d\d))?$/ a = [$1, $2, $3, $4, $5, $6].collect{|i| i.to_i} if $7 ofs = $8.to_i*3600 + $9.to_i*60 ofs = -ofs if $7=='+' utc = Time.utc(*a) + ofs a = [ utc.year, utc.month, utc.day, utc.hour, utc.min, utc.sec ] end XMLRPC::DateTime.new(*a) when /^(-?\d\d)-?(\d\d)-?(\d\d)T(\d\d):(\d\d):(\d\d)(Z|([+-]\d\d):(\d\d))?$/ a = [$1, $2, $3, $4, $5, $6].collect{|i| i.to_i} if a[0] < 70 a[0] += 2000 else a[0] += 1900 end if $7 ofs = $8.to_i*3600 + $9.to_i*60 ofs = -ofs if $7=='+' utc = Time.utc(*a) + ofs a = [ utc.year, utc.month, utc.day, utc.hour, utc.min, utc.sec ] end XMLRPC::DateTime.new(*a) else raise "wrong dateTime.iso8601 format " + str end end # Decodes the given +str+ using XMLRPC::Base64.decode def self.base64(str) XMLRPC::Base64.decode(str) end # Converts the given +hash+ to a marshalled object. # # Returns the given +hash+ if an exception occurs. def self.struct(hash) # convert to marshalled object klass = hash["___class___"] if klass.nil? or Config::ENABLE_MARSHALLING == false hash else begin mod = Module klass.split("::").each {|const| mod = mod.const_get(const.strip)} obj = mod.allocate hash.delete "___class___" hash.each {|key, value| obj.instance_variable_set("@#{ key }", value) if key =~ /^([a-zA-Z_]\w*)$/ } obj rescue hash end end end # Converts the given +hash+ to an XMLRPC::FaultException object by passing # the +faultCode+ and +faultString+ attributes of the Hash to # XMLRPC::FaultException.new # # Raises an Exception if the given +hash+ doesn't meet the requirements. # Those requirements being: # * 2 keys # * 'faultCode' key is an Integer # * 'faultString' key is a String def self.fault(hash) if hash.kind_of? Hash and hash.size == 2 and hash.has_key? "faultCode" and hash.has_key? "faultString" and hash["faultCode"].kind_of? Integer and hash["faultString"].kind_of? String XMLRPC::FaultException.new(hash["faultCode"], hash["faultString"]) else raise "wrong fault-structure: #{hash.inspect}" end end end # module Convert # Parser for XML-RPC call and response module XMLParser class AbstractTreeParser def parseMethodResponse(str) methodResponse_document(createCleanedTree(str)) end def parseMethodCall(str) methodCall_document(createCleanedTree(str)) end private # Removes all whitespaces but in the tags i4, i8, int, boolean.... # and all comments def removeWhitespacesAndComments(node) remove = [] childs = node.childNodes.to_a childs.each do |nd| case _nodeType(nd) when :TEXT # TODO: add nil? unless %w(i4 i8 int boolean string double dateTime.iso8601 base64).include? node.nodeName if node.nodeName == "value" if not node.childNodes.to_a.detect {|n| _nodeType(n) == :ELEMENT}.nil? remove << nd if nd.nodeValue.strip == "" end else remove << nd if nd.nodeValue.strip == "" end end when :COMMENT remove << nd else removeWhitespacesAndComments(nd) end end remove.each { |i| node.removeChild(i) } end def nodeMustBe(node, name) cmp = case name when Array name.include?(node.nodeName) when String name == node.nodeName else raise "error" end if not cmp then raise "wrong xml-rpc (name)" end node end # Returns, when successfully the only child-node def hasOnlyOneChild(node, name=nil) if node.childNodes.to_a.size != 1 raise "wrong xml-rpc (size)" end if name != nil then nodeMustBe(node.firstChild, name) end end def assert(b) if not b then raise "assert-fail" end end # The node `node` has empty string or string def text_zero_one(node) nodes = node.childNodes.to_a.size if nodes == 1 text(node.firstChild) elsif nodes == 0 "" else raise "wrong xml-rpc (size)" end end def integer(node) #TODO: check string for float because to_i returnsa # 0 when wrong string nodeMustBe(node, %w(i4 i8 int)) hasOnlyOneChild(node) Convert.int(text(node.firstChild)) end def boolean(node) nodeMustBe(node, "boolean") hasOnlyOneChild(node) Convert.boolean(text(node.firstChild)) end def v_nil(node) nodeMustBe(node, "nil") assert( node.childNodes.to_a.size == 0 ) nil end def string(node) nodeMustBe(node, "string") text_zero_one(node) end def double(node) #TODO: check string for float because to_f returnsa # 0.0 when wrong string nodeMustBe(node, "double") hasOnlyOneChild(node) Convert.double(text(node.firstChild)) end def dateTime(node) nodeMustBe(node, "dateTime.iso8601") hasOnlyOneChild(node) Convert.dateTime( text(node.firstChild) ) end def base64(node) nodeMustBe(node, "base64") #hasOnlyOneChild(node) Convert.base64(text_zero_one(node)) end def member(node) nodeMustBe(node, "member") assert( node.childNodes.to_a.size == 2 ) [ name(node[0]), value(node[1]) ] end def name(node) nodeMustBe(node, "name") #hasOnlyOneChild(node) text_zero_one(node) end def array(node) nodeMustBe(node, "array") hasOnlyOneChild(node, "data") data(node.firstChild) end def data(node) nodeMustBe(node, "data") node.childNodes.to_a.collect do |val| value(val) end end def param(node) nodeMustBe(node, "param") hasOnlyOneChild(node, "value") value(node.firstChild) end def methodResponse(node) nodeMustBe(node, "methodResponse") hasOnlyOneChild(node, %w(params fault)) child = node.firstChild case child.nodeName when "params" [ true, params(child,false) ] when "fault" [ false, fault(child) ] else raise "unexpected error" end end def methodName(node) nodeMustBe(node, "methodName") hasOnlyOneChild(node) text(node.firstChild) end def params(node, call=true) nodeMustBe(node, "params") if call node.childNodes.to_a.collect do |n| param(n) end else # response (only one param) hasOnlyOneChild(node) param(node.firstChild) end end def fault(node) nodeMustBe(node, "fault") hasOnlyOneChild(node, "value") f = value(node.firstChild) Convert.fault(f) end # _nodeType is defined in the subclass def text(node) assert( _nodeType(node) == :TEXT ) assert( node.hasChildNodes == false ) assert( node.nodeValue != nil ) node.nodeValue.to_s end def struct(node) nodeMustBe(node, "struct") hash = {} node.childNodes.to_a.each do |me| n, v = member(me) hash[n] = v end Convert.struct(hash) end def value(node) nodeMustBe(node, "value") nodes = node.childNodes.to_a.size if nodes == 0 return "" elsif nodes > 1 raise "wrong xml-rpc (size)" end child = node.firstChild case _nodeType(child) when :TEXT text_zero_one(node) when :ELEMENT case child.nodeName when "i4", "i8", "int" then integer(child) when "boolean" then boolean(child) when "string" then string(child) when "double" then double(child) when "dateTime.iso8601" then dateTime(child) when "base64" then base64(child) when "struct" then struct(child) when "array" then array(child) when "nil" if Config::ENABLE_NIL_PARSER v_nil(child) else raise "wrong/unknown XML-RPC type 'nil'" end else raise "wrong/unknown XML-RPC type" end else raise "wrong type of node" end end def methodCall(node) nodeMustBe(node, "methodCall") assert( (1..2).include?( node.childNodes.to_a.size ) ) name = methodName(node[0]) if node.childNodes.to_a.size == 2 then pa = params(node[1]) else # no parameters given pa = [] end [name, pa] end end # module TreeParserMixin class AbstractStreamParser def parseMethodResponse(str) parser = @parser_class.new parser.parse(str) raise "No valid method response!" if parser.method_name != nil if parser.fault != nil # is a fault structure [false, parser.fault] else # is a normal return value raise "Missing return value!" if parser.params.size == 0 raise "Too many return values. Only one allowed!" if parser.params.size > 1 [true, parser.params[0]] end end def parseMethodCall(str) parser = @parser_class.new parser.parse(str) raise "No valid method call - missing method name!" if parser.method_name.nil? [parser.method_name, parser.params] end end module StreamParserMixin attr_reader :params attr_reader :method_name attr_reader :fault def initialize(*a) super(*a) @params = [] @values = [] @val_stack = [] @names = [] @name = [] @structs = [] @struct = {} @method_name = nil @fault = nil @data = nil end def startElement(name, attrs=[]) @data = nil case name when "value" @value = nil when "nil" raise "wrong/unknown XML-RPC type 'nil'" unless Config::ENABLE_NIL_PARSER @value = :nil when "array" @val_stack << @values @values = [] when "struct" @names << @name @name = [] @structs << @struct @struct = {} end end def endElement(name) @data ||= "" case name when "string" @value = @data when "i4", "i8", "int" @value = Convert.int(@data) when "boolean" @value = Convert.boolean(@data) when "double" @value = Convert.double(@data) when "dateTime.iso8601" @value = Convert.dateTime(@data) when "base64" @value = Convert.base64(@data) when "value" @value = @data if @value.nil? @values << (@value == :nil ? nil : @value) when "array" @value = @values @values = @val_stack.pop when "struct" @value = Convert.struct(@struct) @name = @names.pop @struct = @structs.pop when "name" @name[0] = @data when "member" @struct[@name[0]] = @values.pop when "param" @params << @values[0] @values = [] when "fault" @fault = Convert.fault(@values[0]) when "methodName" @method_name = @data end @data = nil end def character(data) if @data @data << data else @data = data end end end # module StreamParserMixin class XMLStreamParser < AbstractStreamParser def initialize require "xmlparser" @parser_class = Class.new(::XMLParser) { include StreamParserMixin } end end # class XMLStreamParser class NQXMLStreamParser < AbstractStreamParser def initialize require "nqxml/streamingparser" @parser_class = XMLRPCParser end class XMLRPCParser include StreamParserMixin def parse(str) parser = NQXML::StreamingParser.new(str) parser.each do |ele| case ele when NQXML::Text @data = ele.text #character(ele.text) when NQXML::Tag if ele.isTagEnd endElement(ele.name) else startElement(ele.name, ele.attrs) end end end # do end # method parse end # class XMLRPCParser end # class NQXMLStreamParser class XMLTreeParser < AbstractTreeParser def initialize require "xmltreebuilder" # The new XMLParser library (0.6.2+) uses a slightly different DOM implementation. # The following code removes the differences between both versions. if defined? XML::DOM::Builder return if defined? XML::DOM::Node::DOCUMENT # code below has been already executed klass = XML::DOM::Node klass.const_set(:DOCUMENT, klass::DOCUMENT_NODE) klass.const_set(:TEXT, klass::TEXT_NODE) klass.const_set(:COMMENT, klass::COMMENT_NODE) klass.const_set(:ELEMENT, klass::ELEMENT_NODE) end end private def _nodeType(node) tp = node.nodeType if tp == XML::SimpleTree::Node::TEXT then :TEXT elsif tp == XML::SimpleTree::Node::COMMENT then :COMMENT elsif tp == XML::SimpleTree::Node::ELEMENT then :ELEMENT else :ELSE end end def methodResponse_document(node) assert( node.nodeType == XML::SimpleTree::Node::DOCUMENT ) hasOnlyOneChild(node, "methodResponse") methodResponse(node.firstChild) end def methodCall_document(node) assert( node.nodeType == XML::SimpleTree::Node::DOCUMENT ) hasOnlyOneChild(node, "methodCall") methodCall(node.firstChild) end def createCleanedTree(str) doc = XML::SimpleTreeBuilder.new.parse(str) doc.documentElement.normalize removeWhitespacesAndComments(doc) doc end end # class XMLParser class NQXMLTreeParser < AbstractTreeParser def initialize require "nqxml/treeparser" end private def _nodeType(node) node.nodeType end def methodResponse_document(node) methodResponse(node) end def methodCall_document(node) methodCall(node) end def createCleanedTree(str) doc = ::NQXML::TreeParser.new(str).document.rootNode removeWhitespacesAndComments(doc) doc end end # class NQXMLTreeParser class REXMLStreamParser < AbstractStreamParser def initialize require "rexml/document" @parser_class = StreamListener end class StreamListener include StreamParserMixin alias :tag_start :startElement alias :tag_end :endElement alias :text :character alias :cdata :character def method_missing(*a) # ignore end def parse(str) REXML::Document.parse_stream(str, self) end end end class XMLScanStreamParser < AbstractStreamParser def initialize require "xmlscan/parser" @parser_class = XMLScanParser end class XMLScanParser include StreamParserMixin Entities = { "lt" => "<", "gt" => ">", "amp" => "&", "quot" => '"', "apos" => "'" } def parse(str) parser = XMLScan::XMLParser.new(self) parser.parse(str) end alias :on_stag :startElement alias :on_etag :endElement def on_stag_end(name); end def on_stag_end_empty(name) startElement(name) endElement(name) end def on_chardata(str) character(str) end def on_cdata(str) character(str) end def on_entityref(ent) str = Entities[ent] if str character(str) else raise "unknown entity" end end def on_charref(code) character(code.chr) end def on_charref_hex(code) character(code.chr) end def method_missing(*a) end # TODO: call/implement? # valid_name? # valid_chardata? # valid_char? # parse_error end end class LibXMLStreamParser < AbstractStreamParser def initialize require 'libxml' @parser_class = LibXMLStreamListener end class LibXMLStreamListener include StreamParserMixin def on_start_element_ns(name, attributes, prefix, uri, namespaces) startElement(name) end def on_end_element_ns(name, prefix, uri) endElement(name) end alias :on_characters :character alias :on_cdata_block :character def method_missing(*a) end def parse(str) parser = LibXML::XML::SaxParser.string(str) parser.callbacks = self parser.parse() end end end XMLParser = XMLTreeParser NQXMLParser = NQXMLTreeParser Classes = [XMLStreamParser, XMLTreeParser, NQXMLStreamParser, NQXMLTreeParser, REXMLStreamParser, XMLScanStreamParser, LibXMLStreamParser] # yields an instance of each installed parser def self.each_installed_parser XMLRPC::XMLParser::Classes.each do |klass| begin yield klass.new rescue LoadError end end end end # module XMLParser end # module XMLRPC