# -*- coding: utf-8 -*- require 'time' require 'handsoap/xml_mason' require 'handsoap/xml_query_front' require 'handsoap/http' require 'handsoap/deferred' module Handsoap def self.http_driver @http_driver || (self.http_driver = :curb) end def self.http_driver=(driver) @http_driver = driver Handsoap::Http.drivers[driver].load! return driver end def self.xml_query_driver @xml_query_driver || (self.xml_query_driver = :nokogiri) end def self.xml_query_driver=(driver) @xml_query_driver = Handsoap::XmlQueryFront.load_driver!(driver) end # Sets the timeout def self.timeout=(timeout) @timeout = timeout end # fetches the timeout # the default timeout is set to 60seconds def self.timeout @timeout || (self.timeout = 60) end # Tell Handsoap to follow redirects def self.follow_redirects! @follow_redirects = true end # Check whether Handsoap should follow redirects def self.follow_redirects? @follow_redirects || false end # Sets the max number of redirects def self.max_redirects=(max_redirects) @max_redirects = max_redirects end # Fetches the max number of redirects # The default is 1 def self.max_redirects @max_redirects || (self.max_redirects = 1) end # Wraps SOAP errors in a standard class. class Fault < StandardError attr_reader :code, :reason, :details def initialize(code, reason, details) @code = code @reason = reason @details = details end def to_s "Handsoap::Fault { :code => '#{@code}', :reason => '#{@reason}' }" end def self.from_xml(node, options = { :namespace => nil }) if not options[:namespace] raise "Missing option :namespace" end ns = { 'env' => options[:namespace] } # tries to find SOAP1.2 fault code fault_code = node.xpath("./env:Code/env:Value", ns).to_s # if no SOAP1.2 fault code was found, try the SOAP1.1 way unless fault_code fault_code = node.xpath('./faultcode', ns).to_s # if fault_code is blank, add the namespace and try again unless fault_code fault_code = node.xpath("//env:faultcode", ns).to_s end end # tries to find SOAP1.2 reason reason = node.xpath("./env:Reason/env:Text[1]", ns).to_s # if no SOAP1.2 faultstring was found, try the SOAP1.1 way unless reason reason = node.xpath('./faultstring', ns).to_s # if reason is blank, add the namespace and try again unless reason reason = node.xpath("//env:faultstring", ns).to_s end end details = node.xpath('./detail/*', ns) self.new(fault_code, reason, details) end end class HttpError < StandardError attr_reader :response def initialize(response) @response = response super() end end class SoapResponse attr_reader :document, :http_response def initialize(document, http_response) @document = document @http_response = http_response end def method_missing(method, *args, &block) if @document.respond_to?(method) @document.__send__ method, *args, &block else super end end end class AsyncDispatch attr_reader :action, :options, :request_block, :response_block def request(action, options = { :soap_action => :auto }, &block) @action = action @options = options @request_block = block end def response(&block) @response_block = block end end class Service @@logger = nil def self.logger=(io) @@logger = io end # Sets the endpoint for the service. # Arguments: # :uri => endpoint uri of the service. Required. # :version => 1 | 2 # :envelope_namespace => Namespace of SOAP-envelope # :request_content_type => Content-Type of HTTP request. # You must supply either :version or both :envelope_namspace and :request_content_type. # :version is simply a shortcut for default values. def self.endpoint(args = {}) @uri = args[:uri] || raise("Missing option :uri") if args[:version] soap_namespace = { 1 => 'http://schemas.xmlsoap.org/soap/envelope/', 2 => 'http://www.w3.org/2003/05/soap-envelope' } raise("Unknown protocol version '#{@protocol_version.inspect}'") if soap_namespace[args[:version]].nil? @envelope_namespace = soap_namespace[args[:version]] @request_content_type = args[:version] == 1 ? "text/xml" : "application/soap+xml" end @envelope_namespace = args[:envelope_namespace] unless args[:envelope_namespace].nil? @request_content_type = args[:request_content_type] unless args[:request_content_type].nil? if @envelope_namespace.nil? || @request_content_type.nil? raise("Missing option :envelope_namespace, :request_content_type or :version") end end def self.envelope_namespace @envelope_namespace end def self.request_content_type @request_content_type end def self.uri @uri end @@instance = {} def self.instance @@instance[self.to_s] ||= self.new end def self.method_missing(method, *args, &block) if instance.respond_to?(method) instance.__send__ method, *args, &block else super end end def envelope_namespace self.class.envelope_namespace end def request_content_type self.class.request_content_type end def uri self.class.uri end def http_driver_instance Handsoap::Http.drivers[Handsoap.http_driver].new end # Creates an XML document and sends it over HTTP. # # +action+ is the QName of the rootnode of the envelope. # # +options+ currently takes one option +:soap_action+, which can be one of: # # :auto sends a SOAPAction http header, deduced from the action name. (This is the default) # # +String+ sends a SOAPAction http header. # # +nil+ sends no SOAPAction http header. def invoke(action, options = { :soap_action => :auto }, &block) # :yields: Handsoap::XmlMason::Element if action if options.kind_of? String options = { :soap_action => options } end if options[:soap_action] == :auto options[:soap_action] = action.gsub(/^.+:/, "") elsif options[:soap_action] == :none options[:soap_action] = nil end doc = make_envelope do |body| body.add action end if block_given? yield doc.find(action) end # ready to dispatch headers = { "Content-Type" => "#{self.request_content_type};charset=UTF-8" } headers["SOAPAction"] = options[:soap_action] unless options[:soap_action].nil? on_before_dispatch request = make_http_request(self.uri, doc.to_s, headers) response = http_driver_instance.send_http_request(request) parse_http_response(response) end end # Async invocation # # Creates an XML document and sends it over HTTP. # # +user_block+ Block from userland def async(user_block, &block) # :yields: Handsoap::AsyncDispatch # Setup userland handlers userland = Handsoap::Deferred.new user_block.call(userland) raise "Missing :callback" unless userland.has_callback? raise "Missing :errback" unless userland.has_errback? # Setup service level handlers dispatcher = Handsoap::AsyncDispatch.new yield dispatcher raise "Missing :request_block" unless dispatcher.request_block raise "Missing :response_block" unless dispatcher.response_block # Done with the external configuration .. let's roll action = dispatcher.action options = dispatcher.options if action #TODO: What if no action ?!? if options.kind_of? String options = { :soap_action => options } end if options[:soap_action] == :auto options[:soap_action] = action.gsub(/^.+:/, "") elsif options[:soap_action] == :none options[:soap_action] = nil end doc = make_envelope do |body| body.add action end dispatcher.request_block.call doc.find(action) # ready to dispatch headers = { "Content-Type" => "#{self.request_content_type};charset=UTF-8" } headers["SOAPAction"] = options[:soap_action] unless options[:soap_action].nil? on_before_dispatch request = make_http_request(self.uri, doc.to_s, headers) driver = self.http_driver_instance if driver.respond_to? :send_http_request_async deferred = driver.send_http_request_async(request) else # Fake async for sync-only drivers deferred = Handsoap::Deferred.new begin deferred.trigger_callback driver.send_http_request(request) rescue deferred.trigger_errback $! end end deferred.callback do |http_response| begin # Parse response response_document = parse_http_response(http_response) # Transform response result = dispatcher.response_block.call(response_document) # Yield to userland code userland.trigger_callback(result) rescue userland.trigger_errback $! end end # Pass driver level errors on deferred.errback do |ex| userland.trigger_errback(ex) end end return nil end # Hook that is called when a new request document is created. # # You can override this to add namespaces and other elements that are common to all requests (Such as authentication). def on_create_document(doc) end # Hook that is called before the message is dispatched. # # You can override this to provide filtering and logging. def on_before_dispatch end # Hook that is called after the http_client is created. # # You can override this to customize the http_client def on_after_create_http_request(http_request) end # Hook that is called when there is a response. # # You can override this to register common namespaces, useful for parsing the document. def on_response_document(doc) end # Hook that is called if there is a HTTP level error. # # Default behaviour is to raise an error. def on_http_error(response) raise HttpError, response end # Hook that is called if the dispatch returns a +Fault+. # # Default behaviour is to raise the Fault, but you can override this to provide logging and more fine-grained handling faults. # # See also: parse_soap_fault def on_fault(fault) raise fault end # Hook that is called if the response does not contain a valid SOAP enevlope. # # Default behaviour is to raise an error # # Note that if your service has operations that are one-way, you shouldn't raise an error here. # This is however a fairly exotic case, so that is why the default behaviour is to raise an error. def on_missing_document(response) raise "The response is not a valid SOAP envelope" end def debug(message = nil) #:nodoc: if @@logger if message @@logger.puts(message) end if block_given? yield @@logger end end end def make_http_request(uri, post_body, headers) request = Handsoap::Http::Request.new(uri, :post) headers.each do |key, value| request.add_header(key, value) end request.body = post_body debug do |logger| logger.puts request.inspect end on_after_create_http_request(request) request end # Start the parsing pipe-line. # There are various stages and hooks for each, so that you can override those in your service classes. def parse_http_response(response) debug do |logger| logger.puts(response.inspect do |body| Handsoap.pretty_format_envelope(body).chomp end) end xml_document = parse_soap_response_document(response.primary_part.body) soap_fault = parse_soap_fault(xml_document) # Is the response a soap-fault? unless soap_fault.nil? return on_fault(soap_fault) end # Does the http-status indicate an error? if response.status >= 400 return on_http_error(response) end # Does the response contain a valid xml-document? if xml_document.nil? return on_missing_document(response) end # Everything seems in order. on_response_document(xml_document) return SoapResponse.new(xml_document, response) end # Creates a standard SOAP envelope and yields the +Body+ element. def make_envelope # :yields: Handsoap::XmlMason::Element doc = XmlMason::Document.new do |doc| doc.alias 'env', self.envelope_namespace doc.add "env:Envelope" do |env| env.add "*:Header" env.add "*:Body" end end self.class.fire_on_create_document doc # deprecated .. use instance method on_create_document(doc) if block_given? yield doc.find("Body") end return doc end # String -> [XmlDocument | nil] def parse_soap_response_document(http_body) begin Handsoap::XmlQueryFront.parse_string(http_body, Handsoap.xml_query_driver) rescue Handsoap::XmlQueryFront::ParseError => ex nil end end # XmlDocument -> [Fault | nil] def parse_soap_fault(document) unless document.nil? node = document.xpath('/env:Envelope/env:Body/descendant-or-self::env:Fault', { 'env' => self.envelope_namespace }).first Fault.from_xml(node, :namespace => self.envelope_namespace) unless node.nil? end end end def self.pretty_format_envelope(xml_string) if /^<.*:Envelope/.match(xml_string) begin doc = Handsoap::XmlQueryFront.parse_string(xml_string, Handsoap.xml_query_driver) rescue return xml_string end return doc.to_xml # return "\n\e[1;33m" + doc.to_s + "\e[0m" end return xml_string end end # Legacy/BC code here. This shouldn't be used in new applications. module Handsoap class Service # Registers a simple method mapping without any arguments and no parsing of response. # # This is deprecated def self.map_method(mapping) if @mapping.nil? @mapping = {} end @mapping.merge! mapping end def self.get_mapping(name) @mapping[name] if @mapping end def method_missing(method, *args, &block) action = self.class.get_mapping(method) if action invoke(action, *args, &block) else super end end # Registers a block to call when a request document is created. # # This is deprecated, in favour of #on_create_document def self.on_create_document(&block) @create_document_callback = block end def self.fire_on_create_document(doc) if @create_document_callback @create_document_callback.call doc end end private # Helper to serialize a node into a ruby string # # *deprecated*. Use Handsoap::XmlQueryFront::XmlElement#to_s def xml_to_str(node, xquery = nil) n = xquery ? node.xpath(xquery, ns).first : node return if n.nil? n.to_s end alias_method :xml_to_s, :xml_to_str # Helper to serialize a node into a ruby integer # # *deprecated*. Use Handsoap::XmlQueryFront::XmlElement#to_i def xml_to_int(node, xquery = nil) n = xquery ? node.xpath(xquery, ns).first : node return if n.nil? n.to_s.to_i end alias_method :xml_to_i, :xml_to_int # Helper to serialize a node into a ruby float # # *deprecated*. Use Handsoap::XmlQueryFront::XmlElement#to_f def xml_to_float(node, xquery = nil) n = xquery ? node.xpath(xquery, ns).first : node return if n.nil? n.to_s.to_f end alias_method :xml_to_f, :xml_to_float # Helper to serialize a node into a ruby boolean # # *deprecated*. Use Handsoap::XmlQueryFront::XmlElement#to_boolean def xml_to_bool(node, xquery = nil) n = xquery ? node.xpath(xquery, ns).first : node return if n.nil? n.to_s == "true" end # Helper to serialize a node into a ruby Time object # # *deprecated*. Use Handsoap::XmlQueryFront::XmlElement#to_date def xml_to_date(node, xquery = nil) n = xquery ? node.xpath(xquery, ns).first : node return if n.nil? Time.iso8601(n.to_s) end end end