aboutsummaryrefslogtreecommitdiffstats
path: root/actionservice/lib/action_service/protocol
diff options
context:
space:
mode:
authorDavid Heinemeier Hansson <david@loudthinking.com>2005-02-18 10:35:25 +0000
committerDavid Heinemeier Hansson <david@loudthinking.com>2005-02-18 10:35:25 +0000
commite7a29380292902eae4799b2658507b3cfffb9cec (patch)
tree99a7cd3c7d720ef73f998c2756be1fef77ff0ee1 /actionservice/lib/action_service/protocol
parente39bf105941133d3d6699c52c18dbd3b9aa0bf5c (diff)
downloadrails-e7a29380292902eae4799b2658507b3cfffb9cec.tar.gz
rails-e7a29380292902eae4799b2658507b3cfffb9cec.tar.bz2
rails-e7a29380292902eae4799b2658507b3cfffb9cec.zip
Added Action Service to the repository
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@658 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
Diffstat (limited to 'actionservice/lib/action_service/protocol')
-rw-r--r--actionservice/lib/action_service/protocol/abstract.rb128
-rw-r--r--actionservice/lib/action_service/protocol/registry.rb55
-rw-r--r--actionservice/lib/action_service/protocol/soap.rb484
-rw-r--r--actionservice/lib/action_service/protocol/xmlrpc.rb187
4 files changed, 854 insertions, 0 deletions
diff --git a/actionservice/lib/action_service/protocol/abstract.rb b/actionservice/lib/action_service/protocol/abstract.rb
new file mode 100644
index 0000000000..ed41c49951
--- /dev/null
+++ b/actionservice/lib/action_service/protocol/abstract.rb
@@ -0,0 +1,128 @@
+module ActionService # :nodoc:
+ module Protocol # :nodoc:
+ CheckedMessage = :checked
+ UncheckedMessage = :unchecked
+
+ class ProtocolError < ActionService::ActionServiceError # :nodoc:
+ end
+
+ class AbstractProtocol # :nodoc:
+ attr :container_class
+
+ def initialize(container_class)
+ @container_class = container_class
+ end
+
+ def unmarshal_request(protocol_request)
+ raise NotImplementedError
+ end
+
+ def marshal_response(protocol_request, return_value)
+ raise NotImplementedError
+ end
+
+ def marshal_exception(exception)
+ raise NotImplementedError
+ end
+
+ def self.create_protocol_request(container_class, action_pack_request)
+ nil
+ end
+
+ def self.create_protocol_client(api, protocol_name, endpoint_uri, options)
+ nil
+ end
+ end
+
+ class AbstractProtocolMessage # :nodoc:
+ attr_accessor :signature
+ attr_accessor :return_signature
+ attr_accessor :type
+ attr :options
+
+ def initialize(options={})
+ @signature = @return_signature = nil
+ @options = options
+ @type = @options[:type] || CheckedMessage
+ end
+
+ def signature=(value)
+ return if value.nil?
+ @signature = []
+ value.each do |klass|
+ if klass.is_a?(Hash)
+ @signature << klass.values.shift
+ else
+ @signature << klass
+ end
+ end
+ @signature
+ end
+
+ def checked?
+ @type == CheckedMessage
+ end
+
+ def check_parameter_types(values, signature)
+ return unless checked? && signature
+ unless signature.length == values.length
+ raise(ProtocolError, "Signature and parameter lengths mismatch")
+ end
+ (1..signature.length).each do |i|
+ check_compatibility(signature[i-1], values[i-1].class)
+ end
+ end
+
+ def check_compatibility(expected_class, received_class)
+ return if \
+ (expected_class == TrueClass or expected_class == FalseClass) and \
+ (received_class == TrueClass or received_class == FalseClass)
+ unless received_class.ancestors.include?(expected_class) or \
+ expected_class.ancestors.include?(received_class)
+ raise(ProtocolError, "value of type #{received_class.name} is not " +
+ "compatible with expected type #{expected_class.name}")
+ end
+ end
+ end
+
+ class ProtocolRequest < AbstractProtocolMessage # :nodoc:
+ attr :protocol
+ attr :raw_body
+
+ attr_accessor :service_name
+ attr_accessor :public_method_name
+ attr_accessor :content_type
+
+ def initialize(protocol, raw_body, service_name, public_method_name, content_type, options={})
+ super(options)
+ @protocol = protocol
+ @raw_body = raw_body
+ @service_name = service_name
+ @public_method_name = public_method_name
+ @content_type = content_type
+ end
+
+ def unmarshal
+ @protocol.unmarshal_request(self)
+ end
+
+ def marshal(return_value)
+ @protocol.marshal_response(self, return_value)
+ end
+ end
+
+ class ProtocolResponse < AbstractProtocolMessage # :nodoc:
+ attr :protocol
+ attr :raw_body
+
+ attr_accessor :content_type
+
+ def initialize(protocol, raw_body, content_type, options={})
+ super(options)
+ @protocol = protocol
+ @raw_body = raw_body
+ @content_type = content_type
+ end
+ end
+ end
+end
diff --git a/actionservice/lib/action_service/protocol/registry.rb b/actionservice/lib/action_service/protocol/registry.rb
new file mode 100644
index 0000000000..e06361f916
--- /dev/null
+++ b/actionservice/lib/action_service/protocol/registry.rb
@@ -0,0 +1,55 @@
+module ActionService # :nodoc:
+ module Protocol # :nodoc:
+ HeaderAndBody = :header_and_body
+ BodyOnly = :body_only
+
+ module Registry # :nodoc:
+ def self.append_features(base) # :nodoc:
+ super
+ base.extend(ClassMethods)
+ base.send(:include, ActionService::Protocol::Registry::InstanceMethods)
+ end
+
+ module ClassMethods # :nodoc:
+ def register_protocol(type, klass) # :nodoc:
+ case type
+ when HeaderAndBody
+ write_inheritable_array("header_and_body_protocols", [klass])
+ when BodyOnly
+ write_inheritable_array("body_only_protocols", [klass])
+ else
+ raise(ProtocolError, "unknown protocol type #{type}")
+ end
+ end
+ end
+
+ module InstanceMethods # :nodoc:
+ private
+ def probe_request_protocol(action_pack_request)
+ (header_and_body_protocols + body_only_protocols).each do |protocol|
+ protocol_request = protocol.create_protocol_request(self.class, action_pack_request)
+ return protocol_request if protocol_request
+ end
+ raise(ProtocolError, "unsupported request message format")
+ end
+
+ def probe_protocol_client(api, protocol_name, endpoint_uri, options)
+ (header_and_body_protocols + body_only_protocols).each do |protocol|
+ protocol_client = protocol.create_protocol_client(api, protocol_name, endpoint_uri, options)
+ return protocol_client if protocol_client
+ end
+ raise(ProtocolError, "unsupported client protocol :#{protocol_name}")
+ end
+
+ def header_and_body_protocols
+ self.class.read_inheritable_attribute("header_and_body_protocols") || []
+ end
+
+ def body_only_protocols
+ self.class.read_inheritable_attribute("body_only_protocols") || []
+ end
+ end
+
+ end
+ end
+end
diff --git a/actionservice/lib/action_service/protocol/soap.rb b/actionservice/lib/action_service/protocol/soap.rb
new file mode 100644
index 0000000000..24cc554b05
--- /dev/null
+++ b/actionservice/lib/action_service/protocol/soap.rb
@@ -0,0 +1,484 @@
+require 'soap/processor'
+require 'soap/mapping'
+require 'soap/rpc/element'
+require 'xsd/datatypes'
+require 'xsd/ns'
+require 'singleton'
+
+module ActionService # :nodoc:
+ module Protocol # :nodoc:
+ module Soap # :nodoc:
+ class ProtocolError < ActionService::ActionServiceError # :nodoc:
+ end
+
+ def self.append_features(base) # :nodoc:
+ super
+ base.register_protocol(HeaderAndBody, SoapProtocol)
+ base.extend(ClassMethods)
+ base.wsdl_service_name('ActionService')
+ end
+
+ module ClassMethods
+ # Specifies the WSDL service name to use when generating WSDL. Highly
+ # recommended that you set this value, or code generators may generate
+ # classes with very generic names.
+ #
+ # === Example
+ # class MyController < ActionController::Base
+ # wsdl_service_name 'MyService'
+ # end
+ def wsdl_service_name(name)
+ write_inheritable_attribute("soap_mapper", SoapMapper.new("urn:#{name}"))
+ end
+
+ def soap_mapper # :nodoc:
+ read_inheritable_attribute("soap_mapper")
+ end
+ end
+
+ class SoapProtocol < AbstractProtocol # :nodoc:
+ attr :mapper
+
+ def initialize(mapper)
+ @mapper = mapper
+ end
+
+ def self.create_protocol_request(container_class, action_pack_request)
+ soap_action = extract_soap_action(action_pack_request)
+ return nil unless soap_action
+ service_name = action_pack_request.parameters['action']
+ public_method_name = soap_action.gsub(/^[\/]+/, '').split(/[\/]+/)[-1]
+ content_type = action_pack_request.env['HTTP_CONTENT_TYPE']
+ content_type ||= 'text/xml'
+ protocol = SoapProtocol.new(container_class.soap_mapper)
+ ProtocolRequest.new(protocol,
+ action_pack_request.raw_post,
+ service_name.to_sym,
+ public_method_name,
+ content_type)
+ end
+
+ def self.create_protocol_client(api, protocol_name, endpoint_uri, options)
+ return nil unless protocol_name.to_s.downcase.to_sym == :soap
+ ActionService::Client::Soap.new(api, endpoint_uri, options)
+ end
+
+ def unmarshal_request(protocol_request)
+ unmarshal = lambda do
+ envelope = SOAP::Processor.unmarshal(protocol_request.raw_body)
+ request = envelope.body.request
+ values = request.collect{|k, v| request[k]}
+ soap_to_ruby_array(values)
+ end
+ signature = protocol_request.signature
+ if signature
+ map_signature_types(signature)
+ values = unmarshal.call
+ signature = signature.map{|x|mapper.lookup(x).ruby_klass}
+ protocol_request.check_parameter_types(values, signature)
+ values
+ else
+ if protocol_request.checked?
+ []
+ else
+ unmarshal.call
+ end
+ end
+ end
+
+ def marshal_response(protocol_request, return_value)
+ marshal = lambda do |signature|
+ mapping = mapper.lookup(signature[0])
+ return_value = fixup_array_types(mapping, return_value)
+ signature = signature.map{|x|mapper.lookup(x).ruby_klass}
+ protocol_request.check_parameter_types([return_value], signature)
+ param_def = [['retval', 'return', mapping.registry_mapping]]
+ [param_def, ruby_to_soap(return_value)]
+ end
+ signature = protocol_request.return_signature
+ param_def = nil
+ if signature
+ param_def, return_value = marshal.call(signature)
+ else
+ if protocol_request.checked?
+ param_def, return_value = nil, nil
+ else
+ param_def, return_value = marshal.call([return_value.class])
+ end
+ end
+ qname = XSD::QName.new(mapper.custom_namespace,
+ protocol_request.public_method_name)
+ response = SOAP::RPC::SOAPMethodResponse.new(qname, param_def)
+ response.retval = return_value unless return_value.nil?
+ ProtocolResponse.new(self, create_response(response), 'text/xml')
+ end
+
+ def marshal_exception(exc)
+ ProtocolResponse.new(self, create_exception_response(exc), 'text/xml')
+ end
+
+ private
+ def self.extract_soap_action(request)
+ return nil unless request.method == :post
+ content_type = request.env['HTTP_CONTENT_TYPE'] || 'text/xml'
+ return nil unless content_type
+ soap_action = request.env['HTTP_SOAPACTION']
+ return nil unless soap_action
+ soap_action.gsub!(/^"/, '')
+ soap_action.gsub!(/"$/, '')
+ soap_action.strip!
+ return nil if soap_action.empty?
+ soap_action
+ end
+
+ def fixup_array_types(mapping, obj)
+ mapping.each_attribute do |name, type, attr_mapping|
+ if attr_mapping.custom_type?
+ attr_obj = obj.send(name)
+ new_obj = fixup_array_types(attr_mapping, attr_obj)
+ obj.send("#{name}=", new_obj) unless new_obj.equal?(attr_obj)
+ end
+ end
+ if mapping.is_a?(SoapArrayMapping)
+ obj = mapping.ruby_klass.new(obj)
+ # man, this is going to be slow for big arrays :(
+ (1..obj.size).each do |i|
+ i -= 1
+ obj[i] = fixup_array_types(mapping.element_mapping, obj[i])
+ end
+ else
+ if !mapping.generated_klass.nil? && mapping.generated_klass.respond_to?(:members)
+ # have to map the publically visible structure of the class
+ new_obj = mapping.generated_klass.new
+ mapping.generated_klass.members.each do |name, klass|
+ new_obj.send("#{name}=", obj.send(name))
+ end
+ obj = new_obj
+ end
+ end
+ obj
+ end
+
+ def map_signature_types(types)
+ types.collect{|type| mapper.map(type)}
+ end
+
+ def create_response(body)
+ header = SOAP::SOAPHeader.new
+ body = SOAP::SOAPBody.new(body)
+ envelope = SOAP::SOAPEnvelope.new(header, body)
+ SOAP::Processor.marshal(envelope)
+ end
+
+ def create_exception_response(exc)
+ detail = SOAP::Mapping::SOAPException.new(exc)
+ body = SOAP::SOAPFault.new(
+ SOAP::SOAPString.new('Server'),
+ SOAP::SOAPString.new(exc.to_s),
+ SOAP::SOAPString.new(self.class.name),
+ SOAP::Mapping.obj2soap(detail))
+ create_response(body)
+ end
+
+ def ruby_to_soap(obj)
+ SOAP::Mapping.obj2soap(obj, mapper.registry)
+ end
+
+ def soap_to_ruby(obj)
+ SOAP::Mapping.soap2obj(obj, mapper.registry)
+ end
+
+ def soap_to_ruby_array(array)
+ array.map{|x| soap_to_ruby(x)}
+ end
+ end
+
+ class SoapMapper # :nodoc:
+ attr :registry
+ attr :custom_namespace
+ attr :custom_types
+
+ def initialize(custom_namespace)
+ @custom_namespace = custom_namespace
+ @registry = SOAP::Mapping::Registry.new
+ @klass2map = {}
+ @custom_types = {}
+ @ar2klass = {}
+ end
+
+ def lookup(klass)
+ lookup_klass = klass.is_a?(Array) ? klass[0] : klass
+ generated_klass = nil
+ unless lookup_klass.respond_to?(:ancestors)
+ raise(ProtocolError, "expected parameter type definition to be a Class")
+ end
+ if lookup_klass.ancestors.include?(ActiveRecord::Base)
+ generated_klass = @ar2klass.has_key?(klass) ? @ar2klass[klass] : nil
+ klass = generated_klass if generated_klass
+ end
+ return @klass2map[klass] if @klass2map.has_key?(klass)
+
+ custom_type = false
+
+ ruby_klass = select_class(lookup_klass)
+ generated_klass = @ar2klass[lookup_klass] if @ar2klass.has_key?(lookup_klass)
+ type_name = ruby_klass.name
+
+ # Array signatures generate a double-mapping and require generation
+ # of an Array subclass to represent the mapping in the SOAP
+ # registry
+ array_klass = nil
+ if klass.is_a?(Array)
+ array_klass = Class.new(Array) do
+ module_eval <<-END
+ def self.name
+ "#{type_name}Array"
+ end
+ END
+ end
+ end
+
+ mapping = @registry.find_mapped_soap_class(ruby_klass) rescue nil
+ unless mapping
+ # Custom structured type, generate a mapping
+ info = { :type => XSD::QName.new(@custom_namespace, type_name) }
+ @registry.add(ruby_klass,
+ SOAP::SOAPStruct,
+ SOAP::Mapping::Registry::TypedStructFactory,
+ info)
+ mapping = ensure_mapped(ruby_klass)
+ custom_type = true
+ end
+
+ array_mapping = nil
+ if array_klass
+ # Typed array always requires a custom type. The info of the array
+ # is the info of its element type (in mapping[2]), falling back
+ # to SOAP base types.
+ info = mapping[2]
+ info ||= {}
+ info[:type] ||= soap_base_type_qname(mapping[0])
+ @registry.add(array_klass,
+ SOAP::SOAPArray,
+ SOAP::Mapping::Registry::TypedArrayFactory,
+ info)
+ array_mapping = ensure_mapped(array_klass)
+ end
+
+ if array_mapping
+ @klass2map[ruby_klass] = SoapMapping.new(self,
+ type_name,
+ ruby_klass,
+ generated_klass,
+ mapping[0],
+ mapping,
+ custom_type)
+ @klass2map[klass] = SoapArrayMapping.new(self,
+ type_name,
+ array_klass,
+ array_mapping[0],
+ array_mapping,
+ @klass2map[ruby_klass])
+ @custom_types[klass] = @klass2map[klass]
+ @custom_types[ruby_klass] = @klass2map[ruby_klass] if custom_type
+ else
+ @klass2map[klass] = SoapMapping.new(self,
+ type_name,
+ ruby_klass,
+ generated_klass,
+ mapping[0],
+ mapping,
+ custom_type)
+ @custom_types[klass] = @klass2map[klass] if custom_type
+ end
+
+ @klass2map[klass]
+ end
+ alias :map :lookup
+
+ def map_container_services(container, &block)
+ dispatching_mode = container.service_dispatching_mode
+ services = nil
+ case dispatching_mode
+ when :direct
+ api = container.class.service_api
+ if container.respond_to?(:controller_class_name)
+ service_name = container.controller_class_name.sub(/Controller$/, '').underscore
+ else
+ service_name = container.class.name.demodulize.underscore
+ end
+ services = { service_name => api }
+ when :delegated
+ services = {}
+ container.class.services.each do |service_name, service_info|
+ begin
+ object = container.service_object(service_name)
+ rescue Exception => e
+ raise(ProtocolError, "failed to retrieve service object for mapping: #{e.message}")
+ end
+ services[service_name] = object.class.service_api
+ end
+ end
+ services.each do |service_name, api|
+ if api.nil?
+ raise(ProtocolError, "no service API set while in :#{dispatching_mode} mode")
+ end
+ map_api(api) do |api_methods|
+ yield service_name, api, api_methods if block_given?
+ end
+ end
+ end
+
+ def map_api(api, &block)
+ lookup_proc = lambda do |klass|
+ mapping = lookup(klass)
+ custom_mapping = nil
+ if mapping.respond_to?(:element_mapping)
+ custom_mapping = mapping.element_mapping
+ else
+ custom_mapping = mapping
+ end
+ if custom_mapping && custom_mapping.custom_type?
+ # What gives? This is required so that structure types
+ # referenced only by structures (and not signatures) still
+ # have a custom type mapping in the registry (needed for WSDL
+ # generation).
+ custom_mapping.each_attribute{}
+ end
+ mapping
+ end
+ api_methods = block.nil?? nil : {}
+ api.api_methods.each do |method_name, method_info|
+ expects = method_info[:expects]
+ expects_signature = nil
+ if expects
+ expects_signature = block ? [] : nil
+ expects.each do |klass|
+ lookup_klass = nil
+ if klass.is_a?(Hash)
+ lookup_klass = lookup_proc.call(klass.values[0])
+ expects_signature << {klass.keys[0]=>lookup_klass} if block
+ else
+ lookup_klass = lookup_proc.call(klass)
+ expects_signature << lookup_klass if block
+ end
+ end
+ end
+ returns = method_info[:returns]
+ returns_signature = returns ? returns.map{|klass| lookup_proc.call(klass)} : nil
+ if block
+ api_methods[method_name] = {
+ :expects => expects_signature,
+ :returns => returns_signature
+ }
+ end
+ end
+ yield api_methods if block
+ end
+
+ private
+ def select_class(klass)
+ return Integer if klass == Fixnum
+ if klass.ancestors.include?(ActiveRecord::Base)
+ new_klass = Class.new(ActionService::Struct)
+ new_klass.class_eval <<-EOS
+ def self.name
+ "#{klass.name}"
+ end
+ EOS
+ klass.columns.each do |column|
+ next if column.klass.nil?
+ new_klass.send(:member, column.name.to_sym, column.klass)
+ end
+ @ar2klass[klass] = new_klass
+ return new_klass
+ end
+ klass
+ end
+
+ def ensure_mapped(klass)
+ mapping = @registry.find_mapped_soap_class(klass) rescue nil
+ raise(ProtocolError, "failed to register #{klass.name}") unless mapping
+ mapping
+ end
+
+ def soap_base_type_qname(base_type)
+ xsd_type = base_type.ancestors.find{|c| c.const_defined? 'Type'}
+ xsd_type ? xsd_type.const_get('Type') : XSD::XSDAnySimpleType::Type
+ end
+ end
+
+ class SoapMapping # :nodoc:
+ attr :ruby_klass
+ attr :generated_klass
+ attr :soap_klass
+ attr :registry_mapping
+
+ def initialize(mapper, type_name, ruby_klass, generated_klass, soap_klass, registry_mapping,
+ custom_type=false)
+ @mapper = mapper
+ @type_name = type_name
+ @ruby_klass = ruby_klass
+ @generated_klass = generated_klass
+ @soap_klass = soap_klass
+ @registry_mapping = registry_mapping
+ @custom_type = custom_type
+ end
+
+ def type_name
+ @type_name
+ end
+
+ def custom_type?
+ @custom_type
+ end
+
+ def qualified_type_name
+ name = type_name
+ if custom_type?
+ "typens:#{name}"
+ else
+ xsd_type_for(@soap_klass)
+ end
+ end
+
+ def each_attribute(&block)
+ if @ruby_klass.respond_to?(:members)
+ @ruby_klass.members.each do |name, klass|
+ name = name.to_s
+ mapping = @mapper.lookup(klass)
+ yield name, mapping.qualified_type_name, mapping
+ end
+ end
+ end
+
+ def is_xsd_type?(klass)
+ klass.ancestors.include?(XSD::NSDBase)
+ end
+
+ def xsd_type_for(klass)
+ ns = XSD::NS.new
+ ns.assign(XSD::Namespace, SOAP::XSDNamespaceTag)
+ xsd_klass = klass.ancestors.find{|c| c.const_defined?('Type')}
+ return ns.name(XSD::AnyTypeName) unless xsd_klass
+ ns.name(xsd_klass.const_get('Type'))
+ end
+ end
+
+ class SoapArrayMapping < SoapMapping # :nodoc:
+ attr :element_mapping
+
+ def initialize(mapper, type_name, ruby_klass, soap_klass, registry_mapping, element_mapping)
+ super(mapper, type_name, ruby_klass, nil, soap_klass, registry_mapping, true)
+ @element_mapping = element_mapping
+ end
+
+ def type_name
+ super + "Array"
+ end
+
+ def each_attribute(&block); end
+ end
+ end
+ end
+end
diff --git a/actionservice/lib/action_service/protocol/xmlrpc.rb b/actionservice/lib/action_service/protocol/xmlrpc.rb
new file mode 100644
index 0000000000..7d29868b59
--- /dev/null
+++ b/actionservice/lib/action_service/protocol/xmlrpc.rb
@@ -0,0 +1,187 @@
+require 'xmlrpc/parser'
+require 'xmlrpc/create'
+require 'xmlrpc/config'
+require 'xmlrpc/utils'
+require 'singleton'
+
+module XMLRPC # :nodoc:
+ class XmlRpcHelper # :nodoc:
+ include Singleton
+ include ParserWriterChooseMixin
+
+ def parse_method_call(message)
+ parser().parseMethodCall(message)
+ end
+
+ def create_method_response(successful, return_value)
+ create().methodResponse(successful, return_value)
+ end
+ end
+end
+
+module ActionService # :nodoc:
+ module Protocol # :nodoc:
+ module XmlRpc # :nodoc:
+ def self.append_features(base) # :nodoc:
+ super
+ base.register_protocol(BodyOnly, XmlRpcProtocol)
+ end
+
+ class XmlRpcProtocol < AbstractProtocol # :nodoc:
+
+ public
+
+ def self.create_protocol_request(container_class, action_pack_request)
+ helper = XMLRPC::XmlRpcHelper.instance
+ service_name = action_pack_request.parameters['action']
+ methodname, params = helper.parse_method_call(action_pack_request.raw_post)
+ methodname.gsub!(/^[^\.]+\./, '') unless methodname =~ /^system\./ # XXX
+ protocol = XmlRpcProtocol.new(container_class)
+ content_type = action_pack_request.env['HTTP_CONTENT_TYPE']
+ content_type ||= 'text/xml'
+ request = ProtocolRequest.new(protocol,
+ action_pack_request.raw_post,
+ service_name.to_sym,
+ methodname,
+ content_type,
+ :xmlrpc_values => params)
+ request
+ rescue
+ nil
+ end
+
+ def self.create_protocol_client(api, protocol_name, endpoint_uri, options)
+ return nil unless protocol_name.to_s.downcase.to_sym == :xmlrpc
+ ActionService::Client::XmlRpc.new(api, endpoint_uri, options)
+ end
+
+ def initialize(container_class)
+ super(container_class)
+ container_class.write_inheritable_hash('default_system_methods', XmlRpcProtocol => method(:xmlrpc_default_system_handler))
+ end
+
+ def unmarshal_request(protocol_request)
+ values = protocol_request.options[:xmlrpc_values]
+ signature = protocol_request.signature
+ if signature
+ values = self.class.transform_incoming_method_params(self.class.transform_array_types(signature), values)
+ protocol_request.check_parameter_types(values, check_array_types(signature))
+ values
+ else
+ protocol_request.checked? ? [] : values
+ end
+ end
+
+ def marshal_response(protocol_request, return_value)
+ helper = XMLRPC::XmlRpcHelper.instance
+ signature = protocol_request.return_signature
+ if signature
+ protocol_request.check_parameter_types([return_value], check_array_types(signature))
+ return_value = self.class.transform_return_value(self.class.transform_array_types(signature), return_value)
+ raw_response = helper.create_method_response(true, return_value)
+ else
+ # XML-RPC doesn't have the concept of a void method, nor does it
+ # support a nil return value, so return true if we would have returned
+ # nil
+ if protocol_request.checked?
+ raw_response = helper.create_method_response(true, true)
+ else
+ return_value = true if return_value.nil?
+ raw_response = helper.create_method_response(true, return_value)
+ end
+ end
+ ProtocolResponse.new(self, raw_response, 'text/xml')
+ end
+
+ def marshal_exception(exception)
+ helper = XMLRPC::XmlRpcHelper.instance
+ exception = XMLRPC::FaultException.new(1, exception.message)
+ raw_response = helper.create_method_response(false, exception)
+ ProtocolResponse.new(self, raw_response, 'text/xml')
+ end
+
+ class << self
+ def transform_incoming_method_params(signature, params)
+ (1..signature.size).each do |i|
+ i -= 1
+ params[i] = xmlrpc_to_ruby(params[i], signature[i])
+ end
+ params
+ end
+
+ def transform_return_value(signature, return_value)
+ ruby_to_xmlrpc(return_value, signature[0])
+ end
+
+ def ruby_to_xmlrpc(param, param_class)
+ if param_class.is_a?(XmlRpcArray)
+ param.map{|p| ruby_to_xmlrpc(p, param_class.klass)}
+ elsif param_class.ancestors.include?(ActiveRecord::Base)
+ param.instance_variable_get('@attributes')
+ elsif param_class.ancestors.include?(ActionService::Struct)
+ struct = {}
+ param_class.members.each do |name, klass|
+ value = param.send(name)
+ next if value.nil?
+ struct[name.to_s] = value
+ end
+ struct
+ else
+ param
+ end
+ end
+
+ def xmlrpc_to_ruby(param, param_class)
+ if param_class.is_a?(XmlRpcArray)
+ param.map{|p| xmlrpc_to_ruby(p, param_class.klass)}
+ elsif param_class.ancestors.include?(ActiveRecord::Base)
+ raise(ProtocolError, "incoming ActiveRecord::Base types are not allowed")
+ elsif param_class.ancestors.include?(ActionService::Struct)
+ unless param.is_a?(Hash)
+ raise(ProtocolError, "expected parameter to be a Hash")
+ end
+ new_param = param_class.new
+ param_class.members.each do |name, klass|
+ new_param.send('%s=' % name.to_s, param[name.to_s])
+ end
+ new_param
+ else
+ param
+ end
+ end
+
+ def transform_array_types(signature)
+ signature.map{|x| x.is_a?(Array) ? XmlRpcArray.new(x[0]) : x}
+ end
+ end
+
+ private
+ def xmlrpc_default_system_handler(name, service_class, *args)
+ case name
+ when 'system.listMethods'
+ methods = []
+ api = service_class.service_api
+ api.api_methods.each do |name, info|
+ methods << api.public_api_method_name(name)
+ end
+ methods.sort
+ else
+ throw :try_default
+ end
+ end
+
+ def check_array_types(signature)
+ signature.map{|x| x.is_a?(Array) ? Array : x}
+ end
+
+ class XmlRpcArray
+ attr :klass
+ def initialize(klass)
+ @klass = klass
+ end
+ end
+ end
+
+ end
+ end
+end