#freeze
 #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.scan(/[^.\\]+(?:\\.[^.\\])*/)	# split with unescaped '.'
 
 name.split(/?./)じゃ駄目なのでしょうか?
 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