#contents

*はじめに [#dbc333ce]

soap4rではwsdlを指定することで、Webサービスのoperation呼び出しをRubyオブジェクトのメソッド呼び出しにマッピングさせることができます。

その際、

 service = SOAP::WSDLDriverFactory.new(wsdlLocation).create_rpc_driver

と記述します。今回はWSDLDriverFactory.newの部分を読むことにしましょう。

なお、今回対象としたバージョンは1.5.5です。soap4rはまだまだ開発途上なのかバージョンによって結構コードが変わるようです。

*SOAP::WSDLDriverFactory(soap/wsdlDriver.rb) [#f229fe34]

ではまずSOAP::WSDLDriverFactoryです。initializeメソッドは以下のようになっています。

 def initialize(wsdl)
   @wsdl = import(wsdl)
   @methoddefcreator = WSDL::SOAP::MethodDefCreator.new(@wsdl)
 end

1行目のimportメソッドが解析処理をしていると考えられるのでimportメソッドに進みましょう。

 def import(location)
   WSDL::Importer.import(location)
 end

というわけで、WSDL::Importerに%%丸投げ%%委譲しています。Rubyを知らない人のために書いておくとRubyではメソッドの最後の式がメソッドの戻り値になります。つまり、

 def one
   1
 end

と書けばoneメソッドの戻り値は1になります。

*WSDL::Importer(wsdl/importer.rb) [#u45cc0a3]

では、WSDL::Importer.importです。

 def self.import(location, originalroot = nil)
   new.import(location, originalroot)
 end

Rubyを知らない人のためにその2ですがRubyではクラスメソッドは上記のようにself.メソッド名で定義します((より正確に言うとクラスメソッドはクラスオブジェクトの特異メソッドとして定義されます。詳しい説明は[[こちら>http://www.ruby-lang.org/ja/man/?cmd=view;name=%A5%AF%A5%E9%A5%B9%A1%BF%A5%E1%A5%BD%A5%C3%A5%C9%A4%CE%C4%EA%B5%C1#a.a5.af.a5.e9.a5.b9.a5.e1.a5.bd.a5.c3.a5.c9.a4.ce.c4.ea.b5.c1]]をどうぞ))。

さて本題に戻って、WSDL::Importerオブジェクトを作成し、インスタンスメソッドのimportメソッドを呼び出しています。WSDL::Importer自身にinitializeメソッド、importメソッドはないのでスーパークラスのWSDL::XMLSchema::Importerに移りましょう。

*WSDL::XMLSchema::Importer(wsdl/xmlSchema/importer.rb) [#jcb4a133]

まず、initializeメソッド。@web_clientをnilにしています。

 def initialize
   @web_client = nil
 end

次に、importメソッド。

 def import(location, originalroot = nil)
   unless location.is_a?(URI)
     location = URI.parse(location)
   end
   content = parse(fetch(location), location, originalroot)
   content.location = location
   content
 end

locationがURIオブジェクトでないならURIオブジェクトにしています。ここら辺は変数に型のないRubyならではないでしょうか?Javaだったら、

 import(String location) {
     import(new URI(location));
 }

みたいにオーバーロードを用意することになることでしょう。

さて読解に戻って、locationで指定されたwsdlをfetchし、それを引数にしてparseメソッドを呼び出しています。

まず、fecthメソッドを見てみましょう。fetchメソッドではlocationがローカルファイルシステム上にあるかどうかで処理が分かれています。ローカルファイルシステム上にある場合は見たままなのでそれ以外の場合(httpの場合)についてコードを追いかけてみましょう。

 client = web_client.new(nil, "WSDL4R")
 client.proxy = ::SOAP::Env::HTTP_PROXY
 client.no_proxy = ::SOAP::Env::NO_PROXY
 if opt = ::SOAP::Property.loadproperty(::SOAP::PropertyName)
   ::SOAP::HTTPConfigLoader.set_options(client,
   opt["client.protocol.http"])
 end
 content = client.get_content(location)

web_clientはwsdlをhttp経由で取得するためのクラスを返すメソッドです。具体的には以下のようになっています。

 def web_client
   @web_client ||= begin
         require 'http-access2'
         if HTTPAccess2::VERSION < "2.0"
           raise LoadError.new("http-access/2.0 or later is required.")
         end
         HTTPAccess2::Client
       rescue LoadError
         warn("Loading http-access2 failed.  Net/http is used.") if $DEBUG
         require 'soap/netHttpClient'
         ::SOAP::NetHttpClient
     end
   @web_client
 end

まず、http-access2のロードおよびバージョンの確認を行っています。http-access2はRubyに標準で含まれていないため、ロードできなかった場合は独自のクラスを返しています。Rubyを知らない人のためにその3、Rubyは制御構造も式なので最後に書かれているものが値として返されます。今回はSOAP::NetHttpClientが返るものとします。

さてfetchメソッドに戻って次の行、環境変数からプロキシを設定している雰囲気です。SOAP::Envがいつの間に設定されたかを確認すると、soap/wsdlDriver→soap/rpc/driver→soap/soapと読み込まれ、soap/soap.rbで定義されています。

次のSOAP::Property.loadproperty(SOAP::PropertyName)ではプロパティファイル(soap/property)を読み込んでオプションをセットしています。プロパティファイルはないのでオプションはセットされませんがなかなか興味深いコードなので眺めてみましょう。

*SOAP::Property(soap/property.rb) [#ea4b93a6]

**プロパティの読み込みとセット [#yefd5c9d]

さて、SOAP::Property.loadpropertyメソッドですが、SOAP::Propertyオブジェクトをnewし、同名のインスタンスメソッドを呼び出しています。インスタンスメソッドの方のloadpropertyメソッドはこんな感じ。

 def loadproperty(propname)
   return loadpropertyfile(propname) if File.file?(propname)
   $:.each do |path|
     if File.file?(file = File.join(path, propname))
       return loadpropertyfile(file)
     end
   end
   nil
 end

カレント以下にsoap/propertyファイルがあればそれを、なければライブラリ検索パス($:にはライブラリ検索パスの配列が格納されています)からsoap/propertyを読み込んでいます。ファイルが見つかった場合、loadpropertyfileメソッドでファイルを開いた後、loadメソッドでプロパティを読み込んでいます。プロパティのセットは、

 self[key] = value

なので、次の[]=メソッドが呼ばれます。

 def []=(name, value)
   name_pair = name_to_a(name).freeze
   hooks = assign(name_pair, value)
   hooks.each do |hook|
     hook.call(name_pair, value)
   end
   value
 end

name_to_aメソッド  ではnameはStringのはずなので以下のコードが実行され、"aaa.bbb.ccc"のような文字列が["aaa", "bbb", "ccc"]のような配列に変換されるようです。以後、nameとして"aaa.bbb.ccc"が指定されているとして読解を進めます。

 name.scan(/[^.??]+(?:??.[^.??])*/)	# split with unescaped '.'

name.split(/?./)じゃ駄目なのでしょうか?

さて、[]=メソッドに戻って続きを見るとassignメソッドが呼ばれています。assignメソッドはこんな感じ。

 def assign(ary, value)
   ref = self
   hook = NO_HOOK
   ary[0..-2].each do |name|
     key = to_key(name)
     hook += ref.local_hook(key, false)
     ref = ref.deref_key(key)
   end
   last_key = to_key(ary.last)
   ref.local_assign(last_key, value)
   hook + ref.local_hook(last_key, true)
 end

ary[0..-2]ではaryとして["aaa", "bbb", "ccc"]を想定しているので、配列の一番最後を除いて"aaa", "bbb"が順にnameにセットされブロックが実行されます。to_keyメソッドは単純にnameを小文字化しているだけのようです。次のref.local_hookメソッドは今までの流れの中でフックが設定されるコードはなかったので無視することにしましょう。というわけで、deref_keyメソッドを眺めます。

 def deref_key(key)
   check_lock(key)
   ref = @store[key] ||= self.class.new
   unless propkey?(ref)
     raise ArgumentError.new("key `#{key}' already defined as a value")
   end
   ref
 end

@storeはHashでkeyとvalueの対応が登録されているようです。初回なのでself.class.newの方、つまり、新しいSOAP::Propertyオブジェクトが作られます。これが"aaa"と"bbb"について行われるので、

 SOAP::Property(root): "aaa" => SOAP::Property("aaa")
 SOAP::Property("aaa"): "bbb" => SOAP::Property("bbb")

みたいな階層構造が構築され、ループが終了した時点でrefにSOAP::Property("bbb")が設定されます。@storeへの格納はいつの間に行われたんだ?と思ったら、||=なのでオブジェクトの生成と同時に格納されるのですね:-)

さて、assignメソッドに戻ると配列の最後"ccc"についてlocal_assignメソッドが呼び出されています。

 def local_assign(key, value)
   check_lock(key)
   if @locked
     if propkey?(value)
       raise FrozenError.new("cannot add any key to locked property")
     elsif propkey?(@store[key])
       raise FrozenError.new("cannot override any key in locked property")
     end
   end
   @store[key] = value
 end

ロックはされていないはずなのでさくっと無視すると、selfは先ほど説明したようにSOAP::Property("bbb")なので

 SOAP::Property("bbb"): "ccc" => value

が設定されます。[]=メソッドに戻ると登録されているフックが呼び出されるようですがフックは登録されていないはずなので無視しましょう。

**プロパティのゲット [#n18ca000]

続いてプロパティのゲットを見てみましょう。[]メソッドはこんな感じです。

 def [](name)
   referent(name_to_a(name))
 end

で、referentメソッド。

 def referent(ary)
   ary[0..-2].inject(self) { |ref, name|
     ref.deref_key(to_key(name))
   }.local_referent(to_key(ary.last))
 end

injectメソッドはリファレンスマニュアルの説明によると、

 最初に初期値 init と self の最初の要素を引数にブロック を実行します。
 2 回目以降のループでは、前のブロックの実行結果と self の次の要素を
 引数に順次ブロックを実行します。そうして最 後の要素まで繰り返し、
 最後のブロックの実行結果を返します。

とのことなので、injectメソッドが実行されていくと

 SOAP::Property(root) => SOAP::Property("aaa") => SOAP::Property("bbb")

ということで、SOAP::Property("bbb")がinjectメソッドの結果になります。なお、injectメソッドはruby 1.7 featureなので定義されていない場合はsoap/property.rbの最後で定義されています。

 unless Enumerable.instance_methods.include?('inject')

moduleにあるメソッドが含まれいるかどうかは上記のように調べるのですね。勉強になります。

次にlocal_referentメソッドです。

 def local_referent(key)
   check_lock(key)
   if propkey?(@store[key]) and @store[key].locked?
     raise FrozenError.new("cannot split any key from locked property")
   end
   @store[key]
 end

というわけで、SOAP::Property("bbb")に登録されている"ccc"の値が返されます。返される値は末端のプロパティを指定した場合はString、それ以外はSOAP::Propertyとなります。

**フックの登録と呼び出し [#ob3cb227]

さて、WSDL::XMLSchema::Importer#fetchメソッドに戻るとSOAP::HTTPConfigLoader.set_optionsメソッドが呼ばれています。set_optionsメソッドでは次のようにhttpクライアントオブジェクトにプロパティが設定されています。

 client.proxy = options["proxy"]
 options.add_hook("proxy") do |key, value|
   client.proxy = value
 end

というわけで、先ほど無視したフックが設定されています。設定されている以上まじめに読解しないといけないようです。add_hookメソッド。

 def add_hook(name = nil, cascade = false, &hook)
   if name == nil or name == true or name == false
     cascade = name
     assign_self_hook(cascade, &hook)
   else
     assign_hook(name_to_a(name), cascade, &hook)
   end
 end

上記の例では"proxy"という値が渡されるのでassign_hookの方が呼ばれます。assign_hookでは例のinjectメソッドにより階層構造がたどられ(今回は"proxy"だけなのでたどられません)、local_assign_hookメソッドが呼ばれます。

 def local_assign_hook(key, cascade, &hook)
   check_lock(key)
   @store[key] ||= nil
   (@hook[key] ||= []) << [hook, cascade]
 end

というわけで"proxy"にフックが登録されます。

フックがいつ呼ばれるかを確認するためにもう一度assignメソッドを眺めます。

 def assign(ary, value)
   ref = self
   hook = NO_HOOK
   ary[0..-2].each do |name|
     key = to_key(name)
     hook += ref.local_hook(key, false)
     ref = ref.deref_key(key)
   end
   last_key = to_key(ary.last)
   ref.local_assign(last_key, value)
   hook + ref.local_hook(last_key, true)
 end

というわけで、local_hookメソッドが階層構造の途中の要素ではfalseで、末端ではtrueで呼ばれています。local_hookメソッド。

 def local_hook(key, direct)
   hooks = []
   (@self_hook + (@hook[key] || NO_HOOK)).each do |hook, cascade|
     hooks << hook if direct or cascade
   end
   hooks
 end

というわけで階層の途中ではcascade=trueと指定されている場合(階層以下のいずれかのプロパティが変更されたときにフックを呼びたいという場合にtrueにすればよさそうです)、末端の場合は常にフックが追加されることになります。これらのフックは[]=メソッドで呼ばれるのでプロパティが変わったときに新しい値を設定するという使い方ができるようです。

*SOAP::NetHttpClient(soap/netHttpClient.rb) [#d1bdf617]

ではそろそろメインの処理に戻りましょう。get_contentメソッドでlocationからwsdlを取得しているようです。get_contentメソッドでは、以下の動作をしています。

-startメソッドをブロック付きで呼び出す
--create_connectionメソッドを呼び出す
---プロキシやSSLを考慮してNet::HTTPオブジェクトを生成
--httpセッションを開始し、渡されたブロックを呼び出す。ブロックの戻り値を返す
-渡されたurlをget。結果、レスポンスが戻り値としてstartメソッドから返る
-レスポンスのbodyを返す

*WSDL::Parser(wsdl/parser.rb) [#v46a249f]

ようやくWSDLが取得できました。次に、WSDL::XMLSchema::Importer#importメソッドでは次にparseメソッドが呼ばれるのですが、parseメソッドはWSDL::Importerでオーバーライドされているのでそちらを見てみましょう。

 def parse(content, location, originalroot)
   opt = {
     :location => location,
     :originalroot => originalroot
   }
   begin
     WSDL::Parser.new(opt).parse(content)
   rescue WSDL::Parser::ParseError
     super(content, location, originalroot)
   end
 end

というわけでWSDL::Parserに処理が移っています。なお、originalrootの初期値はnilです。

まず、initializeメソッドです。

 def initialize(opt = {})
   @parser = XSD::XMLParser.create_parser(self, opt)
   @parsestack = nil
   @lastnode = nil
   @ignored = {}
   @location = opt[:location]
   @originalroot = opt[:originalroot]
 end

で、parseメソッド。

 def parse(string_or_readable)
   @parsestack = []
   @lastnode = nil
   @textbuf = ''
   @parser.do_parse(string_or_readable)
   @lastnode
 end

initializeメソッドでselfを渡しているのでXSD::XMLParser#do_parseメソッドが呼ばれて解析が開始されるとWSDL::Parserにコールバックされると考えられます。wsdl/parser.rbの下の方を見るとstart_element, characters, end_elementとSAXっぽいメソッドが見つかります。とりあえず、XSD::XMLParserを見てみることにしましょう。

*XSD::XMLParser(xsd/xmlparser.rb) [#f7558157]

create_parserメソッドは次の通りです。

 def create_parser(host, opt)
   XSD::XMLParser::Parser.create_parser(host, opt)
 end

う〜ん、何でこんな構造になっているのでしょう?ともかくXSD::XMLParser::Parser.create_parserメソッド。

 def self.create_parser(host, opt = {})
   @@parser_factory.new(host, opt)
 end

@@parser_factoryは?と上を見るとnilになっています。このままではnilにnewすることになってしまいます。いつの間に@@parser_factoryが定義されるのかはxsd/xmlparser.rbに戻って下の方を見るとわかります。

 [
   'xsd/xmlparser/xmlparser',
   'xsd/xmlparser/xmlscanner',
   'xsd/xmlparser/rexmlparser',
 ].each do |lib|
   begin
     require lib
     loaded = true
     break
   rescue LoadError
   end
 end

というわけで複数のparser候補を順にrequireしてみて使えるparserを使っているという感じです。上2つはロードできなかったとして、xsd/xmlparser/rexmlparser.rbを見てみましょう。

 class REXMLParser < XSD::XMLParser::Parser
   (途中省略)
   add_factory(self)
 end

add_factoryメソッド。

 def self.add_factory(factory)
   @@parser_factory = factory
 end

というわけでxsd/xmlparser/rexmlparser.rbが読み込まれると@@parser_factoryが設定されるようです。

以上のことからdo_parseメソッドが呼ばれるとREXMLParser#do_parseメソッドが呼ばれることになります。do_parseメソッド。

 def do_parse(string_or_readable)
   source = nil
   source = REXML::SourceFactory.create_from(string_or_readable)
   source.encoding = charset if charset
   # Listener passes a String in utf-8.
   @charset = 'utf-8'
   REXML::Document.parse_stream(source, self)
 end

というわけでREXMLのStreamParserを使って解析を行っています。StreamParserではタグの開始、タグの終了、内部テキストでそれぞれtag_start, tag_end, textの各メソッドがコールバックとして呼ばれるようです。

 def tag_start(name, attrs)
   start_element(name, attrs)
 end
 
 def tag_end(name)
   end_element(name)
 end
 
 def text(text)
   characters(text)
 end

スーパークラスであるXSD::XMLParser::Parserのstart_element, end_element, charactersメソッド。

 def start_element(name, attrs)
   @host.start_element(name, attrs)
 end
 
 def characters(text)
    @host.characters(text)
 end
 
 def end_element(name)
   @host.end_element(name)
 end

これで、WSDL::Parserに制御が戻ってきました。

*WSDL::Parser再び [#z604e5d7]

さて、というわけでWSDLの各タグが見つかるとWSDL::Parser#start_elementメソッドが呼ばれるので見てみることにしましょう。

 def start_element(name, attrs)
   lastframe = @parsestack.last
   ns = parent = nil
   if lastframe
     ns = lastframe.ns.clone_ns
     parent = lastframe.node
   else
     ns = XSD::NS.new
     parent = nil
   end
   attrs = XSD::XMLParser.filter_ns(ns, attrs)
   node = decode_tag(ns, name, attrs, parent)
   @parsestack << ParseFrame.new(ns, name, node)
 end

decode_tagメソッドで渡されたタグに対するノードを作成し、スタックに積んでいるようです。次にdecode_tagメソッドに進みましょう。長いのでいろいろと省略します。

 def decode_tag(ns, name, attrs, parent)
   o = nil
   elename = ns.parse(name)
   if !parent
     if elename == DefinitionsName
       o = Definitions.parse_element(elename)
       o.location = @location
     else
       raise UnknownElementError.new("unknown element: #{elename}")
     end
     o.root = @originalroot if @originalroot   # o.root = o otherwise
   else
     if elename == XMLSchema::AnnotationName
       (省略)
     else
       o = parent.parse_element(elename)
     end
     (省略)
     # node could be a pseudo element.  pseudo element has its own parent.
     o.root = parent.root
     o.parent = parent if o.parent.nil?
   end
   attrs.each do |key, value|
     attr_ele = ns.parse(key, true)
     value_ele = ns.parse(value, true)
     value_ele.source = value  # for recovery; value may not be a QName
     unless o.parse_attr(attr_ele, value_ele)
       (省略)
     end
   end
   o
 end

まず前半部分。parent => nilなのでDefinitions.parse_elementsメソッドが呼ばれます。Definitions.parse_elementメソッドでは単純にDefinitionsオブジェクトを生成して返しています。

次に、各属性について今生成したDefinitionsオブジェクトのparse_attrメソッドが呼ばれ、属性がDefinitionsオブジェクトに設定されます。decode_tagメソッドから戻るとDefinitionsオブジェクトがスタックに積まれます。

次にstart_elementメソッドが呼ばれるとスタック上にあるDefinitionsオブジェクトに対してparse_elementインスタンスメソッドが呼ばれ、definitionsタグの下にあるべきタグに対応するオブジェクトが生成されます。この処理が繰り返されることによってWSDLに対応するRubyオブジェクトツリーが構築されることになります。

*WSDL::Import(wsdl/import.rb) [#xaba967c]

ツリーの構築は大体タグに対応するRubyオブジェクトを生成し属性をしているわけですがその中で興味深いものにWSDL::Importがあります。importタグでは別のwsdlを取り込むことが可能でそれを実現するコードは以下のようになっています。

 def parse_attr(attr, value)
   case attr
   (中略)
   when LocationAttrName
     @location = URI.parse(value.source)
     if @location.relative? and !parent.location.nil? and
         !parent.location.relative?
       @location = parent.location + @location
     end
     if root.importedschema.key?(@location)
       @content = root.importedschema[@location]
     else
       root.importedschema[@location] = nil      # placeholder
       @content = import(@location)
       if @content.is_a?(Definitions)
         @content.root = root
         if @namespace
           @content.targetnamespace = @namespace
         end
       end
       root.importedschema[@location] = @content
     end
     @location
   else
     nil
   end
 end

まず、相対パスなら絶対パスに変換しています。次にすでにimport済みか確認して無駄に取得しないようにしています。まだ取得していない場合はimportメソッドを呼び出して取得した後、キャッシュしています。

次に、importメソッドです。

 def import(location)
   Importer.import(location, root)
 end

というわけで、WSDL::Importerを用いて再帰的に解析が行われています。

*おわりに [#wcec87cb]

今回はsoap4rのうち、wsdl解析処理を読みました。今回学んだこととしては以下があります。

-xmlからRubyオブジェクトを構築する方法
-利用可能なライブラリの検出、ラッピング方法
-メソッドが定義されているかの確認方法
-Ruby的なコードの読み方:-)

それではみなさんもよいコードリーディングを。


トップ   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS