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

root/trunk/lib/soap/rpc/router.rb

Revision 2005, 20.0 kB (checked in by nahi, 1 year ago)
  • introduce SOAP::RPC::MethodDef? and use it instead of complex Array structure.
  • param type 'in', 'out', 'retval' -> :in, :out, :retval
  • 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 |param|
221       param = MethodDef.to_param(param)
222       if param.io_type == SOAPMethod::IN
223         return param.qname
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_response_qnames = []
427         param_def.each do |param|
428           param = MethodDef.to_param(param)
429           case param.io_type
430           when SOAPMethod::IN
431             @doc_request_qnames << param.qname
432           when SOAPMethod::OUT
433             @doc_response_qnames << param.qname
434           else
435             raise ArgumentError.new(
436               "illegal inout definition for document style: #{param.io_type}")
437           end
438         end
439       end
440     end
441
442     def request_default_encodingstyle
443       (@request_use == :encoded) ? EncodingNamespace : LiteralNamespace
444     end
445
446     def response_default_encodingstyle
447       (@response_use == :encoded) ? EncodingNamespace : LiteralNamespace
448     end
449
450     def call(body, mapping_registry, literal_mapping_registry, opt)
451       if @request_style == :rpc
452         values = request_rpc(body, mapping_registry, literal_mapping_registry,
453           opt)
454       else
455         values = request_document(body, mapping_registry,
456           literal_mapping_registry, opt)
457       end
458       result = receiver.method(@name.intern).call(*values)
459       return result if result.is_a?(SOAPFault)
460       if @response_style == :rpc
461         response_rpc(result, mapping_registry, literal_mapping_registry, opt)
462       elsif @doc_response_qnames.empty?
463         # nothing to do
464       else
465         response_doc(result, mapping_registry, literal_mapping_registry, opt)
466       end
467     end
468
469   private
470
471     def receiver
472       raise NotImplementedError.new('must be defined in derived class')
473     end
474
475     def request_rpc(body, mapping_registry, literal_mapping_registry, opt)
476       request = body.request
477       unless request.is_a?(SOAPNameAccessible)
478         if request.is_a?(SOAPNil)
479           # SOAP::Lite/0.69 seems to send xsi:nil="true" element as a request.
480           request = SOAPStruct.new(request.elename)
481         else
482           raise RPCRoutingError.new("not an RPC style")
483         end
484       end
485       if @request_use == :encoded
486         request_rpc_enc(request, mapping_registry, opt)
487       else
488         request_rpc_lit(request, literal_mapping_registry, opt)
489       end
490     end
491
492     def request_document(body, mapping_registry, literal_mapping_registry, opt)
493       # ToDo: compare names with @doc_request_qnames
494       if @request_use == :encoded
495         request_doc_enc(body, mapping_registry, opt)
496       else
497         request_doc_lit(body, literal_mapping_registry, opt)
498       end
499     end
500
501     def request_rpc_enc(request, mapping_registry, opt)
502       param = Mapping.soap2obj(request, mapping_registry, nil, opt)
503       request.collect { |key, value|
504         param[key]
505       }
506     end
507
508     def request_rpc_lit(request, mapping_registry, opt)
509       request.collect { |key, value|
510         Mapping.soap2obj(value, mapping_registry, nil, opt)
511       }
512     end
513
514     def request_doc_enc(body, mapping_registry, opt)
515       body.collect { |key, value|
516         Mapping.soap2obj(value, mapping_registry, nil, opt)
517       }
518     end
519
520     def request_doc_lit(body, mapping_registry, opt)
521       body.collect { |key, value|
522         Mapping.soap2obj(value, mapping_registry, nil, opt)
523       }
524     end
525
526     def response_rpc(result, mapping_registry, literal_mapping_registry, opt)
527       if @response_use == :encoded
528         response_rpc_enc(result, mapping_registry, opt)
529       else
530         response_rpc_lit(result, literal_mapping_registry, opt)
531       end
532     end
533
534     def response_doc(result, mapping_registry, literal_mapping_registry, opt)
535       if @doc_response_qnames.size == 0
536         result = []
537       elsif @doc_response_qnames.size == 1
538         result = [result]
539       end
540       if result.size != @doc_response_qnames.size
541         raise "required #{@doc_response_qnames.size} responses " +
542           "but #{result.size} given"
543       end
544       if @response_use == :encoded
545         response_doc_enc(result, mapping_registry, opt)
546       else
547         response_doc_lit(result, literal_mapping_registry, opt)
548       end
549     end
550
551     def response_rpc_enc(result, mapping_registry, opt)
552       soap_response =
553         @rpc_method_factory.create_method_response(@rpc_response_qname)
554       if soap_response.have_outparam?
555         unless result.is_a?(Array)
556           raise RPCRoutingError.new("out parameter was not returned")
557         end
558         outparams = {}
559         i = 1
560         soap_response.output_params.each do |outparam|
561           outparams[outparam] = Mapping.obj2soap(result[i], mapping_registry,
562             nil, opt)
563           i += 1
564         end
565         soap_response.set_outparam(outparams)
566         soap_response.retval = Mapping.obj2soap(result[0], mapping_registry,
567           nil, opt)
568       else
569         soap_response.retval = Mapping.obj2soap(result, mapping_registry, nil,
570           opt)
571       end
572       soap_response
573     end
574
575     def response_rpc_lit(result, mapping_registry, opt)
576       soap_response =
577         @rpc_method_factory.create_method_response(@rpc_response_qname)
578       if soap_response.have_outparam?
579         unless result.is_a?(Array)
580           raise RPCRoutingError.new("out parameter was not returned")
581         end
582         outparams = {}
583         i = 1
584         soap_response.output_params.each do |outparam|
585           outparams[outparam] = Mapping.obj2soap(result[i], mapping_registry,
586             XSD::QName.new(nil, outparam), opt)
587           i += 1
588         end
589         soap_response.set_outparam(outparams)
590         soap_response.retval = Mapping.obj2soap(result[0], mapping_registry,
591           soap_response.elename, opt)
592       else
593         soap_response.retval = Mapping.obj2soap(result, mapping_registry,
594           soap_response.elename, opt)
595       end
596       soap_response
597     end
598
599     def response_doc_enc(result, mapping_registry, opt)
600       (0...result.size).collect { |idx|
601         ele = Mapping.obj2soap(result[idx], mapping_registry, nil, opt)
602         ele.elename = @doc_response_qnames[idx]
603         ele
604       }
605     end
606
607     def response_doc_lit(result, mapping_registry, opt)
608       (0...result.size).collect { |idx|
609         ele = Mapping.obj2soap(result[idx], mapping_registry,
610           @doc_response_qnames[idx])
611         ele.encodingstyle = LiteralNamespace
612         ele
613       }
614     end
615
616     def check_style(style)
617       unless [:rpc, :document].include?(style)
618         raise ArgumentError.new("unknown style: #{style}")
619       end
620     end
621
622     # nil means oneway
623     def check_use(use)
624       unless [:encoded, :literal, nil].include?(use)
625         raise ArgumentError.new("unknown use: #{use}")
626       end
627     end
628   end
629
630   class ApplicationScopeOperation < Operation
631     def initialize(soapaction, receiver, name, param_def, opt)
632       super(soapaction, name, param_def, opt)
633       @receiver = receiver
634     end
635
636   private
637
638     def receiver
639       @receiver
640     end
641   end
642
643   class RequestScopeOperation < Operation
644     def initialize(soapaction, receiver_factory, name, param_def, opt)
645       super(soapaction, name, param_def, opt)
646       unless receiver_factory.respond_to?(:create)
647         raise TypeError.new("factory must respond to 'create'")
648       end
649       @receiver_factory = receiver_factory
650     end
651
652   private
653
654     def receiver
655       @receiver_factory.create
656     end
657   end
658 end
659
660
661 end
662 end
Note: See TracBrowser for help on using the browser.