From e7a29380292902eae4799b2658507b3cfffb9cec Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 18 Feb 2005 10:35:25 +0000 Subject: Added Action Service to the repository git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@658 5ecf4fe2-1ee6-0310-87b1-e25e094e27de --- actionservice/lib/action_service/protocol/soap.rb | 484 ++++++++++++++++++++++ 1 file changed, 484 insertions(+) create mode 100644 actionservice/lib/action_service/protocol/soap.rb (limited to 'actionservice/lib/action_service/protocol/soap.rb') diff --git a/actionservice/lib/action_service/protocol/soap.rb b/actionservice/lib/action_service/protocol/soap.rb new file mode 100644 index 0000000000..24cc554b05 --- /dev/null +++ b/actionservice/lib/action_service/protocol/soap.rb @@ -0,0 +1,484 @@ +require 'soap/processor' +require 'soap/mapping' +require 'soap/rpc/element' +require 'xsd/datatypes' +require 'xsd/ns' +require 'singleton' + +module ActionService # :nodoc: + module Protocol # :nodoc: + module Soap # :nodoc: + class ProtocolError < ActionService::ActionServiceError # :nodoc: + end + + def self.append_features(base) # :nodoc: + super + base.register_protocol(HeaderAndBody, SoapProtocol) + base.extend(ClassMethods) + base.wsdl_service_name('ActionService') + end + + module ClassMethods + # Specifies the WSDL service name to use when generating WSDL. Highly + # recommended that you set this value, or code generators may generate + # classes with very generic names. + # + # === Example + # class MyController < ActionController::Base + # wsdl_service_name 'MyService' + # end + def wsdl_service_name(name) + write_inheritable_attribute("soap_mapper", SoapMapper.new("urn:#{name}")) + end + + def soap_mapper # :nodoc: + read_inheritable_attribute("soap_mapper") + end + end + + class SoapProtocol < AbstractProtocol # :nodoc: + attr :mapper + + def initialize(mapper) + @mapper = mapper + end + + def self.create_protocol_request(container_class, action_pack_request) + soap_action = extract_soap_action(action_pack_request) + return nil unless soap_action + service_name = action_pack_request.parameters['action'] + public_method_name = soap_action.gsub(/^[\/]+/, '').split(/[\/]+/)[-1] + content_type = action_pack_request.env['HTTP_CONTENT_TYPE'] + content_type ||= 'text/xml' + protocol = SoapProtocol.new(container_class.soap_mapper) + ProtocolRequest.new(protocol, + action_pack_request.raw_post, + service_name.to_sym, + public_method_name, + content_type) + end + + def self.create_protocol_client(api, protocol_name, endpoint_uri, options) + return nil unless protocol_name.to_s.downcase.to_sym == :soap + ActionService::Client::Soap.new(api, endpoint_uri, options) + end + + def unmarshal_request(protocol_request) + unmarshal = lambda do + envelope = SOAP::Processor.unmarshal(protocol_request.raw_body) + request = envelope.body.request + values = request.collect{|k, v| request[k]} + soap_to_ruby_array(values) + end + signature = protocol_request.signature + if signature + map_signature_types(signature) + values = unmarshal.call + signature = signature.map{|x|mapper.lookup(x).ruby_klass} + protocol_request.check_parameter_types(values, signature) + values + else + if protocol_request.checked? + [] + else + unmarshal.call + end + end + end + + def marshal_response(protocol_request, return_value) + marshal = lambda do |signature| + mapping = mapper.lookup(signature[0]) + return_value = fixup_array_types(mapping, return_value) + signature = signature.map{|x|mapper.lookup(x).ruby_klass} + protocol_request.check_parameter_types([return_value], signature) + param_def = [['retval', 'return', mapping.registry_mapping]] + [param_def, ruby_to_soap(return_value)] + end + signature = protocol_request.return_signature + param_def = nil + if signature + param_def, return_value = marshal.call(signature) + else + if protocol_request.checked? + param_def, return_value = nil, nil + else + param_def, return_value = marshal.call([return_value.class]) + end + end + qname = XSD::QName.new(mapper.custom_namespace, + protocol_request.public_method_name) + response = SOAP::RPC::SOAPMethodResponse.new(qname, param_def) + response.retval = return_value unless return_value.nil? + ProtocolResponse.new(self, create_response(response), 'text/xml') + end + + def marshal_exception(exc) + ProtocolResponse.new(self, create_exception_response(exc), 'text/xml') + end + + private + def self.extract_soap_action(request) + return nil unless request.method == :post + content_type = request.env['HTTP_CONTENT_TYPE'] || 'text/xml' + return nil unless content_type + soap_action = request.env['HTTP_SOAPACTION'] + return nil unless soap_action + soap_action.gsub!(/^"/, '') + soap_action.gsub!(/"$/, '') + soap_action.strip! + return nil if soap_action.empty? + soap_action + end + + def fixup_array_types(mapping, obj) + mapping.each_attribute do |name, type, attr_mapping| + if attr_mapping.custom_type? + attr_obj = obj.send(name) + new_obj = fixup_array_types(attr_mapping, attr_obj) + obj.send("#{name}=", new_obj) unless new_obj.equal?(attr_obj) + end + end + if mapping.is_a?(SoapArrayMapping) + obj = mapping.ruby_klass.new(obj) + # man, this is going to be slow for big arrays :( + (1..obj.size).each do |i| + i -= 1 + obj[i] = fixup_array_types(mapping.element_mapping, obj[i]) + end + else + if !mapping.generated_klass.nil? && mapping.generated_klass.respond_to?(:members) + # have to map the publically visible structure of the class + new_obj = mapping.generated_klass.new + mapping.generated_klass.members.each do |name, klass| + new_obj.send("#{name}=", obj.send(name)) + end + obj = new_obj + end + end + obj + end + + def map_signature_types(types) + types.collect{|type| mapper.map(type)} + end + + def create_response(body) + header = SOAP::SOAPHeader.new + body = SOAP::SOAPBody.new(body) + envelope = SOAP::SOAPEnvelope.new(header, body) + SOAP::Processor.marshal(envelope) + end + + def create_exception_response(exc) + detail = SOAP::Mapping::SOAPException.new(exc) + body = SOAP::SOAPFault.new( + SOAP::SOAPString.new('Server'), + SOAP::SOAPString.new(exc.to_s), + SOAP::SOAPString.new(self.class.name), + SOAP::Mapping.obj2soap(detail)) + create_response(body) + end + + def ruby_to_soap(obj) + SOAP::Mapping.obj2soap(obj, mapper.registry) + end + + def soap_to_ruby(obj) + SOAP::Mapping.soap2obj(obj, mapper.registry) + end + + def soap_to_ruby_array(array) + array.map{|x| soap_to_ruby(x)} + end + end + + class SoapMapper # :nodoc: + attr :registry + attr :custom_namespace + attr :custom_types + + def initialize(custom_namespace) + @custom_namespace = custom_namespace + @registry = SOAP::Mapping::Registry.new + @klass2map = {} + @custom_types = {} + @ar2klass = {} + end + + def lookup(klass) + lookup_klass = klass.is_a?(Array) ? klass[0] : klass + generated_klass = nil + unless lookup_klass.respond_to?(:ancestors) + raise(ProtocolError, "expected parameter type definition to be a Class") + end + if lookup_klass.ancestors.include?(ActiveRecord::Base) + generated_klass = @ar2klass.has_key?(klass) ? @ar2klass[klass] : nil + klass = generated_klass if generated_klass + end + return @klass2map[klass] if @klass2map.has_key?(klass) + + custom_type = false + + ruby_klass = select_class(lookup_klass) + generated_klass = @ar2klass[lookup_klass] if @ar2klass.has_key?(lookup_klass) + type_name = ruby_klass.name + + # Array signatures generate a double-mapping and require generation + # of an Array subclass to represent the mapping in the SOAP + # registry + array_klass = nil + if klass.is_a?(Array) + array_klass = Class.new(Array) do + module_eval <<-END + def self.name + "#{type_name}Array" + end + END + end + end + + mapping = @registry.find_mapped_soap_class(ruby_klass) rescue nil + unless mapping + # Custom structured type, generate a mapping + info = { :type => XSD::QName.new(@custom_namespace, type_name) } + @registry.add(ruby_klass, + SOAP::SOAPStruct, + SOAP::Mapping::Registry::TypedStructFactory, + info) + mapping = ensure_mapped(ruby_klass) + custom_type = true + end + + array_mapping = nil + if array_klass + # Typed array always requires a custom type. The info of the array + # is the info of its element type (in mapping[2]), falling back + # to SOAP base types. + info = mapping[2] + info ||= {} + info[:type] ||= soap_base_type_qname(mapping[0]) + @registry.add(array_klass, + SOAP::SOAPArray, + SOAP::Mapping::Registry::TypedArrayFactory, + info) + array_mapping = ensure_mapped(array_klass) + end + + if array_mapping + @klass2map[ruby_klass] = SoapMapping.new(self, + type_name, + ruby_klass, + generated_klass, + mapping[0], + mapping, + custom_type) + @klass2map[klass] = SoapArrayMapping.new(self, + type_name, + array_klass, + array_mapping[0], + array_mapping, + @klass2map[ruby_klass]) + @custom_types[klass] = @klass2map[klass] + @custom_types[ruby_klass] = @klass2map[ruby_klass] if custom_type + else + @klass2map[klass] = SoapMapping.new(self, + type_name, + ruby_klass, + generated_klass, + mapping[0], + mapping, + custom_type) + @custom_types[klass] = @klass2map[klass] if custom_type + end + + @klass2map[klass] + end + alias :map :lookup + + def map_container_services(container, &block) + dispatching_mode = container.service_dispatching_mode + services = nil + case dispatching_mode + when :direct + api = container.class.service_api + if container.respond_to?(:controller_class_name) + service_name = container.controller_class_name.sub(/Controller$/, '').underscore + else + service_name = container.class.name.demodulize.underscore + end + services = { service_name => api } + when :delegated + services = {} + container.class.services.each do |service_name, service_info| + begin + object = container.service_object(service_name) + rescue Exception => e + raise(ProtocolError, "failed to retrieve service object for mapping: #{e.message}") + end + services[service_name] = object.class.service_api + end + end + services.each do |service_name, api| + if api.nil? + raise(ProtocolError, "no service API set while in :#{dispatching_mode} mode") + end + map_api(api) do |api_methods| + yield service_name, api, api_methods if block_given? + end + end + end + + def map_api(api, &block) + lookup_proc = lambda do |klass| + mapping = lookup(klass) + custom_mapping = nil + if mapping.respond_to?(:element_mapping) + custom_mapping = mapping.element_mapping + else + custom_mapping = mapping + end + if custom_mapping && custom_mapping.custom_type? + # What gives? This is required so that structure types + # referenced only by structures (and not signatures) still + # have a custom type mapping in the registry (needed for WSDL + # generation). + custom_mapping.each_attribute{} + end + mapping + end + api_methods = block.nil?? nil : {} + api.api_methods.each do |method_name, method_info| + expects = method_info[:expects] + expects_signature = nil + if expects + expects_signature = block ? [] : nil + expects.each do |klass| + lookup_klass = nil + if klass.is_a?(Hash) + lookup_klass = lookup_proc.call(klass.values[0]) + expects_signature << {klass.keys[0]=>lookup_klass} if block + else + lookup_klass = lookup_proc.call(klass) + expects_signature << lookup_klass if block + end + end + end + returns = method_info[:returns] + returns_signature = returns ? returns.map{|klass| lookup_proc.call(klass)} : nil + if block + api_methods[method_name] = { + :expects => expects_signature, + :returns => returns_signature + } + end + end + yield api_methods if block + end + + private + def select_class(klass) + return Integer if klass == Fixnum + if klass.ancestors.include?(ActiveRecord::Base) + new_klass = Class.new(ActionService::Struct) + new_klass.class_eval <<-EOS + def self.name + "#{klass.name}" + end + EOS + klass.columns.each do |column| + next if column.klass.nil? + new_klass.send(:member, column.name.to_sym, column.klass) + end + @ar2klass[klass] = new_klass + return new_klass + end + klass + end + + def ensure_mapped(klass) + mapping = @registry.find_mapped_soap_class(klass) rescue nil + raise(ProtocolError, "failed to register #{klass.name}") unless mapping + mapping + end + + def soap_base_type_qname(base_type) + xsd_type = base_type.ancestors.find{|c| c.const_defined? 'Type'} + xsd_type ? xsd_type.const_get('Type') : XSD::XSDAnySimpleType::Type + end + end + + class SoapMapping # :nodoc: + attr :ruby_klass + attr :generated_klass + attr :soap_klass + attr :registry_mapping + + def initialize(mapper, type_name, ruby_klass, generated_klass, soap_klass, registry_mapping, + custom_type=false) + @mapper = mapper + @type_name = type_name + @ruby_klass = ruby_klass + @generated_klass = generated_klass + @soap_klass = soap_klass + @registry_mapping = registry_mapping + @custom_type = custom_type + end + + def type_name + @type_name + end + + def custom_type? + @custom_type + end + + def qualified_type_name + name = type_name + if custom_type? + "typens:#{name}" + else + xsd_type_for(@soap_klass) + end + end + + def each_attribute(&block) + if @ruby_klass.respond_to?(:members) + @ruby_klass.members.each do |name, klass| + name = name.to_s + mapping = @mapper.lookup(klass) + yield name, mapping.qualified_type_name, mapping + end + end + end + + def is_xsd_type?(klass) + klass.ancestors.include?(XSD::NSDBase) + end + + def xsd_type_for(klass) + ns = XSD::NS.new + ns.assign(XSD::Namespace, SOAP::XSDNamespaceTag) + xsd_klass = klass.ancestors.find{|c| c.const_defined?('Type')} + return ns.name(XSD::AnyTypeName) unless xsd_klass + ns.name(xsd_klass.const_get('Type')) + end + end + + class SoapArrayMapping < SoapMapping # :nodoc: + attr :element_mapping + + def initialize(mapper, type_name, ruby_klass, soap_klass, registry_mapping, element_mapping) + super(mapper, type_name, ruby_klass, nil, soap_klass, registry_mapping, true) + @element_mapping = element_mapping + end + + def type_name + super + "Array" + end + + def each_attribute(&block); end + end + end + end +end -- cgit v1.2.3