はじめに †soap4rではwsdlを指定することで、Webサービスのoperation呼び出しをRubyオブジェクトのメソッド呼び出しにマッピングさせることができます。 その際、 service = SOAP::WSDLDriverFactory.new(wsdlLocation).create_rpc_driver と記述します。今回はWSDLDriverFactory.newの部分を読むことにしましょう。 なお、今回対象としたバージョンは1.5.5です。soap4rはまだまだ開発途上なのかバージョンによって結構コードが変わるようです。 SOAP::WSDLDriverFactory(soap/wsdlDriver.rb) †ではまず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に def one 1 end と書けばoneメソッドの戻り値は1になります。 WSDL::Importer(wsdl/importer.rb) †では、WSDL::Importer.importです。 def self.import(location, originalroot = nil) new.import(location, originalroot) end Rubyを知らない人のためにその2ですがRubyではクラスメソッドは上記のようにself.メソッド名で定義します*1。 さて本題に戻って、WSDL::Importerオブジェクトを作成し、インスタンスメソッドのimportメソッドを呼び出しています。WSDL::Importer自身にinitializeメソッド、importメソッドはないのでスーパークラスのWSDL::XMLSchema::Importerに移りましょう。 WSDL::XMLSchema::Importer(wsdl/xmlSchema/importer.rb) †まず、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) †プロパティの読み込みとセット †さて、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 が設定されます。[]=メソッドに戻ると登録されているフックが呼び出されるようですがフックは登録されていないはずなので無視しましょう。 プロパティのゲット †続いてプロパティのゲットを見てみましょう。[]メソッドはこんな感じです。 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となります。 フックの登録と呼び出し †さて、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) †ではそろそろメインの処理に戻りましょう。get_contentメソッドでlocationからwsdlを取得しているようです。get_contentメソッドでは、以下の動作をしています。
WSDL::Parser(wsdl/parser.rb) †ようやく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) †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再び †さて、というわけで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) †ツリーの構築は大体タグに対応する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を用いて再帰的に解析が行われています。 おわりに †今回はsoap4rのうち、wsdl解析処理を読みました。今回学んだこととしては以下があります。
それではみなさんもよいコードリーディングを。 |