diff options
Diffstat (limited to 'actionwebservice/lib/action_web_service')
31 files changed, 1295 insertions, 1326 deletions
diff --git a/actionwebservice/lib/action_web_service/api.rb b/actionwebservice/lib/action_web_service/api.rb index 0c71de5654..ab8b696ab4 100644 --- a/actionwebservice/lib/action_web_service/api.rb +++ b/actionwebservice/lib/action_web_service/api.rb @@ -1,2 +1 @@ -require 'action_web_service/api/abstract' -require 'action_web_service/api/action_controller' +require 'action_web_service/api/base' diff --git a/actionwebservice/lib/action_web_service/api/abstract.rb b/actionwebservice/lib/action_web_service/api/base.rb index 0ce68d10f7..952c6baa0d 100644 --- a/actionwebservice/lib/action_web_service/api/abstract.rb +++ b/actionwebservice/lib/action_web_service/api/base.rb @@ -1,70 +1,5 @@ module ActionWebService # :nodoc: module API # :nodoc: - class APIError < ActionWebService::ActionWebServiceError # :nodoc: - end - - def self.append_features(base) # :nodoc: - super - base.extend(ClassMethods) - end - - module ClassMethods - # Attaches ActionWebService API +definition+ to the calling class. - # - # Action Controllers can have a default associated API, removing the need - # to call this method if you follow the Action Web Service naming conventions. - # - # A controller with a class name of GoogleSearchController will - # implicitly load <tt>app/apis/google_search_api.rb</tt>, and expect the - # API definition class to be named <tt>GoogleSearchAPI</tt> or - # <tt>GoogleSearchApi</tt>. - # - # ==== Service class example - # - # class MyService < ActionWebService::Base - # web_service_api MyAPI - # end - # - # class MyAPI < ActionWebService::API::Base - # ... - # end - # - # ==== Controller class example - # - # class MyController < ActionController::Base - # web_service_api MyAPI - # end - # - # class MyAPI < ActionWebService::API::Base - # ... - # end - def web_service_api(definition=nil) - if definition.nil? - read_inheritable_attribute("web_service_api") - else - if definition.is_a?(Symbol) - raise(APIError, "symbols can only be used for #web_service_api inside of a controller") - end - unless definition.respond_to?(:ancestors) && definition.ancestors.include?(Base) - raise(APIError, "#{definition.to_s} is not a valid API definition") - end - write_inheritable_attribute("web_service_api", definition) - call_web_service_api_callbacks(self, definition) - end - end - - def add_web_service_api_callback(&block) # :nodoc: - write_inheritable_array("web_service_api_callbacks", [block]) - end - - private - def call_web_service_api_callbacks(container_class, definition) - (read_inheritable_attribute("web_service_api_callbacks") || []).each do |block| - block.call(container_class, definition) - end - end - end - # A web service API class specifies the methods that will be available for # invocation for an API. It also contains metadata such as the method type # signature hints. @@ -87,8 +22,6 @@ module ActionWebService # :nodoc: private_class_method :new, :allocate class << self - include ActionWebService::Signature - # API methods have a +name+, which must be the Ruby method name to use when # performing the invocation on the web service object. # @@ -125,11 +58,11 @@ module ActionWebService # :nodoc: expects = options[:expects] returns = options[:returns] end - expects = canonical_signature(expects) if expects - returns = canonical_signature(returns) if returns + expects = canonical_signature(expects) + returns = canonical_signature(returns) if expects expects.each do |param| - klass = signature_parameter_class(param) + klass = WS::BaseTypes.canonical_param_type_class(param) klass = klass[0] if klass.is_a?(Array) if klass.ancestors.include?(ActiveRecord::Base) raise(ActionWebServiceError, "ActiveRecord model classes not allowed in :expects") @@ -186,6 +119,10 @@ module ActionWebService # :nodoc: end end + def canonical_signature(signature) + return nil if signature.nil? + signature.map{|spec| WS::BaseTypes.canonical_param_type_spec(spec)} + end end end end diff --git a/actionwebservice/lib/action_web_service/base.rb b/actionwebservice/lib/action_web_service/base.rb index 42a514716a..16e4d87f41 100644 --- a/actionwebservice/lib/action_web_service/base.rb +++ b/actionwebservice/lib/action_web_service/base.rb @@ -1,6 +1,3 @@ -require 'action_web_service/support/class_inheritable_options' -require 'action_web_service/support/signature' - module ActionWebService # :nodoc: class ActionWebServiceError < StandardError # :nodoc: end diff --git a/actionwebservice/lib/action_web_service/client/base.rb b/actionwebservice/lib/action_web_service/client/base.rb index 431b78c748..9dada7bf98 100644 --- a/actionwebservice/lib/action_web_service/client/base.rb +++ b/actionwebservice/lib/action_web_service/client/base.rb @@ -12,28 +12,17 @@ module ActionWebService # :nodoc: def method_missing(name, *args) # :nodoc: call_name = method_name(name) return super(name, *args) if call_name.nil? - perform_invocation(call_name, args) + self.perform_invocation(call_name, args) end - protected - def perform_invocation(method_name, args) # :nodoc: - raise NotImplementedError, "use a protocol-specific client" - end - private def method_name(name) if @api.has_api_method?(name.to_sym) name.to_s elsif @api.has_public_api_method?(name.to_s) @api.api_method_name(name.to_s).to_s - else - nil end end - - def lookup_class(klass) - klass.is_a?(Hash) ? klass.values[0] : klass - end end end end diff --git a/actionwebservice/lib/action_web_service/client/soap_client.rb b/actionwebservice/lib/action_web_service/client/soap_client.rb index dfabfd86ee..b93f6475d9 100644 --- a/actionwebservice/lib/action_web_service/client/soap_client.rb +++ b/actionwebservice/lib/action_web_service/client/soap_client.rb @@ -28,10 +28,10 @@ module ActionWebService # :nodoc: # option, you must specify it here def initialize(api, endpoint_uri, options={}) super(api, endpoint_uri) - @service_name = options[:service_name] || 'ActionWebService' - @namespace = "urn:#{@service_name}" - @mapper = ActionWebService::Protocol::Soap::SoapMapper.new(@namespace) - @protocol = ActionWebService::Protocol::Soap::SoapProtocol.new(@mapper) + @service_name = options[:service_name] + @namespace = @service_name ? '' : "urn:#{@service_name}" + @marshaler = WS::Marshaling::SoapMarshaler.new + @encoder = WS::Encoding::SoapRpcEncoding.new @soap_action_base = options[:soap_action_base] @soap_action_base ||= URI.parse(endpoint_uri).path @driver = create_soap_rpc_driver(api, endpoint_uri) @@ -48,9 +48,9 @@ module ActionWebService # :nodoc: private def create_soap_rpc_driver(api, endpoint_uri) - @mapper.map_api(api) + register_api(@marshaler, api) driver = SoapDriver.new(endpoint_uri, nil) - driver.mapping_registry = @mapper.registry + driver.mapping_registry = @marshaler.registry api.api_methods.each do |name, info| public_name = api.public_api_method_name(name) qname = XSD::QName.new(@namespace, public_name) @@ -58,25 +58,38 @@ module ActionWebService # :nodoc: expects = info[:expects] returns = info[:returns] param_def = [] - i = 1 + i = 0 if expects - expects.each do |klass| - param_name = klass.is_a?(Hash) ? klass.keys[0] : "param#{i}" - param_klass = lookup_class(klass) - mapping = @mapper.lookup(param_klass) - param_def << ['in', param_name, mapping.registry_mapping] + expects.each do |spec| + param_name = spec.is_a?(Hash) ? spec.keys[0].to_s : "param#{i}" + type_binding = @marshaler.register_type(spec) + param_def << ['in', param_name, type_binding.mapping] i += 1 end end if returns - mapping = @mapper.lookup(lookup_class(returns[0])) - param_def << ['retval', 'return', mapping.registry_mapping] + type_binding = @marshaler.register_type(returns[0]) + param_def << ['retval', 'return', type_binding.mapping] end driver.add_method(qname, action, name.to_s, param_def) end driver end + def register_api(marshaler, api) + type_bindings = [] + api.api_methods.each do |name, info| + expects, returns = info[:expects], info[:returns] + if expects + expects.each{|type| type_bindings << marshaler.register_type(type)} + end + if returns + returns.each{|type| type_bindings << marshaler.register_type(type)} + end + end + type_bindings + end + class SoapDriver < SOAP::RPC::Driver # :nodoc: def add_method(qname, soapaction, name, param_def) @proxy.add_rpc_method(qname, soapaction, name, param_def) diff --git a/actionwebservice/lib/action_web_service/client/xmlrpc_client.rb b/actionwebservice/lib/action_web_service/client/xmlrpc_client.rb index bb52c20453..dc7ad1517f 100644 --- a/actionwebservice/lib/action_web_service/client/xmlrpc_client.rb +++ b/actionwebservice/lib/action_web_service/client/xmlrpc_client.rb @@ -31,6 +31,7 @@ module ActionWebService # :nodoc: @api = api @handler_name = options[:handler_name] @client = XMLRPC::Client.new2(endpoint_uri, options[:proxy], options[:timeout]) + @marshaler = WS::Marshaling::XmlRpcMarshaler.new end protected @@ -43,18 +44,21 @@ module ActionWebService # :nodoc: def transform_outgoing_method_params(method_name, params) info = @api.api_methods[method_name.to_sym] - signature = info[:expects] - signature_length = signature.nil?? 0 : signature.length - if signature_length != params.length - raise(ProtocolError, "API declares #{public_name(method_name)} to accept " + - "#{signature_length} parameters, but #{params.length} parameters " + - "were supplied") + expects = info[:expects] + expects_length = expects.nil?? 0 : expects.length + if expects_length != params.length + raise(ClientError, "API declares #{public_name(method_name)} to accept " + + "#{expects_length} parameters, but #{params.length} parameters " + + "were supplied") end - if signature_length > 0 - signature = Protocol::XmlRpc::XmlRpcProtocol.transform_array_types(signature) - (1..signature.size).each do |i| - i -= 1 - params[i] = Protocol::XmlRpc::XmlRpcProtocol.ruby_to_xmlrpc(params[i], lookup_class(signature[i])) + params = params.dup + if expects_length > 0 + i = 0 + expects.each do |spec| + type_binding = @marshaler.register_type(spec) + info = WS::ParamInfo.create(spec, i, type_binding) + params[i] = @marshaler.marshal(WS::Param.new(params[i], info)) + i += 1 end end params @@ -62,10 +66,11 @@ module ActionWebService # :nodoc: def transform_return_value(method_name, return_value) info = @api.api_methods[method_name.to_sym] - return true unless signature = info[:returns] - param_klass = lookup_class(signature[0]) - signature = Protocol::XmlRpc::XmlRpcProtocol.transform_array_types([param_klass]) - Protocol::XmlRpc::XmlRpcProtocol.xmlrpc_to_ruby(return_value, signature[0]) + return true unless returns = info[:returns] + type_binding = @marshaler.register_type(returns[0]) + info = WS::ParamInfo.create(returns[0], 0, type_binding) + info.name = 'return' + @marshaler.transform_inbound(WS::Param.new(return_value, info)) end def public_name(method_name) diff --git a/actionwebservice/lib/action_web_service/container.rb b/actionwebservice/lib/action_web_service/container.rb index f02717579e..13d9d8ab56 100644 --- a/actionwebservice/lib/action_web_service/container.rb +++ b/actionwebservice/lib/action_web_service/container.rb @@ -1,85 +1,3 @@ -module ActionWebService # :nodoc: - module Container # :nodoc: - class ContainerError < ActionWebService::ActionWebServiceError # :nodoc: - end - - def self.append_features(base) # :nodoc: - super - base.extend(ClassMethods) - base.send(:include, ActionWebService::Container::InstanceMethods) - end - - module ClassMethods - # Declares a web service that will provides access to the API of the given - # +object+. +object+ must be an ActionWebService::Base derivative. - # - # Web service object creation can either be _immediate_, where the object - # instance is given at class definition time, or _deferred_, where - # object instantiation is delayed until request time. - # - # ==== Immediate web service object example - # - # class ApiController < ApplicationController - # web_service_dispatching_mode :delegated - # - # web_service :person, PersonService.new - # end - # - # For deferred instantiation, a block should be given instead of an - # object instance. This block will be executed in controller instance - # context, so it can rely on controller instance variables being present. - # - # ==== Deferred web service object example - # - # class ApiController < ApplicationController - # web_service_dispatching_mode :delegated - # - # web_service(:person) { PersonService.new(@request.env) } - # end - def web_service(name, object=nil, &block) - if (object && block_given?) || (object.nil? && block.nil?) - raise(ContainerError, "either service, or a block must be given") - end - name = name.to_sym - if block_given? - info = { name => { :block => block } } - else - info = { name => { :object => object } } - end - write_inheritable_hash("web_services", info) - call_web_service_definition_callbacks(self, name, info) - end - - # Whether this service contains a service with the given +name+ - def has_web_service?(name) - web_services.has_key?(name.to_sym) - end - - def web_services # :nodoc: - read_inheritable_attribute("web_services") || {} - end - - def add_web_service_definition_callback(&block) # :nodoc: - write_inheritable_array("web_service_definition_callbacks", [block]) - end - - private - def call_web_service_definition_callbacks(container_class, web_service_name, service_info) - (read_inheritable_attribute("web_service_definition_callbacks") || []).each do |block| - block.call(container_class, web_service_name, service_info) - end - end - end - - module InstanceMethods # :nodoc: - def web_service_object(web_service_name) - info = self.class.web_services[web_service_name.to_sym] - unless info - raise(ContainerError, "no such web service '#{web_service_name}'") - end - service = info[:block] - service ? instance_eval(&service) : info[:object] - end - end - end -end +require 'action_web_service/container/direct_container' +require 'action_web_service/container/delegated_container' +require 'action_web_service/container/action_controller_container' diff --git a/actionwebservice/lib/action_web_service/api/action_controller.rb b/actionwebservice/lib/action_web_service/container/action_controller_container.rb index 604cbfe704..4dda93ec11 100644 --- a/actionwebservice/lib/action_web_service/api/action_controller.rb +++ b/actionwebservice/lib/action_web_service/container/action_controller_container.rb @@ -1,5 +1,5 @@ module ActionWebService # :nodoc: - module API # :nodoc: + module Container # :nodoc: module ActionController # :nodoc: def self.append_features(base) # :nodoc: base.class_eval do @@ -36,7 +36,7 @@ module ActionWebService # :nodoc: api_klass = options.delete(:api) || require_web_service_api(name) class_eval do define_method(name) do - probe_protocol_client(api_klass, protocol, endpoint_uri, options) + create_web_service_client(api_klass, protocol, endpoint_uri, options) end protected name end diff --git a/actionwebservice/lib/action_web_service/container/delegated_container.rb b/actionwebservice/lib/action_web_service/container/delegated_container.rb new file mode 100644 index 0000000000..674141aab6 --- /dev/null +++ b/actionwebservice/lib/action_web_service/container/delegated_container.rb @@ -0,0 +1,87 @@ +module ActionWebService # :nodoc: + module Container # :nodoc: + module Delegated # :nodoc: + class ContainerError < ActionWebServiceError # :nodoc: + end + + def self.append_features(base) # :nodoc: + super + base.extend(ClassMethods) + base.send(:include, ActionWebService::Container::Delegated::InstanceMethods) + end + + module ClassMethods + # Declares a web service that will provides access to the API of the given + # +object+. +object+ must be an ActionWebService::Base derivative. + # + # Web service object creation can either be _immediate_, where the object + # instance is given at class definition time, or _deferred_, where + # object instantiation is delayed until request time. + # + # ==== Immediate web service object example + # + # class ApiController < ApplicationController + # web_service_dispatching_mode :delegated + # + # web_service :person, PersonService.new + # end + # + # For deferred instantiation, a block should be given instead of an + # object instance. This block will be executed in controller instance + # context, so it can rely on controller instance variables being present. + # + # ==== Deferred web service object example + # + # class ApiController < ApplicationController + # web_service_dispatching_mode :delegated + # + # web_service(:person) { PersonService.new(@request.env) } + # end + def web_service(name, object=nil, &block) + if (object && block_given?) || (object.nil? && block.nil?) + raise(ContainerError, "either service, or a block must be given") + end + name = name.to_sym + if block_given? + info = { name => { :block => block } } + else + info = { name => { :object => object } } + end + write_inheritable_hash("web_services", info) + call_web_service_definition_callbacks(self, name, info) + end + + # Whether this service contains a service with the given +name+ + def has_web_service?(name) + web_services.has_key?(name.to_sym) + end + + def web_services # :nodoc: + read_inheritable_attribute("web_services") || {} + end + + def add_web_service_definition_callback(&block) # :nodoc: + write_inheritable_array("web_service_definition_callbacks", [block]) + end + + private + def call_web_service_definition_callbacks(container_class, web_service_name, service_info) + (read_inheritable_attribute("web_service_definition_callbacks") || []).each do |block| + block.call(container_class, web_service_name, service_info) + end + end + end + + module InstanceMethods # :nodoc: + def web_service_object(web_service_name) + info = self.class.web_services[web_service_name.to_sym] + unless info + raise(ContainerError, "no such web service '#{web_service_name}'") + end + service = info[:block] + service ? instance_eval(&service) : info[:object] + end + end + end + end +end diff --git a/actionwebservice/lib/action_web_service/container/direct_container.rb b/actionwebservice/lib/action_web_service/container/direct_container.rb new file mode 100644 index 0000000000..b53c542fc8 --- /dev/null +++ b/actionwebservice/lib/action_web_service/container/direct_container.rb @@ -0,0 +1,70 @@ +module ActionWebService # :nodoc: + module Container # :nodoc: + module Direct # :nodoc: + class ContainerError < ActionWebServiceError # :nodoc: + end + + def self.append_features(base) # :nodoc: + super + base.extend(ClassMethods) + end + + module ClassMethods + # Attaches ActionWebService API +definition+ to the calling class. + # + # Action Controllers can have a default associated API, removing the need + # to call this method if you follow the Action Web Service naming conventions. + # + # A controller with a class name of GoogleSearchController will + # implicitly load <tt>app/apis/google_search_api.rb</tt>, and expect the + # API definition class to be named <tt>GoogleSearchAPI</tt> or + # <tt>GoogleSearchApi</tt>. + # + # ==== Service class example + # + # class MyService < ActionWebService::Base + # web_service_api MyAPI + # end + # + # class MyAPI < ActionWebService::API::Base + # ... + # end + # + # ==== Controller class example + # + # class MyController < ActionController::Base + # web_service_api MyAPI + # end + # + # class MyAPI < ActionWebService::API::Base + # ... + # end + def web_service_api(definition=nil) + if definition.nil? + read_inheritable_attribute("web_service_api") + else + if definition.is_a?(Symbol) + raise(ContainerError, "symbols can only be used for #web_service_api inside of a controller") + end + unless definition.respond_to?(:ancestors) && definition.ancestors.include?(ActionWebService::API::Base) + raise(ContainerError, "#{definition.to_s} is not a valid API definition") + end + write_inheritable_attribute("web_service_api", definition) + call_web_service_api_callbacks(self, definition) + end + end + + def add_web_service_api_callback(&block) # :nodoc: + write_inheritable_array("web_service_api_callbacks", [block]) + end + + private + def call_web_service_api_callbacks(container_class, definition) + (read_inheritable_attribute("web_service_api_callbacks") || []).each do |block| + block.call(container_class, definition) + end + end + end + end + end +end diff --git a/actionwebservice/lib/action_web_service/dispatcher/abstract.rb b/actionwebservice/lib/action_web_service/dispatcher/abstract.rb index 2262cd1cdd..b7560afc87 100644 --- a/actionwebservice/lib/action_web_service/dispatcher/abstract.rb +++ b/actionwebservice/lib/action_web_service/dispatcher/abstract.rb @@ -14,136 +14,103 @@ module ActionWebService # :nodoc: module InstanceMethods # :nodoc: private - def dispatch_web_service_request(action_pack_request) - protocol_request = protocol_response = nil - bm = Benchmark.measure do - protocol_request = probe_request_protocol(action_pack_request) - protocol_response = dispatch_protocol_request(protocol_request) - end - [protocol_request, protocol_response, bm.real, nil] - rescue Exception => e - protocol_response = prepare_exception_response(protocol_request, e) - [protocol_request, prepare_exception_response(protocol_request, e), nil, e] - end - - def dispatch_protocol_request(protocol_request) + def invoke_web_service_request(protocol_request) + invocation = web_service_invocation(protocol_request) case web_service_dispatching_mode when :direct - dispatch_direct_request(protocol_request) + web_service_direct_invoke(invocation) when :delegated - dispatch_delegated_request(protocol_request) - else - raise(ContainerError, "unsupported dispatching mode :#{web_service_dispatching_mode}") + web_service_delegated_invoke(invocation) end end - - def dispatch_direct_request(protocol_request) - request = prepare_dispatch_request(protocol_request) - return_value = direct_invoke(request) - protocol_request.marshal(return_value) - end - - def dispatch_delegated_request(protocol_request) - request = prepare_dispatch_request(protocol_request) - return_value = delegated_invoke(request) - protocol_request.marshal(return_value) - end - - def direct_invoke(request) - return nil unless before_direct_invoke(request) - return_value = send(request.method_name) - after_direct_invoke(request) - return_value - end - - def before_direct_invoke(request) - @method_params = request.params - end - - def after_direct_invoke(request) + + def web_service_direct_invoke(invocation) + @method_params = invocation.method_ordered_params + return_value = self.__send__(invocation.api_method_name) + returns = invocation.returns ? invocation.returns[0] : nil + invocation.protocol.marshal_response(invocation.public_method_name, return_value, returns) end - def delegated_invoke(request) + def web_service_delegated_invoke(invocation) cancellation_reason = nil - web_service = request.web_service - return_value = web_service.perform_invocation(request.method_name, request.params) do |x| + return_value = invocation.service.perform_invocation(invocation.api_method_name, invocation.method_ordered_params) do |x| cancellation_reason = x end if cancellation_reason raise(DispatcherError, "request canceled: #{cancellation_reason}") end - return_value + returns = invocation.returns ? invocation.returns[0] : nil + invocation.protocol.marshal_response(invocation.public_method_name, return_value, returns) end - def prepare_dispatch_request(protocol_request) - api = method_name = web_service_name = web_service = params = nil - public_method_name = protocol_request.public_method_name + def web_service_invocation(request) + invocation = Invocation.new + invocation.protocol = request.protocol + invocation.service_name = request.service_name case web_service_dispatching_mode when :direct - api = self.class.web_service_api + invocation.api = self.class.web_service_api + invocation.service = self when :delegated - web_service_name = protocol_request.web_service_name - web_service = web_service_object(web_service_name) - api = web_service.class.web_service_api - end - method_name = api.api_method_name(public_method_name) - signature = nil - if method_name - signature = api.api_methods[method_name] - protocol_request.type = Protocol::CheckedMessage - protocol_request.signature = signature[:expects] - protocol_request.return_signature = signature[:returns] - else - method_name = api.default_api_method - if method_name - protocol_request.type = Protocol::UncheckedMessage - else - raise(DispatcherError, "no such method #{web_service_name}##{public_method_name}") + invocation.service = web_service_object(request.service_name) rescue nil + unless invocation.service + raise(DispatcherError, "failed to instantiate service #{invocation.service_name}") end + invocation.api = invocation.service.class.web_service_api end - params = protocol_request.unmarshal - DispatchRequest.new( - :api => api, - :public_method_name => public_method_name, - :method_name => method_name, - :signature => signature, - :web_service_name => web_service_name, - :web_service => web_service, - :params => params) - end - - def prepare_exception_response(protocol_request, exception) - if protocol_request && exception - case web_service_dispatching_mode - when :direct - if web_service_exception_reporting - return protocol_request.protocol.marshal_exception(exception) - end - when :delegated - web_service = web_service_object(protocol_request.web_service_name) - if web_service && web_service.class.web_service_exception_reporting - return protocol_request.protocol.marshal_exception(exception) + public_method_name = request.method_name + unless invocation.api.has_public_api_method?(public_method_name) + raise(DispatcherError, "no such method '#{public_method_name}' on API #{invocation.api}") + end + invocation.public_method_name = public_method_name + invocation.api_method_name = invocation.api.api_method_name(public_method_name) + info = invocation.api.api_methods[invocation.api_method_name] + invocation.expects = info[:expects] + invocation.returns = info[:returns] + if invocation.expects + i = 0 + invocation.method_ordered_params = request.method_params.map do |param| + if invocation.protocol.is_a?(Protocol::XmlRpc::XmlRpcProtocol) + marshaler = invocation.protocol.marshaler + decoded_param = WS::Encoding::XmlRpcDecodedParam.new(param.info.name, param.value) + marshaled_param = marshaler.typed_unmarshal(decoded_param, invocation.expects[i]) rescue nil + param = marshaled_param ? marshaled_param : param end + i += 1 + param.value + end + i = 0 + params = [] + invocation.expects.each do |spec| + type_binding = invocation.protocol.register_signature_type(spec) + info = WS::ParamInfo.create(spec, i, type_binding) + params << WS::Param.new(invocation.method_ordered_params[i], info) + i += 1 + end + invocation.method_ws_params = params + invocation.method_named_params = {} + invocation.method_ws_params.each do |param| + invocation.method_named_params[param.info.name] = param.value end else - protocol_request.protocol.marshal_exception(RuntimeError.new("missing protocol request or exception")) + invocation.method_ordered_params = [] + invocation.method_named_params = {} end - rescue Exception - nil + invocation end - class DispatchRequest - attr :api - attr :public_method_name - attr :method_name - attr :signature - attr :web_service_name - attr :web_service - attr :params - - def initialize(values={}) - values.each{|k,v| instance_variable_set("@#{k.to_s}", v)} - end + class Invocation + attr_accessor :protocol + attr_accessor :service_name + attr_accessor :api + attr_accessor :public_method_name + attr_accessor :api_method_name + attr_accessor :method_ordered_params + attr_accessor :method_named_params + attr_accessor :method_ws_params + attr_accessor :expects + attr_accessor :returns + attr_accessor :service end end end diff --git a/actionwebservice/lib/action_web_service/dispatcher/action_controller_dispatcher.rb b/actionwebservice/lib/action_web_service/dispatcher/action_controller_dispatcher.rb index 511d00ca44..a35029b40d 100644 --- a/actionwebservice/lib/action_web_service/dispatcher/action_controller_dispatcher.rb +++ b/actionwebservice/lib/action_web_service/dispatcher/action_controller_dispatcher.rb @@ -1,3 +1,6 @@ +require 'benchmark' +require 'builder/xmlmarkup' + module ActionWebService # :nodoc: module Dispatcher # :nodoc: module ActionController # :nodoc: @@ -7,106 +10,121 @@ module ActionWebService # :nodoc: class << self alias_method :inherited_without_action_controller, :inherited end - alias_method :before_direct_invoke_without_action_controller, :before_direct_invoke - alias_method :after_direct_invoke_without_action_controller, :after_direct_invoke + alias_method :web_service_direct_invoke_without_controller, :web_service_direct_invoke end base.add_web_service_api_callback do |klass, api| if klass.web_service_dispatching_mode == :direct - klass.class_eval <<-EOS - def api - controller_dispatch_web_service_request - end - EOS + klass.class_eval 'def api; dispatch_web_service_request; end' end end base.add_web_service_definition_callback do |klass, name, info| if klass.web_service_dispatching_mode == :delegated - klass.class_eval <<-EOS - def #{name} - controller_dispatch_web_service_request - end - EOS + klass.class_eval "def #{name}; dispatch_web_service_request; end" end end base.extend(ClassMethods) - base.send(:include, ActionWebService::Dispatcher::ActionController::Invocation) + base.send(:include, ActionWebService::Dispatcher::ActionController::InstanceMethods) end - module ClassMethods # :nodoc: + module ClassMethods def inherited(child) inherited_without_action_controller(child) - child.send(:include, ActionWebService::Dispatcher::ActionController::WsdlGeneration) + child.send(:include, ActionWebService::Dispatcher::ActionController::WsdlAction) end end - module Invocation # :nodoc: + module InstanceMethods private - def controller_dispatch_web_service_request - request, response, elapsed, exception = dispatch_web_service_request(@request) - if response - begin - log_request(request) - log_error(exception) if exception && logger - log_response(response, elapsed) - response_options = { :type => response.content_type, :disposition => 'inline' } - send_data(response.raw_body, response_options) - rescue Exception => e - log_error(e) unless logger.nil? - render_text("Internal protocol error", "500 Internal Server Error") + def dispatch_web_service_request + request = discover_web_service_request(@request) + if request + log_request(request, @request.raw_post) + response = nil + exception = nil + bm = Benchmark.measure do + begin + response = invoke_web_service_request(request) + rescue Exception => e + exception = e + end + end + if exception + log_error(exception) unless logger.nil? + send_web_service_error_response(request, exception) + else + send_web_service_response(response, bm.real) end else - logger.error("No response available") unless logger.nil? - render_text("Internal protocol error", "500 Internal Server Error") + exception = DispatcherError.new("Malformed SOAP or XML-RPC protocol message") + send_web_service_error_response(request, exception) end + rescue Exception => e + log_error(e) unless logger.nil? + send_web_service_error_response(request, e) end - def before_direct_invoke(request) - before_direct_invoke_without_action_controller(request) - @params ||= {} - signature = request.signature - if signature && (expects = request.signature[:expects]) - (0..(@method_params.size-1)).each do |i| - if expects[i].is_a?(Hash) - @params[expects[i].keys[0].to_s] = @method_params[i] - else - @params['param%d' % i] = @method_params[i] - end + def send_web_service_response(response, elapsed=nil) + log_response(response, elapsed) + options = { :type => response.content_type, :disposition => 'inline' } + send_data(response.body, options) + end + + def send_web_service_error_response(request, exception) + if request + unless self.class.web_service_exception_reporting + exception = DispatcherError.new("Internal server error (exception raised)") end + response = request.protocol.marshal_response(request.method_name, exception, exception.class) + send_web_service_response(response) + else + if self.class.web_service_exception_reporting + message = exception.message + else + message = "Exception raised" + end + render_text("Internal protocol error: #{message}", "500 #{message}") end - @params['action'] = request.method_name.to_s - @session ||= {} - @assigns ||= {} - return nil if before_action == false - true end - def after_direct_invoke(request) - after_direct_invoke_without_action_controller(request) + def web_service_direct_invoke(invocation) + @params ||= {} + invocation.method_named_params.each do |name, value| + @params[name] = value + end + @session ||= {} + @assigns ||= {} + @params['action'] = invocation.api_method_name.to_s + if before_action == false + raise(DispatcherError, "Method filtered") + end + return_value = web_service_direct_invoke_without_controller(invocation) after_action + return_value end - def log_request(request) - unless logger.nil? || request.nil? - logger.debug("\nWeb Service Request:") - indented = request.raw_body.split(/\n/).map{|x| " #{x}"}.join("\n") - logger.debug(indented) + def log_request(request, body) + unless logger.nil? + name = request.method_name + params = request.method_params.map{|x| x.value.inspect} + service = request.service_name + logger.debug("\nWeb Service Request: #{name}(#{params}) #{service}") + logger.debug(indent(body)) end end - def log_response(response, elapsed) - unless logger.nil? || response.nil? - logger.debug("\nWeb Service Response" + (elapsed ? " (%f):" % elapsed : ":")) - indented = response.raw_body.split(/\n/).map{|x| " #{x}"}.join("\n") - logger.debug(indented) + def log_response(response, elapsed=nil) + unless logger.nil? + logger.debug("\nWeb Service Response (%f):" + (elapsed ? " (%f):" % elapsed : ":")) + logger.debug(indent(response.body)) end end - unless method_defined?(:logger) - def logger; @logger; end + def indent(body) + body.split(/\n/).map{|x| " #{x}"}.join("\n") end end - module WsdlGeneration # :nodoc: + module WsdlAction XsdNs = 'http://www.w3.org/2001/XMLSchema' WsdlNs = 'http://schemas.xmlsoap.org/wsdl/' SoapNs = 'http://schemas.xmlsoap.org/wsdl/soap/' @@ -117,40 +135,53 @@ module ActionWebService # :nodoc: case @request.method when :get begin - host_name = @request.env['HTTP_HOST'] || @request.env['SERVER_NAME'] - uri = "http://#{host_name}/#{controller_name}/" - soap_action_base = "/#{controller_name}" - xml = to_wsdl(self, uri, soap_action_base) - send_data(xml, :type => 'text/xml', :disposition => 'inline') + options = { :type => 'text/xml', :disposition => 'inline' } + send_data(to_wsdl, options) rescue Exception => e - log_error e unless logger.nil? - render_text('', "500 #{e.message}") + log_error(e) unless logger.nil? end when :post - render_text('', "500 POST not supported") + render_text('POST not supported', '500 POST not supported') end end private - def to_wsdl(container, uri, soap_action_base) - wsdl = "" - - web_service_dispatching_mode = container.web_service_dispatching_mode - mapper = container.class.soap_mapper - namespace = mapper.custom_namespace - wsdl_service_name = namespace.split(/:/)[1] - - services = {} - mapper.map_container_services(container) do |name, api, api_methods| - services[name] = [api, api_methods] + def base_uri + host = @request ? (@request.env['HTTP_HOST'] || @request.env['SERVER_NAME']) : 'localhost' + 'http://%s/%s/' % [host, controller_name] + end + + def to_wsdl + xml = '' + dispatching_mode = web_service_dispatching_mode + global_service_name = wsdl_service_name + namespace = "urn:#{global_service_name}" + soap_action_base = "/#{controller_name}" + + marshaler = WS::Marshaling::SoapMarshaler.new(namespace) + apis = {} + case dispatching_mode + when :direct + api = self.class.web_service_api + web_service_name = controller_class_name.sub(/Controller$/, '').underscore + apis[web_service_name] = [api, register_api(marshaler, api)] + when :delegated + self.class.web_services.each do |web_service_name, info| + service = web_service_object(web_service_name) + api = service.class.web_service_api + apis[web_service_name] = [api, register_api(marshaler, api)] + end end - custom_types = mapper.custom_types - - - xm = Builder::XmlMarkup.new(:target => wsdl, :indent => 2) + custom_types = [] + apis.values.each do |api, bindings| + bindings.each do |b| + custom_types << b if b.is_custom_type? + end + end + + xm = Builder::XmlMarkup.new(:target => xml, :indent => 2) xm.instruct! - - xm.definitions('name' => wsdl_service_name, + xm.definitions('name' => wsdl_service_name, 'targetNamespace' => namespace, 'xmlns:typens' => namespace, 'xmlns:xsd' => XsdNs, @@ -158,95 +189,95 @@ module ActionWebService # :nodoc: 'xmlns:soapenc' => SoapEncodingNs, 'xmlns:wsdl' => WsdlNs, 'xmlns' => WsdlNs) do - - # Custom type XSD generation + # Generate XSD if custom_types.size > 0 xm.types do xm.xsd(:schema, 'xmlns' => XsdNs, 'targetNamespace' => namespace) do - custom_types.each do |klass, mapping| + custom_types.each do |binding| case - when mapping.is_a?(ActionWebService::Protocol::Soap::SoapArrayMapping) - xm.xsd(:complexType, 'name' => mapping.type_name) do + when binding.is_typed_array? + xm.xsd(:complexType, 'name' => binding.type_name) do xm.xsd(:complexContent) do xm.xsd(:restriction, 'base' => 'soapenc:Array') do xm.xsd(:attribute, 'ref' => 'soapenc:arrayType', - 'wsdl:arrayType' => mapping.element_mapping.qualified_type_name + '[]') + 'wsdl:arrayType' => binding.element_binding.qualified_type_name + '[]') end end end - when mapping.is_a?(ActionWebService::Protocol::Soap::SoapMapping) - xm.xsd(:complexType, 'name' => mapping.type_name) do + when binding.is_typed_struct? + xm.xsd(:complexType, 'name' => binding.type_name) do xm.xsd(:all) do - mapping.each_attribute do |name, type_name| + binding.each_member do |name, type_name| xm.xsd(:element, 'name' => name, 'type' => type_name) end end end - else - raise(WsdlError, "unsupported mapping type #{mapping.class.name}") end end end end end - - services.each do |service_name, service_values| - service_api, api_methods = service_values - # Parameter list message definitions - api_methods.each do |method_name, method_signature| + + # APIs + apis.each do |api_name, values| + api = values[0] + api.api_methods.each do |name, info| gen = lambda do |msg_name, direction| xm.message('name' => msg_name) do sym = nil if direction == :out - if method_signature[:returns] - xm.part('name' => 'return', 'type' => method_signature[:returns][0].qualified_type_name) + returns = info[:returns] + if returns + binding = marshaler.register_type(returns[0]) + xm.part('name' => 'return', 'type' => binding.qualified_type_name) end else - mapping_list = method_signature[:expects] + expects = info[:expects] i = 1 - mapping_list.each do |mapping| - if mapping.is_a?(Hash) - param_name = mapping.keys.shift - mapping = mapping.values.shift + expects.each do |type| + if type.is_a?(Hash) + param_name = type.keys.shift + type = type.values.shift else param_name = "param#{i}" end - xm.part('name' => param_name, 'type' => mapping.qualified_type_name) + binding = marshaler.register_type(type) + xm.part('name' => param_name, 'type' => binding.qualified_type_name) i += 1 - end if mapping_list + end if expects end end end - public_name = service_api.public_api_method_name(method_name) + public_name = api.public_api_method_name(name) gen.call(public_name, :in) gen.call("#{public_name}Response", :out) end - - # Declare the port - port_name = port_name_for(wsdl_service_name, service_name) + + # Port + port_name = port_name_for(global_service_name, api_name) xm.portType('name' => port_name) do - api_methods.each do |method_name, method_signature| - public_name = service_api.public_api_method_name(method_name) + api.api_methods.each do |name, info| + public_name = api.public_api_method_name(name) xm.operation('name' => public_name) do xm.input('message' => "typens:#{public_name}") xm.output('message' => "typens:#{public_name}Response") end end end - - # Bind the port to SOAP - binding_name = binding_name_for(wsdl_service_name, service_name) + + # Bind it + binding_name = binding_name_for(global_service_name, api_name) xm.binding('name' => binding_name, 'type' => "typens:#{port_name}") do xm.soap(:binding, 'style' => 'rpc', 'transport' => SoapHttpTransport) - api_methods.each do |method_name, method_signature| - public_name = service_api.public_api_method_name(method_name) + api.api_methods.each do |name, info| + public_name = api.public_api_method_name(name) xm.operation('name' => public_name) do case web_service_dispatching_mode when :direct soap_action = soap_action_base + "/api/" + public_name when :delegated soap_action = soap_action_base \ - + "/" + service_name.to_s \ + + "/" + api_name.to_s \ + "/" + public_name end xm.soap(:operation, 'soapAction' => soap_action) @@ -266,32 +297,46 @@ module ActionWebService # :nodoc: end end end - - # Define the service - xm.service('name' => "#{wsdl_service_name}Service") do - services.each do |service_name, service_values| - port_name = port_name_for(wsdl_service_name, service_name) - binding_name = binding_name_for(wsdl_service_name, service_name) + + # Define it + xm.service('name' => "#{global_service_name}Service") do + apis.each do |api_name, values| + port_name = port_name_for(global_service_name, api_name) + binding_name = binding_name_for(global_service_name, api_name) case web_service_dispatching_mode when :direct binding_target = 'api' when :delegated - binding_target = service_name.to_s + binding_target = api_name.to_s end xm.port('name' => port_name, 'binding' => "typens:#{binding_name}") do - xm.soap(:address, 'location' => "#{uri}#{binding_target}") + xm.soap(:address, 'location' => "#{base_uri}#{binding_target}") end end end end end - def port_name_for(wsdl_service_name, service_name) - "#{wsdl_service_name}#{service_name.to_s.camelize}Port" + def port_name_for(global_service, service) + "#{global_service}#{service.to_s.camelize}Port" + end + + def binding_name_for(global_service, service) + "#{global_service}#{service.to_s.camelize}Binding" end - def binding_name_for(wsdl_service_name, service_name) - "#{wsdl_service_name}#{service_name.to_s.camelize}Binding" + def register_api(marshaler, api) + type_bindings = [] + api.api_methods.each do |name, info| + expects, returns = info[:expects], info[:returns] + if expects + expects.each{|type| type_bindings << marshaler.register_type(type)} + end + if returns + returns.each{|type| type_bindings << marshaler.register_type(type)} + end + end + type_bindings end end end diff --git a/actionwebservice/lib/action_web_service/protocol.rb b/actionwebservice/lib/action_web_service/protocol.rb index b15e850676..053e9cb4be 100644 --- a/actionwebservice/lib/action_web_service/protocol.rb +++ b/actionwebservice/lib/action_web_service/protocol.rb @@ -1,4 +1,4 @@ require 'action_web_service/protocol/abstract' -require 'action_web_service/protocol/registry' +require 'action_web_service/protocol/discovery' require 'action_web_service/protocol/soap_protocol' require 'action_web_service/protocol/xmlrpc_protocol' diff --git a/actionwebservice/lib/action_web_service/protocol/abstract.rb b/actionwebservice/lib/action_web_service/protocol/abstract.rb index 9199dfe33f..f628fc4aee 100644 --- a/actionwebservice/lib/action_web_service/protocol/abstract.rb +++ b/actionwebservice/lib/action_web_service/protocol/abstract.rb @@ -1,126 +1,28 @@ module ActionWebService # :nodoc: module Protocol # :nodoc: - CheckedMessage = :checked - UncheckedMessage = :unchecked - - class ProtocolError < ActionWebService::ActionWebServiceError # :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 + class ProtocolError < ActionWebService::ActionWebServiceError end - class ProtocolRequest < AbstractProtocolMessage # :nodoc: + class Request attr :protocol - attr :raw_body + attr :method_name + attr :method_params + attr :service_name - attr_accessor :web_service_name - attr_accessor :public_method_name - attr_accessor :content_type - - def initialize(protocol, raw_body, web_service_name, public_method_name, content_type, options={}) - super(options) + def initialize(protocol, method_name, method_params, service_name) @protocol = protocol - @raw_body = raw_body - @web_service_name = web_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) + @method_name = method_name + @method_params = method_params + @service_name = service_name end end - class ProtocolResponse < AbstractProtocolMessage # :nodoc: - attr :protocol - attr :raw_body - - attr_accessor :content_type + class Response + attr :body + attr :content_type - def initialize(protocol, raw_body, content_type, options={}) - super(options) - @protocol = protocol - @raw_body = raw_body + def initialize(body, content_type) + @body = body @content_type = content_type end end diff --git a/actionwebservice/lib/action_web_service/protocol/discovery.rb b/actionwebservice/lib/action_web_service/protocol/discovery.rb new file mode 100644 index 0000000000..ab51958ed9 --- /dev/null +++ b/actionwebservice/lib/action_web_service/protocol/discovery.rb @@ -0,0 +1,37 @@ +module ActionWebService + module Protocol + module Discovery + def self.included(base) + base.extend(ClassMethods) + base.send(:include, ActionWebService::Protocol::Discovery::InstanceMethods) + end + + module ClassMethods + def register_protocol(klass) + write_inheritable_array("web_service_protocols", [klass]) + end + end + + module InstanceMethods + private + def discover_web_service_request(ap_request) + (self.class.read_inheritable_attribute("web_service_protocols") || []).each do |protocol| + protocol = protocol.new + request = protocol.unmarshal_request(ap_request) + return request unless request.nil? + end + nil + end + + def create_web_service_client(api, protocol_name, endpoint_uri, options) + (self.class.read_inheritable_attribute("web_service_protocols") || []).each do |protocol| + protocol = protocol.new + client = protocol.protocol_client(api, protocol_name, endpoint_uri, options) + return client unless client.nil? + end + nil + end + end + end + end +end diff --git a/actionwebservice/lib/action_web_service/protocol/registry.rb b/actionwebservice/lib/action_web_service/protocol/registry.rb deleted file mode 100644 index 0173673556..0000000000 --- a/actionwebservice/lib/action_web_service/protocol/registry.rb +++ /dev/null @@ -1,55 +0,0 @@ -module ActionWebService # :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, ActionWebService::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/actionwebservice/lib/action_web_service/protocol/soap_protocol.rb b/actionwebservice/lib/action_web_service/protocol/soap_protocol.rb index 3c527fea93..f2e761f431 100644 --- a/actionwebservice/lib/action_web_service/protocol/soap_protocol.rb +++ b/actionwebservice/lib/action_web_service/protocol/soap_protocol.rb @@ -1,127 +1,49 @@ -require 'soap/processor' -require 'soap/mapping' -require 'soap/rpc/element' -require 'xsd/datatypes' -require 'xsd/ns' -require 'singleton' - -module ActionWebService # :nodoc: - module Protocol # :nodoc: - module Soap # :nodoc: - class ProtocolError < ActionWebService::ActionWebServiceError # :nodoc: - end - - def self.append_features(base) # :nodoc: - super - base.register_protocol(HeaderAndBody, SoapProtocol) - base.extend(ClassMethods) - base.wsdl_service_name('ActionWebService') - 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 +module ActionWebService + module Protocol + module Soap + def self.included(base) + base.register_protocol(SoapProtocol) + base.class_inheritable_option(:wsdl_service_name) 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 - ActionWebService::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 + + class SoapProtocol + def initialize + @encoder = WS::Encoding::SoapRpcEncoding.new + @marshaler = WS::Marshaling::SoapMarshaler.new + end + + def unmarshal_request(ap_request) + return nil unless has_valid_soap_action?(ap_request) + method_name, params = @encoder.decode_rpc_call(ap_request.raw_post) + params = params.map{|x| @marshaler.unmarshal(x)} + service_name = ap_request.parameters['action'] + Request.new(self, method_name, params, service_name) + end + + def marshal_response(method_name, return_value, signature_type) + if !return_value.nil? && signature_type + type_binding = @marshaler.register_type(signature_type) + info = WS::ParamInfo.create(signature_type, 0, type_binding) + return_value = @marshaler.marshal(WS::Param.new(return_value, info)) else - if protocol_request.checked? - [] - else - unmarshal.call - end + return_value = nil end + body = @encoder.encode_rpc_response(method_name, return_value) + Response.new(body, 'text/xml') 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') + def register_signature_type(spec) + @marshaler.register_type(spec) end - def marshal_exception(exc) - ProtocolResponse.new(self, create_exception_response(exc), 'text/xml') + def protocol_client(api, protocol_name, endpoint_uri, options) + return nil unless protocol_name == :soap + ActionWebService::Client::Soap.new(api, endpoint_uri, options) end private - def self.extract_soap_action(request) + def has_valid_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!(/^"/, '') @@ -130,355 +52,7 @@ module ActionWebService # :nodoc: 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.web_service_dispatching_mode - web_services = nil - case dispatching_mode - when :direct - api = container.class.web_service_api - if container.respond_to?(:controller_class_name) - web_service_name = container.controller_class_name.sub(/Controller$/, '').underscore - else - web_service_name = container.class.name.demodulize.underscore - end - web_services = { web_service_name => api } - when :delegated - web_services = {} - container.class.web_services.each do |web_service_name, web_service_info| - begin - object = container.web_service_object(web_service_name) - rescue Exception => e - raise(ProtocolError, "failed to retrieve web service object for web service '#{web_service_name}': #{e.message}") - end - web_services[web_service_name] = object.class.web_service_api - end - end - web_services.each do |web_service_name, api| - if api.nil? - raise(ProtocolError, "no web service API set while in :#{dispatching_mode} mode") - end - map_api(api) do |api_methods| - yield web_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(ActionWebService::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/actionwebservice/lib/action_web_service/protocol/xmlrpc_protocol.rb b/actionwebservice/lib/action_web_service/protocol/xmlrpc_protocol.rb index 1addccba56..1533a2bdb9 100644 --- a/actionwebservice/lib/action_web_service/protocol/xmlrpc_protocol.rb +++ b/actionwebservice/lib/action_web_service/protocol/xmlrpc_protocol.rb @@ -1,167 +1,47 @@ -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 ActionWebService # :nodoc: - module Protocol # :nodoc: - module XmlRpc # :nodoc: - def self.append_features(base) # :nodoc: - super - base.register_protocol(BodyOnly, XmlRpcProtocol) +module ActionWebService + module Protocol + module XmlRpc + def self.included(base) + base.register_protocol(XmlRpcProtocol) end + + class XmlRpcProtocol + attr :marshaler - class XmlRpcProtocol < AbstractProtocol # :nodoc: - 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 - ActionWebService::Client::XmlRpc.new(api, endpoint_uri, options) + def initialize + @encoder = WS::Encoding::XmlRpcEncoding.new + @marshaler = WS::Marshaling::XmlRpcMarshaler.new end - def initialize(container_class) - super(container_class) + def unmarshal_request(ap_request) + method_name, params = @encoder.decode_rpc_call(ap_request.raw_post) + params = params.map{|x| @marshaler.unmarshal(x)} + service_name = ap_request.parameters['action'] + Request.new(self, method_name, params, service_name) + rescue + nil 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) + def marshal_response(method_name, return_value, signature_type) + if !return_value.nil? && signature_type + type_binding = @marshaler.register_type(signature_type) + info = WS::ParamInfo.create(signature_type, 0, type_binding) + return_value = @marshaler.marshal(WS::Param.new(return_value, info)) 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 + return_value = nil end - ProtocolResponse.new(self, raw_response, 'text/xml') + body = @encoder.encode_rpc_response(method_name, return_value) + Response.new(body, '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?(ActionWebService::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?(ActionWebService::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 register_signature_type(spec) + nil + end - def transform_array_types(signature) - signature.map{|x| x.is_a?(Array) ? XmlRpcArray.new(x[0]) : x} - end + def protocol_client(api, protocol_name, endpoint_uri, options) + return nil unless protocol_name == :xmlrpc + ActionWebService::Client::XmlRpc.new(api, endpoint_uri, options) end - - private - 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 diff --git a/actionwebservice/lib/action_web_service/struct.rb b/actionwebservice/lib/action_web_service/struct.rb index 5420f4cf49..77f4fbf4aa 100644 --- a/actionwebservice/lib/action_web_service/struct.rb +++ b/actionwebservice/lib/action_web_service/struct.rb @@ -35,12 +35,10 @@ module ActionWebService end class << self - include ActionWebService::Signature - # Creates a structure member with the specified +name+ and +type+. Generates # accessor methods for reading and writing the member value. def member(name, type) - write_inheritable_hash("struct_members", name => signature_parameter_class(type)) + write_inheritable_hash("struct_members", name => WS::BaseTypes.canonical_param_type_class(type)) class_eval <<-END def #{name}; @#{name}; end def #{name}=(value); @#{name} = value; end diff --git a/actionwebservice/lib/action_web_service/support/signature.rb b/actionwebservice/lib/action_web_service/support/signature.rb deleted file mode 100644 index 00c62a2232..0000000000 --- a/actionwebservice/lib/action_web_service/support/signature.rb +++ /dev/null @@ -1,100 +0,0 @@ -module ActionWebService # :nodoc: - # Action Web Service parameter specifiers may contain symbols or strings - # instead of Class objects, for a limited set of base types. - # - # This provides an unambiguous way to specify that a given parameter - # contains an integer or boolean value, for example. - # - # The allowed set of symbol/string aliases: - # - # [<tt>:int</tt>] any integer value - # [<tt>:float</tt>] any floating point value - # [<tt>:string</tt>] any string value - # [<tt>:bool</tt>] any boolean value - # [<tt>:time</tt>] any value containing both date and time - # [<tt>:date</tt>] any value containing only a date - module Signature - class SignatureError < StandardError # :nodoc: - end - - private - def canonical_signature(params) - return nil if params.nil? - params.map do |param| - klass = signature_parameter_class(param) - if param.is_a?(Hash) - param[param.keys[0]] = klass - param - else - klass - end - end - end - - def signature_parameter_class(param) - param = param.is_a?(Hash) ? param.values[0] : param - is_array = param.is_a?(Array) - param = is_array ? param[0] : param - param = param.is_a?(String) ? param.to_sym : param - param = param.is_a?(Symbol) ? signature_ruby_class(param) : param - is_array ? [param] : param - end - - - def canonical_signature_base_type(base_type) - base_type = base_type.to_sym - case base_type - when :int, :integer, :fixnum, :bignum - :int - when :string, :base64 - :string - when :bool, :boolean - :bool - when :float, :double - :float - when :time, :datetime, :timestamp - :time - when :date - :date - else - raise(SignatureError, ":#{base_type} is not an ActionWebService base type") - end - end - - def signature_ruby_class(base_type) - case canonical_signature_base_type(base_type) - when :int - Integer - when :string - String - when :bool - TrueClass - when :float - Float - when :time - Time - when :date - Date - end - end - - def signature_base_type(ruby_class) - case ruby_class - when Bignum, Integer, Fixnum - :int - when String - :string - when TrueClass, FalseClass - :bool - when Float, Numeric, Precision - :float - when Time, DateTime - :time - when Date - :date - else - raise(SignatureError, "#{ruby_class.name} is not an ActionWebService base type") - end - end - end -end diff --git a/actionwebservice/lib/action_web_service/vendor/ws.rb b/actionwebservice/lib/action_web_service/vendor/ws.rb new file mode 100644 index 0000000000..18a32a555e --- /dev/null +++ b/actionwebservice/lib/action_web_service/vendor/ws.rb @@ -0,0 +1,4 @@ +require 'ws/common' +require 'ws/types' +require 'ws/marshaling' +require 'ws/encoding' diff --git a/actionwebservice/lib/action_web_service/vendor/ws/common.rb b/actionwebservice/lib/action_web_service/vendor/ws/common.rb new file mode 100644 index 0000000000..4266a7141d --- /dev/null +++ b/actionwebservice/lib/action_web_service/vendor/ws/common.rb @@ -0,0 +1,8 @@ +module WS + class WSError < StandardError + end + + def self.derived_from?(ancestor, child) + child.ancestors.include?(ancestor) + end +end diff --git a/actionwebservice/lib/action_web_service/vendor/ws/encoding.rb b/actionwebservice/lib/action_web_service/vendor/ws/encoding.rb new file mode 100644 index 0000000000..790317639b --- /dev/null +++ b/actionwebservice/lib/action_web_service/vendor/ws/encoding.rb @@ -0,0 +1,3 @@ +require 'ws/encoding/abstract' +require 'ws/encoding/soap_rpc_encoding' +require 'ws/encoding/xmlrpc_encoding' diff --git a/actionwebservice/lib/action_web_service/vendor/ws/encoding/abstract.rb b/actionwebservice/lib/action_web_service/vendor/ws/encoding/abstract.rb new file mode 100644 index 0000000000..257c7d0993 --- /dev/null +++ b/actionwebservice/lib/action_web_service/vendor/ws/encoding/abstract.rb @@ -0,0 +1,26 @@ +module WS + module Encoding + # Encoders operate on _foreign_ objects. That is, Ruby object + # instances that are the _marshaling format specific_ representation + # of objects. In other words, objects that have not yet been marshaled, but + # are in protocol-specific form (such as an AST or DOM element), and not + # native Ruby form. + class AbstractEncoding + def encode_rpc_call(method_name, params) + raise NotImplementedError + end + + def decode_rpc_call(obj) + raise NotImplementedError + end + + def encode_rpc_response(method_name, return_value) + raise NotImplementedError + end + + def decode_rpc_response(obj) + raise NotImplementedError + end + end + end +end diff --git a/actionwebservice/lib/action_web_service/vendor/ws/encoding/soap_rpc_encoding.rb b/actionwebservice/lib/action_web_service/vendor/ws/encoding/soap_rpc_encoding.rb new file mode 100644 index 0000000000..f4d2f5a7d6 --- /dev/null +++ b/actionwebservice/lib/action_web_service/vendor/ws/encoding/soap_rpc_encoding.rb @@ -0,0 +1,90 @@ +require 'soap/processor' +require 'soap/mapping' +require 'soap/rpc/element' + +module WS + module Encoding + class SoapRpcError < WSError + end + + class SoapRpcEncoding < AbstractEncoding + attr_accessor :method_namespace + + def initialize(method_namespace='') + @method_namespace = method_namespace + end + + def encode_rpc_call(method_name, foreign_params) + qname = create_method_qname(method_name) + param_def = [] + params = foreign_params.map do |p| + param_def << ['in', p.param.info.name, p.param.info.data.mapping] + [p.param.info.name, p.soap_object] + end + request = SOAP::RPC::SOAPMethodRequest.new(qname, param_def) + request.set_param(params) + envelope = create_soap_envelope(request) + SOAP::Processor.marshal(envelope) + end + + def decode_rpc_call(obj) + envelope = SOAP::Processor.unmarshal(obj) + unless envelope + raise(SoapRpcError, "Malformed SOAP request") + end + request = envelope.body.request + method_name = request.elename.name + params = request.collect do |key, value| + info = ParamInfo.new(key, nil, nil) + param = Param.new(nil, info) + Marshaling::SoapForeignObject.new(param, request[key]) + end + [method_name, params] + end + + def encode_rpc_response(method_name, return_value) + response = nil + qname = create_method_qname(method_name) + if return_value.nil? + response = SOAP::RPC::SOAPMethodResponse.new(qname, nil) + else + param = return_value.param + soap_object = return_value.soap_object + param_def = [['retval', 'return', param.info.data.mapping]] + if soap_object.is_a?(SOAP::SOAPFault) + response = soap_object + else + response = SOAP::RPC::SOAPMethodResponse.new(qname, param_def) + response.retval = soap_object + end + end + envelope = create_soap_envelope(response) + SOAP::Processor.marshal(envelope) + end + + def decode_rpc_response(obj) + envelope = SOAP::Processor.unmarshal(obj) + unless envelope + raise(SoapRpcError, "Malformed SOAP response") + end + method_name = envelope.body.request.elename.name + return_value = envelope.body.response + info = ParamInfo.new('return', nil, nil) + param = Param.new(nil, info) + return_value = Marshaling::SoapForeignObject.new(param, return_value) + [method_name, return_value] + end + + private + def create_soap_envelope(body) + header = SOAP::SOAPHeader.new + body = SOAP::SOAPBody.new(body) + SOAP::SOAPEnvelope.new(header, body) + end + + def create_method_qname(method_name) + XSD::QName.new(@method_namespace, method_name) + end + end + end +end diff --git a/actionwebservice/lib/action_web_service/vendor/ws/encoding/xmlrpc_encoding.rb b/actionwebservice/lib/action_web_service/vendor/ws/encoding/xmlrpc_encoding.rb new file mode 100644 index 0000000000..b38ae81abf --- /dev/null +++ b/actionwebservice/lib/action_web_service/vendor/ws/encoding/xmlrpc_encoding.rb @@ -0,0 +1,53 @@ +require 'xmlrpc/marshal' + +module WS + module Encoding + class XmlRpcError < WSError + end + + class XmlRpcEncoding < AbstractEncoding + def encode_rpc_call(method_name, params) + XMLRPC::Marshal.dump_call(method_name, *params) + end + + def decode_rpc_call(obj) + method_name, params = XMLRPC::Marshal.load_call(obj) rescue nil + unless method_name && params + raise(XmlRpcError, "Malformed XML-RPC request") + end + i = 0 + params = params.map do |value| + param = XmlRpcDecodedParam.new("param#{i}", value) + i += 1 + param + end + [method_name, params] + end + + def encode_rpc_response(method_name, return_value) + if return_value.nil? + XMLRPC::Marshal.dump_response(true) + else + XMLRPC::Marshal.dump_response(return_value) + end + end + + def decode_rpc_response(obj) + return_value = XMLRPC::Marshal.load_response(obj) rescue nil + if return_value.nil? + raise(XmlRpcError, "Malformed XML-RPC response") + end + [nil, XmlRpcDecodedParam.new('return', return_value)] + end + end + + class XmlRpcDecodedParam + attr :param + + def initialize(name, value) + info = ParamInfo.new(name, value.class) + @param = Param.new(value, info) + end + end + end +end diff --git a/actionwebservice/lib/action_web_service/vendor/ws/marshaling.rb b/actionwebservice/lib/action_web_service/vendor/ws/marshaling.rb new file mode 100644 index 0000000000..3a0a2e8cc1 --- /dev/null +++ b/actionwebservice/lib/action_web_service/vendor/ws/marshaling.rb @@ -0,0 +1,3 @@ +require 'ws/marshaling/abstract' +require 'ws/marshaling/soap_marshaling' +require 'ws/marshaling/xmlrpc_marshaling' diff --git a/actionwebservice/lib/action_web_service/vendor/ws/marshaling/abstract.rb b/actionwebservice/lib/action_web_service/vendor/ws/marshaling/abstract.rb new file mode 100644 index 0000000000..53120e1447 --- /dev/null +++ b/actionwebservice/lib/action_web_service/vendor/ws/marshaling/abstract.rb @@ -0,0 +1,17 @@ +module WS + module Marshaling + class AbstractMarshaler + def marshal(param) + raise NotImplementedError + end + + def unmarshal(param) + raise NotImplementedError + end + + def register_type(type) + nil + end + end + end +end diff --git a/actionwebservice/lib/action_web_service/vendor/ws/marshaling/soap_marshaling.rb b/actionwebservice/lib/action_web_service/vendor/ws/marshaling/soap_marshaling.rb new file mode 100644 index 0000000000..99e2a7ff28 --- /dev/null +++ b/actionwebservice/lib/action_web_service/vendor/ws/marshaling/soap_marshaling.rb @@ -0,0 +1,224 @@ +require 'soap/mapping' +require 'xsd/ns' + +module WS + module Marshaling + SoapEncodingNS = 'http://schemas.xmlsoap.org/soap/encoding/' + + class SoapError < WSError + end + + class SoapMarshaler < AbstractMarshaler + attr :registry + attr_accessor :type_namespace + + def initialize(type_namespace='') + @type_namespace = type_namespace + @registry = SOAP::Mapping::Registry.new + @spec2binding = {} + end + + def marshal(param) + if param.info.type.is_a?(Array) + (class << param.value; self; end).class_eval do + define_method(:arytype) do + param.info.data.qname + end + end + end + if param.value.is_a?(Exception) + detail = SOAP::Mapping::SOAPException.new(param.value) + soap_obj = SOAP::SOAPFault.new( + SOAP::SOAPString.new('Server'), + SOAP::SOAPString.new(param.value.to_s), + SOAP::SOAPString.new(self.class.name), + SOAP::Mapping.obj2soap(detail)) + else + soap_obj = SOAP::Mapping.obj2soap(param.value, @registry) + end + SoapForeignObject.new(param, soap_obj) + end + + def unmarshal(obj) + param = obj.param + soap_object = obj.soap_object + soap_type = soap_object ? soap_object.type : nil + value = soap_object ? SOAP::Mapping.soap2obj(soap_object, @registry) : nil + param.value = value + param.info.type = value.class + mapping = @registry.find_mapped_soap_class(param.info.type) rescue nil + if soap_type && soap_type.name == 'Array' && soap_type.namespace == SoapEncodingNS + param.info.data = SoapBinding.new(soap_object.arytype, mapping) + else + param.info.data = SoapBinding.new(soap_type, mapping) + end + param + end + + def register_type(spec) + if @spec2binding.has_key?(spec) + return @spec2binding[spec] + end + + klass = BaseTypes.canonical_param_type_class(spec) + if klass.is_a?(Array) + type_class = klass[0] + else + type_class = klass + end + + type_binding = nil + if (mapping = @registry.find_mapped_soap_class(type_class) rescue nil) + qname = mapping[2] ? mapping[2][:type] : nil + qname ||= soap_base_type_name(mapping[0]) + type_binding = SoapBinding.new(qname, mapping) + else + qname = XSD::QName.new(@type_namespace, soap_type_name(type_class.name)) + @registry.add(type_class, + SOAP::SOAPStruct, + typed_struct_factory(type_class), + { :type => qname }) + mapping = @registry.find_mapped_soap_class(type_class) + type_binding = SoapBinding.new(qname, mapping) + end + + array_binding = nil + if klass.is_a?(Array) + array_mapping = @registry.find_mapped_soap_class(Array) rescue nil + if (array_mapping && !array_mapping[1].is_a?(SoapTypedArrayFactory)) || array_mapping.nil? + @registry.set(Array, + SOAP::SOAPArray, + SoapTypedArrayFactory.new) + array_mapping = @registry.find_mapped_soap_class(Array) + end + qname = XSD::QName.new(@type_namespace, soap_type_name(type_class.name) + 'Array') + array_binding = SoapBinding.new(qname, array_mapping, type_binding) + end + + @spec2binding[spec] = array_binding ? array_binding : type_binding + end + + protected + def typed_struct_factory(type_class) + if Object.const_defined?('ActiveRecord') + if WS.derived_from?(ActiveRecord::Base, type_class) + qname = XSD::QName.new(@type_namespace, soap_type_name(type_class.name)) + type_class.instance_variable_set('@qname', qname) + return SoapActiveRecordStructFactory.new + end + end + SOAP::Mapping::Registry::TypedStructFactory + end + + def soap_type_name(type_name) + type_name.gsub(/::/, '..') + end + + def soap_base_type_name(type) + xsd_type = type.ancestors.find{|c| c.const_defined? 'Type'} + xsd_type ? xsd_type.const_get('Type') : XSD::XSDAnySimpleType::Type + end + end + + class SoapForeignObject + attr_accessor :param + attr_accessor :soap_object + + def initialize(param, soap_object) + @param = param + @soap_object = soap_object + end + end + + class SoapBinding + attr :qname + attr :mapping + attr :element_binding + + def initialize(qname, mapping, element_binding=nil) + @qname = qname + @mapping = mapping + @element_binding = element_binding + end + + def is_custom_type? + is_typed_array? || is_typed_struct? + end + + def is_typed_array? + @mapping[1].is_a?(WS::Marshaling::SoapTypedArrayFactory) + end + + def is_typed_struct? + @mapping[1] == SOAP::Mapping::Registry::TypedStructFactory || \ + @mapping[1].is_a?(WS::Marshaling::SoapActiveRecordStructFactory) + end + + def each_member(&block) + unless is_typed_struct? + raise(SoapError, "not a structured type") + end + end + + def type_name + is_custom_type? ? @qname.name : nil + end + + def qualified_type_name(ns=nil) + if is_custom_type? + "#{ns ? ns : @qname.namespace}:#{@qname.name}" + else + ns = XSD::NS.new + ns.assign(XSD::Namespace, SOAP::XSDNamespaceTag) + xsd_klass = mapping[0].ancestors.find{|c| c.const_defined?('Type')} + return ns.name(XSD::AnyTypeName) unless xsd_klass + ns.name(xsd_klass.const_get('Type')) + end + end + end + + class SoapActiveRecordStructFactory < SOAP::Mapping::Factory + def obj2soap(soap_class, obj, info, map) + unless obj.is_a?(ActiveRecord::Base) + return nil + end + soap_obj = soap_class.new(obj.class.instance_variable_get('@qname')) + obj.attributes.each do |key, value| + soap_obj[key] = SOAP::Mapping._obj2soap(value, map) + end + soap_obj + end + + def soap2obj(obj_class, node, info, map) + unless node.type == obj_class.instance_variable_get('@qname') + return false + end + obj = obj_class.new + node.each do |key, value| + obj[key] = value.data + end + obj.instance_variable_set('@new_record', false) + return true, obj + end + end + + class SoapTypedArrayFactory < SOAP::Mapping::Factory + def obj2soap(soap_class, obj, info, map) + unless obj.respond_to?(:arytype) + return nil + end + soap_obj = soap_class.new(SOAP::ValueArrayName, 1, obj.arytype) + mark_marshalled_obj(obj, soap_obj) + obj.each do |item| + child = SOAP::Mapping._obj2soap(item, map) + soap_obj.add(child) + end + soap_obj + end + + def soap2obj(obj_class, node, info, map) + return false + end + end + end +end diff --git a/actionwebservice/lib/action_web_service/vendor/ws/marshaling/xmlrpc_marshaling.rb b/actionwebservice/lib/action_web_service/vendor/ws/marshaling/xmlrpc_marshaling.rb new file mode 100644 index 0000000000..87154f87e1 --- /dev/null +++ b/actionwebservice/lib/action_web_service/vendor/ws/marshaling/xmlrpc_marshaling.rb @@ -0,0 +1,116 @@ +module WS + module Marshaling + class XmlRpcError < WSError + end + + class XmlRpcMarshaler < AbstractMarshaler + def initialize + @caster = BaseTypeCaster.new + @spec2binding = {} + end + + def marshal(param) + transform_outbound(param) + end + + def unmarshal(obj) + obj.param.value = transform_inbound(obj.param) + obj.param + end + + def typed_unmarshal(obj, spec) + param = obj.param + param.info.data = register_type(spec) + param.value = transform_inbound(param) + param + end + + def register_type(spec) + if @spec2binding.has_key?(spec) + return @spec2binding[spec] + end + + klass = BaseTypes.canonical_param_type_class(spec) + type_binding = nil + if klass.is_a?(Array) + type_binding = XmlRpcArrayBinding.new(klass[0]) + else + type_binding = XmlRpcBinding.new(klass) + end + + @spec2binding[spec] = type_binding + end + + def transform_outbound(param) + binding = param.info.data + case binding + when XmlRpcArrayBinding + param.value.map{|x| cast_outbound(x, binding.element_klass)} + when XmlRpcBinding + cast_outbound(param.value, param.info.type) + end + end + + def transform_inbound(param) + return param.value if param.info.data.nil? + binding = param.info.data + param.info.type = binding.klass + case binding + when XmlRpcArrayBinding + param.value.map{|x| cast_inbound(x, binding.element_klass)} + when XmlRpcBinding + cast_inbound(param.value, param.info.type) + end + end + + def cast_outbound(value, klass) + if BaseTypes.base_type?(klass) + @caster.cast(value, klass) + elsif value.is_a?(Exception) + XMLRPC::FaultException.new(2, value.message) + elsif Object.const_defined?('ActiveRecord') && value.is_a?(ActiveRecord::Base) + value.attributes + else + struct = {} + value.instance_variables.each do |name| + key = name.sub(/^@/, '') + struct[key] = value.instance_variable_get(name) + end + struct + end + end + + def cast_inbound(value, klass) + if BaseTypes.base_type?(klass) + value = value.to_time if value.is_a?(XMLRPC::DateTime) + @caster.cast(value, klass) + elsif value.is_a?(XMLRPC::FaultException) + value + else + obj = klass.new + value.each do |name, val| + obj.send('%s=' % name.to_s, val) + end + obj + end + end + end + + class XmlRpcBinding + attr :klass + + def initialize(klass) + @klass = klass + end + end + + class XmlRpcArrayBinding < XmlRpcBinding + attr :element_klass + + def initialize(element_klass) + super(Array) + @element_klass = element_klass + end + end + end +end diff --git a/actionwebservice/lib/action_web_service/vendor/ws/types.rb b/actionwebservice/lib/action_web_service/vendor/ws/types.rb new file mode 100644 index 0000000000..24b96dc327 --- /dev/null +++ b/actionwebservice/lib/action_web_service/vendor/ws/types.rb @@ -0,0 +1,162 @@ +require 'time' +require 'date' + +module WS + module BaseTypes + class << self + def type_name_to_class(name) + case canonical_type_name(name) + when :int + Integer + when :string + String + when :bool + TrueClass + when :float + Float + when :time + Time + when :date + Date + end + end + + def class_to_type_name(klass) + if WS.derived_from?(Integer, klass) || WS.derived_from?(Fixnum, klass) || WS.derived_from?(Bignum, klass) + :int + elsif klass == String + :string + elsif klass == TrueClass || klass == FalseClass + :bool + elsif WS.derived_from?(Float, klass) || WS.derived_from?(Precision, klass) || WS.derived_from?(Numeric, klass) + :float + elsif klass == Time || klass == DateTime + :time + elsif klass == Date + :date + else + raise(TypeError, "#{klass} is not a valid base type") + end + end + + def base_type?(klass) + !(canonical_type_class(klass) rescue nil).nil? + end + + def canonical_type_class(klass) + type_name_to_class(class_to_type_name(klass)) + end + + def canonical_param_type_class(spec) + klass = spec.is_a?(Hash) ? spec.values[0] : spec + array_element_class = klass.is_a?(Array) ? klass[0] : nil + klass = array_element_class ? array_element_class : klass + klass = type_name_to_class(klass) if klass.is_a?(Symbol) || klass.is_a?(String) + base_class = canonical_type_class(klass) rescue nil + klass = base_class unless base_class.nil? + array_element_class ? [klass] : klass + end + + def canonical_param_type_spec(spec) + klass = canonical_param_type_class(spec) + spec.is_a?(Hash) ? {spec.keys[0]=>klass} : klass + end + + def canonical_type_name(name) + name = name.to_sym + case name + when :int, :integer, :fixnum, :bignum + :int + when :string, :base64 + :string + when :bool, :boolean + :bool + when :float, :double + :float + when :time, :datetime, :timestamp + :time + when :date + :date + else + raise(TypeError, "#{name} is not a valid base type") + end + end + end + end + + class Param + attr_accessor :value + attr_accessor :info + + def initialize(value, info) + @value = value + @info = info + end + end + + class ParamInfo + attr_accessor :name + attr_accessor :type + attr_accessor :data + + def initialize(name, type, data=nil) + @name = name + @type = type + @data = data + end + + def self.create(spec, index=nil, data=nil) + name = spec.is_a?(Hash) ? spec.keys[0].to_s : (index ? "param#{index}" : nil) + type = BaseTypes.canonical_param_type_class(spec) + ParamInfo.new(name, type, data) + end + end + + class BaseTypeCaster + def initialize + @handlers = {} + install_handlers + end + + def cast(value, klass) + type_class = BaseTypes.canonical_type_class(klass) + return value unless type_class + @handlers[type_class].call(value, type_class) + end + + protected + def install_handlers + handler = method(:cast_base_type) + [:int, :string, :bool, :float, :time, :date].each do |name| + type = BaseTypes.type_name_to_class(name) + @handlers[type] = handler + end + @handlers[Fixnum] = handler + end + + def cast_base_type(value, type_class) + desired_class = BaseTypes.canonical_type_class(type_class) + value_class = BaseTypes.canonical_type_class(value.class) + return value if desired_class == value_class + desired_name = BaseTypes.class_to_type_name(desired_class) + case desired_name + when :int + Integer(value) + when :string + value.to_s + when :bool + return false if value.nil? + value = value.to_s + return true if value == 'true' + return false if value == 'false' + raise(TypeError, "can't convert #{value} to boolean") + when :float + Float(value) + when :time + Time.parse(value.to_s) + when :date + Date.parse(value.to_s) + end + end + end +end |