Welcome to the "trac"-ing site of soap4r!
[soap4r] [httpclient] [openpgp4u] [pkcs1] [logger] [csv] [vtr]

root/branches/1_5/lib/soap/rpc/router.rb

Revision 1974, 20.3 kB (checked in by nahi, 1 year ago)
  • add the Generator option for using default namespace in SOAP message. it's for interoperability with non XML Namespace comformant implementation. false by default (same behavior as before.) you can turn it on with Driver#use_default_namespace = true or Server#use_default_namespace = true. closes #430.
  • Property svn:eol-style set to native
  • Property svn:keywords set to author date id revision
Line 
1 # SOAP4R - RPC Routing library
2 # Copyright (C) 2000-2007  NAKAMURA, Hiroshi <nahi@ruby-lang.org>.
3
4 # This program is copyrighted free software by NAKAMURA, Hiroshi.  You can
5 # redistribute it and/or modify it under the same terms of Ruby's license;
6 # either the dual license version in 2003, or any later version.
7
8
9 require 'soap/soap'
10 require 'soap/processor'
11 require 'soap/mapping'
12 require 'soap/mapping/literalregistry'
13 require 'soap/rpc/rpc'
14 require 'soap/rpc/element'
15 require 'soap/header/handlerset'
16 require 'soap/filter'
17 require 'soap/streamHandler'
18 require 'soap/mimemessage'
19 require 'soap/header/handlerset'
20
21
22 module SOAP
23 module RPC
24
25
26 class Router
27   include SOAP
28
29   attr_reader :actor
30   attr_accessor :mapping_registry
31   attr_accessor :literal_mapping_registry
32   attr_accessor :generate_explicit_type
33   attr_accessor :use_default_namespace
34   attr_accessor :external_ces
35   attr_reader :filterchain
36
37   def initialize(actor)
38     @actor = actor
39     @mapping_registry = nil
40     @headerhandler = Header::HandlerSet.new
41     @literal_mapping_registry = ::SOAP::Mapping::LiteralRegistry.new
42     @generate_explicit_type = true
43     @use_default_namespace = false
44     @external_ces = nil
45     @operation_by_soapaction = {}
46     @operation_by_qname = {}
47     @headerhandlerfactory = []
48     @filterchain = Filter::FilterChain.new
49   end
50
51   ###
52   ## header handler interface
53   #
54   def add_request_headerhandler(factory)
55     unless factory.respond_to?(:create)
56       raise TypeError.new("factory must respond to 'create'")
57     end
58     @headerhandlerfactory << factory
59   end
60
61   def add_headerhandler(handler)
62     @headerhandler.add(handler)
63   end
64
65   ###
66   ## servant definition interface
67   #
68   def add_rpc_request_servant(factory, namespace)
69     unless factory.respond_to?(:create)
70       raise TypeError.new("factory must respond to 'create'")
71     end
72     obj = factory.create        # a dummy instance for introspection
73     ::SOAP::RPC.defined_methods(obj).each do |name|
74       begin
75         qname = XSD::QName.new(namespace, name)
76         param_def = ::SOAP::RPC::SOAPMethod.derive_rpc_param_def(obj, name)
77         opt = create_styleuse_option(:rpc, :encoded)
78         add_rpc_request_operation(factory, qname, nil, name, param_def, opt)
79       rescue SOAP::RPC::MethodDefinitionError => e
80         p e if $DEBUG
81       end
82     end
83   end
84
85   def add_rpc_servant(obj, namespace)
86     ::SOAP::RPC.defined_methods(obj).each do |name|
87       begin
88         qname = XSD::QName.new(namespace, name)
89         param_def = ::SOAP::RPC::SOAPMethod.derive_rpc_param_def(obj, name)
90         opt = create_styleuse_option(:rpc, :encoded)
91         add_rpc_operation(obj, qname, nil, name, param_def, opt)
92       rescue SOAP::RPC::MethodDefinitionError => e
93         p e if $DEBUG
94       end
95     end
96   end
97   alias add_servant add_rpc_servant
98
99   ###
100   ## operation definition interface
101   #
102   def add_rpc_operation(receiver, qname, soapaction, name, param_def, opt = {})
103     ensure_styleuse_option(opt, :rpc, :encoded)
104     opt[:request_qname] = qname
105     op = ApplicationScopeOperation.new(soapaction, receiver, name, param_def,
106       opt)
107     if opt[:request_style] != :rpc
108       raise RPCRoutingError.new("illegal request_style given")
109     end
110     assign_operation(soapaction, qname, op)
111   end
112   alias add_method add_rpc_operation
113   alias add_rpc_method add_rpc_operation
114
115   def add_rpc_request_operation(factory, qname, soapaction, name, param_def, opt = {})
116     ensure_styleuse_option(opt, :rpc, :encoded)
117     opt[:request_qname] = qname
118     op = RequestScopeOperation.new(soapaction, factory, name, param_def, opt)
119     if opt[:request_style] != :rpc
120       raise RPCRoutingError.new("illegal request_style given")
121     end
122     assign_operation(soapaction, qname, op)
123   end
124
125   def add_document_operation(receiver, soapaction, name, param_def, opt = {})
126     #
127     # adopt workaround for doc/lit wrapper method
128     # (you should consider to simply use rpc/lit service)
129     #
130     #unless soapaction
131     #  raise RPCRoutingError.new("soapaction is a must for document method")
132     #end
133     ensure_styleuse_option(opt, :document, :literal)
134     op = ApplicationScopeOperation.new(soapaction, receiver, name, param_def,
135       opt)
136     if opt[:request_style] != :document
137       raise RPCRoutingError.new("illegal request_style given")
138     end
139     assign_operation(soapaction, first_input_part_qname(param_def), op)
140   end
141   alias add_document_method add_document_operation
142
143   def add_document_request_operation(factory, soapaction, name, param_def, opt = {})
144     #
145     # adopt workaround for doc/lit wrapper method
146     # (you should consider to simply use rpc/lit service)
147     #
148     #unless soapaction
149     #  raise RPCRoutingError.new("soapaction is a must for document method")
150     #end
151     ensure_styleuse_option(opt, :document, :literal)
152     op = RequestScopeOperation.new(soapaction, receiver, name, param_def, opt)
153     if opt[:request_style] != :document
154       raise RPCRoutingError.new("illegal request_style given")
155     end
156     assign_operation(soapaction, first_input_part_qname(param_def), op)
157   end
158
159   def route(conn_data)
160     # we cannot set request_default_encodingsyle before parsing the content.
161     env = unmarshal(conn_data)
162     if env.nil?
163       raise ArgumentError.new("illegal SOAP marshal format")
164     end
165     op = lookup_operation(conn_data.soapaction, env.body)
166     headerhandler = @headerhandler.dup
167     @headerhandlerfactory.each do |f|
168       headerhandler.add(f.create)
169     end
170     soap_response = default_encodingstyle = nil
171     begin
172       receive_headers(headerhandler, env.header)
173       soap_response =
174         op.call(env.body, @mapping_registry, @literal_mapping_registry,
175           create_mapping_opt)
176       conn_data.is_fault = true if soap_response.is_a?(SOAPFault)
177       default_encodingstyle = op.response_default_encodingstyle
178     rescue Exception => e
179       # If a wsdl fault was raised by service, the fault declaration details
180       # is kept in wsdl_fault. Otherwise (exception is a program fault)
181       # wsdl_fault is nil
182       wsdl_fault_details = op.faults && op.faults[e.class.name]
183       soap_response = fault(e, wsdl_fault_details)
184       conn_data.is_fault = true
185       default_encodingstyle = nil
186     end
187     header = call_headers(headerhandler)
188     if op.response_use.nil?
189       conn_data.send_string = ''
190       conn_data.is_nocontent = true
191       conn_data
192     else
193       body = SOAPBody.new(soap_response, conn_data.is_fault)
194       env = SOAPEnvelope.new(header, body)
195       marshal(conn_data, env, default_encodingstyle)
196     end
197   end
198
199   # Create fault response string.
200   def create_fault_response(e)
201     env = SOAPEnvelope.new(SOAPHeader.new, SOAPBody.new(fault(e, nil), true))
202     opt = {}
203     opt[:external_content] = nil
204     @filterchain.reverse_each do |filter|
205       env = filter.on_outbound(env, opt)
206       break unless env
207     end
208     response_string = Processor.marshal(env, opt)
209     conn_data = StreamHandler::ConnectionData.new(response_string)
210     conn_data.is_fault = true
211     if ext = opt[:external_content]
212       mimeize(conn_data, ext)
213     end
214     conn_data
215   end
216
217 private
218
219   def first_input_part_qname(param_def)
220     param_def.each do |inout, paramname, typeinfo|
221       if inout == SOAPMethod::IN
222         klass, nsdef, namedef = typeinfo
223         return XSD::QName.new(nsdef, namedef)
224       end
225     end
226     nil
227   end
228
229   def create_styleuse_option(style, use)
230     opt = {}
231     opt[:request_style] = opt[:response_style] = style
232     opt[:request_use] = opt[:response_use] = use
233     opt
234   end
235
236   def ensure_styleuse_option(opt, style, use)
237     if opt[:request_style] || opt[:response_style] || opt[:request_use] || opt[:response_use]
238       # do not edit
239     else
240       opt[:request_style] ||= style
241       opt[:response_style] ||= style
242       opt[:request_use] ||= use
243       opt[:response_use] ||= use
244     end
245   end
246
247   def assign_operation(soapaction, qname, op)
248     assigned = false
249     if soapaction and !soapaction.empty?
250       @operation_by_soapaction[soapaction] = op
251       assigned = true
252     end
253     if qname
254       @operation_by_qname[qname] = op
255       assigned = true
256     end
257     unless assigned
258       raise RPCRoutingError.new("cannot assign operation")
259     end
260   end
261
262   def lookup_operation(soapaction, body)
263     if op = @operation_by_soapaction[soapaction]
264       return op
265     end
266     qname = body.root_node.elename
267     if op = @operation_by_qname[qname]
268       return op
269     end
270     if soapaction
271       raise RPCRoutingError.new(
272         "operation: #{soapaction} #{qname} not supported")
273     else
274       raise RPCRoutingError.new("operation: #{qname} not supported")
275     end
276   end
277
278   def call_headers(headerhandler)
279     header = ::SOAP::SOAPHeader.new
280     items = headerhandler.on_outbound(header)
281     items.each do |item|
282       header.add(item.elename.name, item)
283     end
284     header
285   end
286
287   def receive_headers(headerhandler, header)
288     headerhandler.on_inbound(header) if header
289   end
290
291   def unmarshal(conn_data)
292     xml = nil
293     opt = {}
294     contenttype = conn_data.receive_contenttype
295     if /#{MIMEMessage::MultipartContentType}/i =~ contenttype
296       opt[:external_content] = {}
297       mime = MIMEMessage.parse("Content-Type: " + contenttype,
298         conn_data.receive_string)
299       mime.parts.each do |part|
300         value = Attachment.new(part.content)
301         value.contentid = part.contentid
302         obj = SOAPAttachment.new(value)
303         opt[:external_content][value.contentid] = obj if value.contentid
304       end
305       opt[:charset] =
306         StreamHandler.parse_media_type(mime.root.headers['content-type'].str)
307       xml = mime.root.content
308     else
309       opt[:charset] = ::SOAP::StreamHandler.parse_media_type(contenttype)
310       xml = conn_data.receive_string
311     end
312     @filterchain.each do |filter|
313       xml = filter.on_inbound(xml, opt)
314       break unless xml
315     end
316     env = Processor.unmarshal(xml, opt)
317     charset = opt[:charset]
318     conn_data.send_contenttype = "text/xml; charset=\"#{charset}\""
319     env
320   end
321
322   def marshal(conn_data, env, default_encodingstyle = nil)
323     opt = {}
324     opt[:external_content] = nil
325     opt[:default_encodingstyle] = default_encodingstyle
326     opt[:generate_explicit_type] = @generate_explicit_type
327     opt[:use_default_namespace] = @use_default_namespace
328     @filterchain.reverse_each do |filter|
329       env = filter.on_outbound(env, opt)
330       break unless env
331     end
332     response_string = Processor.marshal(env, opt)
333     conn_data.send_string = response_string
334     if ext = opt[:external_content]
335       mimeize(conn_data, ext)
336     end
337     conn_data
338   end
339
340   def mimeize(conn_data, ext)
341     mime = MIMEMessage.new
342     ext.each do |k, v|
343       mime.add_attachment(v.data)
344     end
345     mime.add_part(conn_data.send_string + "\r\n")
346     mime.close
347     conn_data.send_string = mime.content_str
348     conn_data.send_contenttype = mime.headers['content-type'].str
349     conn_data
350   end
351
352   # Create fault response.
353   def fault(e, wsdl_fault_details)
354     if e.is_a?(UnhandledMustUnderstandHeaderError)
355       faultcode = FaultCode::MustUnderstand
356     else
357       faultcode = FaultCode::Server
358     end
359
360     # If the exception represents a WSDL fault, the fault element should
361     # be added as the SOAP fault <detail> element. If the exception is a
362     # normal program exception, it is wrapped inside a custom SOAP4R
363     # SOAP exception element.
364     detail = nil
365     begin
366       if (wsdl_fault_details)
367         registry = wsdl_fault_details[:use] == "literal" ?
368           @literal_mapping_registry : @mapping_registry
369         faultQName = XSD::QName.new(
370           wsdl_fault_details[:ns], wsdl_fault_details[:name]
371         )
372         detail = Mapping.obj2soap(e, registry, faultQName)
373         # wrap fault element (SOAPFault swallows top-level element)
374         wrapper = SOAP::SOAPElement.new(faultQName)
375         wrapper.add(detail)
376         detail = wrapper
377       else
378         # Exception is a normal program exception. Wrap it.
379         detail = Mapping.obj2soap(Mapping::SOAPException.new(e),
380                                   @mapping_registry)
381         detail.elename ||= XSD::QName::EMPTY # for literal mappingregstry
382       end
383     rescue
384       detail = SOAPString.new("failed to serialize detail object: #{$!}")
385     end
386
387     SOAPFault.new(
388       SOAPElement.new(nil, faultcode),
389       SOAPString.new(e.to_s),
390       SOAPString.new(@actor),
391       detail)
392   end
393
394   def create_mapping_opt
395     { :external_ces => @external_ces }
396   end
397
398   class Operation
399     attr_reader :name
400     attr_reader :soapaction
401     attr_reader :request_style
402     attr_reader :response_style
403     attr_reader :request_use
404     attr_reader :response_use
405     attr_reader :faults
406
407     def initialize(soapaction, name, param_def, opt)
408       @soapaction = soapaction
409       @name = name
410       @request_style = opt[:request_style]
411       @response_style = opt[:response_style]
412       @request_use = opt[:request_use]
413       @response_use = opt[:response_use]
414       @faults = opt[:faults]
415       check_style(@request_style)
416       check_style(@response_style)
417       check_use(@request_use)
418       check_use(@response_use)
419       if @response_style == :rpc
420         request_qname = opt[:request_qname] or raise
421         @rpc_method_factory =
422           RPC::SOAPMethodRequest.new(request_qname, param_def, @soapaction)
423         @rpc_response_qname = opt[:response_qname]
424       else
425         @doc_request_qnames = []
426         @doc_request_qualified = []
427         @doc_response_qnames = []
428         @doc_response_qualified = []
429         param_def.each do |inout, paramname, typeinfo, eleinfo|
430           klass, nsdef, namedef = typeinfo
431           qualified = eleinfo
432           case inout
433           when SOAPMethod::IN
434             @doc_request_qnames << XSD::QName.new(nsdef, namedef)
435             @doc_request_qualified << qualified
436           when SOAPMethod::OUT
437             @doc_response_qnames << XSD::QName.new(nsdef, namedef)
438             @doc_response_qualified << qualified
439           else
440             raise ArgumentError.new(
441               "illegal inout definition for document style: #{inout}")
442           end
443         end
444       end
445     end
446
447     def request_default_encodingstyle
448       (@request_use == :encoded) ? EncodingNamespace : LiteralNamespace
449     end
450
451     def response_default_encodingstyle
452       (@response_use == :encoded) ? EncodingNamespace : LiteralNamespace
453     end
454
455     def call(body, mapping_registry, literal_mapping_registry, opt)
456       if @request_style == :rpc
457         values = request_rpc(body, mapping_registry, literal_mapping_registry,
458           opt)
459       else
460         values = request_document(body, mapping_registry,
461           literal_mapping_registry, opt)
462       end
463       result = receiver.method(@name.intern).call(*values)
464       return result if result.is_a?(SOAPFault)
465       if @response_style == :rpc
466         response_rpc(result, mapping_registry, literal_mapping_registry, opt)
467       elsif @doc_response_qnames.empty?
468         # nothing to do
469       else
470         response_doc(result, mapping_registry, literal_mapping_registry, opt)
471       end
472     end
473
474   private
475
476     def receiver
477       raise NotImplementedError.new('must be defined in derived class')
478     end
479
480     def request_rpc(body, mapping_registry, literal_mapping_registry, opt)
481       request = body.request
482       unless request.is_a?(SOAPNameAccessible)
483         if request.is_a?(SOAPNil)
484           # SOAP::Lite/0.69 seems to send xsi:nil="true" element as a request.
485           request = SOAPStruct.new(request.elename)
486         else
487           raise RPCRoutingError.new("not an RPC style")
488         end
489       end
490       if @request_use == :encoded
491         request_rpc_enc(request, mapping_registry, opt)
492       else
493         request_rpc_lit(request, literal_mapping_registry, opt)
494       end
495     end
496
497     def request_document(body, mapping_registry, literal_mapping_registry, opt)
498       # ToDo: compare names with @doc_request_qnames
499       if @request_use == :encoded
500         request_doc_enc(body, mapping_registry, opt)
501       else
502         request_doc_lit(body, literal_mapping_registry, opt)
503       end
504     end
505
506     def request_rpc_enc(request, mapping_registry, opt)
507       param = Mapping.soap2obj(request, mapping_registry, nil, opt)
508       request.collect { |key, value|
509         param[key]
510       }
511     end
512
513     def request_rpc_lit(request, mapping_registry, opt)
514       request.collect { |key, value|
515         Mapping.soap2obj(value, mapping_registry, nil, opt)
516       }
517     end
518
519     def request_doc_enc(body, mapping_registry, opt)
520       body.collect { |key, value|
521         Mapping.soap2obj(value, mapping_registry, nil, opt)
522       }
523     end
524
525     def request_doc_lit(body, mapping_registry, opt)
526       body.collect { |key, value|
527         Mapping.soap2obj(value, mapping_registry, nil, opt)
528       }
529     end
530
531     def response_rpc(result, mapping_registry, literal_mapping_registry, opt)
532       if @response_use == :encoded
533         response_rpc_enc(result, mapping_registry, opt)
534       else
535         response_rpc_lit(result, literal_mapping_registry, opt)
536       end
537     end
538
539     def response_doc(result, mapping_registry, literal_mapping_registry, opt)
540       if @doc_response_qnames.size == 0
541         result = []
542       elsif @doc_response_qnames.size == 1
543         result = [result]
544       end
545       if result.size != @doc_response_qnames.size
546         raise "required #{@doc_response_qnames.size} responses " +
547           "but #{result.size} given"
548       end
549       if @response_use == :encoded
550         response_doc_enc(result, mapping_registry, opt)
551       else
552         response_doc_lit(result, literal_mapping_registry, opt)
553       end
554     end
555
556     def response_rpc_enc(result, mapping_registry, opt)
557       soap_response =
558         @rpc_method_factory.create_method_response(@rpc_response_qname)
559       if soap_response.have_outparam?
560         unless result.is_a?(Array)
561           raise RPCRoutingError.new("out parameter was not returned")
562         end
563         outparams = {}
564         i = 1
565         soap_response.output_params.each do |outparam|
566           outparams[outparam] = Mapping.obj2soap(result[i], mapping_registry,
567             nil, opt)
568           i += 1
569         end
570         soap_response.set_outparam(outparams)
571         soap_response.retval = Mapping.obj2soap(result[0], mapping_registry,
572           nil, opt)
573       else
574         soap_response.retval = Mapping.obj2soap(result, mapping_registry, nil,
575           opt)
576       end
577       soap_response
578     end
579
580     def response_rpc_lit(result, mapping_registry, opt)
581       soap_response =
582         @rpc_method_factory.create_method_response(@rpc_response_qname)
583       if soap_response.have_outparam?
584         unless result.is_a?(Array)
585           raise RPCRoutingError.new("out parameter was not returned")
586         end
587         outparams = {}
588         i = 1
589         soap_response.output_params.each do |outparam|
590           outparams[outparam] = Mapping.obj2soap(result[i], mapping_registry,
591             XSD::QName.new(nil, outparam), opt)
592           i += 1
593         end
594         soap_response.set_outparam(outparams)
595         soap_response.retval = Mapping.obj2soap(result[0], mapping_registry,
596           soap_response.elename, opt)
597       else
598         soap_response.retval = Mapping.obj2soap(result, mapping_registry,
599           soap_response.elename, opt)
600       end
601       soap_response
602     end
603
604     def response_doc_enc(result, mapping_registry, opt)
605       (0...result.size).collect { |idx|
606         ele = Mapping.obj2soap(result[idx], mapping_registry, nil, opt)
607         ele.elename = @doc_response_qnames[idx]
608         ele.qualified = @doc_response_qualified[idx]
609         ele
610       }
611     end
612
613     def response_doc_lit(result, mapping_registry, opt)
614       (0...result.size).collect { |idx|
615         ele = Mapping.obj2soap(result[idx], mapping_registry,
616           @doc_response_qnames[idx])
617         ele.encodingstyle = LiteralNamespace
618         ele.qualified = @doc_response_qualified[idx]
619         ele
620       }
621     end
622
623     def check_style(style)
624       unless [:rpc, :document].include?(style)
625         raise ArgumentError.new("unknown style: #{style}")
626       end
627     end
628
629     # nil means oneway
630     def check_use(use)
631       unless [:encoded, :literal, nil].include?(use)
632         raise ArgumentError.new("unknown use: #{use}")
633       end
634     end
635   end
636
637   class ApplicationScopeOperation < Operation
638     def initialize(soapaction, receiver, name, param_def, opt)
639       super(soapaction, name, param_def, opt)
640       @receiver = receiver
641     end
642
643   private
644
645     def receiver
646       @receiver
647     end
648   end
649
650   class RequestScopeOperation < Operation
651     def initialize(soapaction, receiver_factory, name, param_def, opt)
652       super(soapaction, name, param_def, opt)
653       unless receiver_factory.respond_to?(:create)
654         raise TypeError.new("factory must respond to 'create'")
655       end
656       @receiver_factory = receiver_factory
657     end
658
659   private
660
661     def receiver
662       @receiver_factory.create
663     end
664   end
665 end
666
667
668 end
669 end
Note: See TracBrowser for help on using the browser.