MozillaにはWeb Servicesに関する実装がすでにあり、リッチクライアント作成環境として大いに注目を浴びています。だがしかし、開発に関するドキュメントはちゃんとそろっているとは言えず、日本語のとっかかりとなる資料もほとんどありません。
そこで、本記事では筆者がMozillaのSOAPサポートについて調べた結果を元に、簡単なクライアント/サーバを例にアプリケーションの作成方法を紹介します。
今回は以下のものを用意してください。
サーバのサンプルをPythonで実装しています。SOAPはサーバ/クライアントが対になっている必要がありますので、今回のサンプルを動かす場合は必ず用意してください。
今回はMacOSX Panther付属のPython2.3でテストを行いました。
開発の話をする前にSOAPについて、さらっと紹介します。
SOAPとはSimple Object Access Protocolの略で、コンピュータ通しで様々な情報交換を行うXMLベースのプロトコルです。XMLメッセージングと呼ばれる類いの物で、同種な物にXML-RPCという物があります。ちなみに、歴史的経緯ではXML-RPCの仕様が発展し、SOAPとなったという話があります。そのため、XML-RPCに比べるとかなり複雑な仕様になっています。Simpleとか言ってるくせに(笑
XMLメッセージングという仕組みは、サーバに対するリクエスト/レスポンスがXMLの形でやりとりをする物です。XML自体がテキストベースであるため、言語やOSを問わず利用する事が可能です。もちろんSOAPもXML-RPCも様々な言語の実装が存在していて、例えばMacOS上でPythonで書かれたクライアントを動かし、Linux上でJavaで書かれたサーバと通信する事が可能です。Mozilla上のSOAP実装もどのOSで動いているMozillaかは問わず、どのSOAPサーバであっても通信する事が可能です。ただし、SOAPサーバと言っても、正しく実装されている物に限りますが。(古いMSの実装ではSAOPの必須ヘッダーであるSOAPActionを受け取ると必ずエラーになる物があったらしい。SOAP1.2の仕様ではSOAPActionは任意という形になったが、SOAP1.1実装の物では空文字列のSOAPActionを送るものがあるので、少々注意が必要かもしれない)
また、XMLでやりとりを行うと言っても、XMLの知識が必須であるというわけではありません。多くの実装ではクライアント、サーバともにあたかも普通のアプリケーションのメソッドを叩くような感じで記述でき、SOAP実装側がそれらをXMLに変換して処理を行うような形になっています。ただし、デバッグを行う際にはXML、そしてSOAPの実装を知っていれば問題を発見するのに役に立ちますので、一通り目を通しておくと良いでしょう。
クライアントを作ってもサーバがなければ意味がありません。まずはサーバの実装を見てみましょう。
今回IBM DeveloperWorksのPython Web Servicesデベロッパー: Python SOAPライブラリーのカレンダープログラムのサンプルを扱います。サーバの実装はほぼこのままですが、若干の修正が必要になります。
一つ目の修正点はSOAP.pyの導入に関する物です。このモジュール自体、プロジェクトが凍結したとあるが、なんかアップデートされてたりします。そのためか、記事とは少々異なるインストール方法を取ります。
今回はSOAP.py 0.11.4を扱います。SOAP.pyのインストールにはfpconstとpyXMLというモジュールが必要になりますので、先にインストールします。pyXMLはMacOSX Panther上で既に入っていたので今回は解説しません。
まず、上記のURLからfpconst-0.7.0.tar.gzを落としてきてインストールします。
garynuman:~ btm$ cd tmp/src
garynuman:~/tmp/src btm$ tar zxf \
~/ftp/develop/python/fpconst-0.7.0.tar.gz
garynuman:~/tmp/src btm$ cd fpconst-0.7.0/
garynuman:~/tmp/src/fpconst-0.7.0 btm$ ls
PKG-INFO README fpconst.py setup.py
garynuman:~/tmp/src/fpconst-0.7.0 btm$ sudo python setup.py \
install
running install
running build
running build_py
creating build
creating build/lib
copying fpconst.py -> build/lib
running install_lib
garynuman:~/tmp/src/fpconst-0.7.0 btm$
次にSOAP.pyのインストールを行います。SOAP.pyのサイトからSOAPpy-0.11.4.tar.gzを落としてきてインストールします。
garynuman:~ btm$ cd tmp/src/
garynuman:~/tmp/src btm$ tar zxf \
~/ftp/develop/python/SOAPpy-0.11.4.tar.gz
garynuman:~/tmp/src btm$ cd SOAPpy-0.11.4/
garynuman:~/tmp/src/SOAPpy-0.11.4 btm$ sudo python setup.py \
install
running install
running build
running build_py
creating build
creating build/lib
creating build/lib/SOAPpy
copying SOAPpy/__init__.py -> build/lib/SOAPpy
copying SOAPpy/Client.py -> build/lib/SOAPpy
copying SOAPpy/Config.py -> build/lib/SOAPpy
copying SOAPpy/Errors.py -> build/lib/SOAPpy
copying SOAPpy/GSIServer.py -> build/lib/SOAPpy
copying SOAPpy/NS.py -> build/lib/SOAPpy
copying SOAPpy/Parser.py -> build/lib/SOAPpy
copying SOAPpy/Server.py -> build/lib/SOAPpy
copying SOAPpy/SOAP.py -> build/lib/SOAPpy
copying SOAPpy/SOAPBuilder.py -> build/lib/SOAPpy
copying SOAPpy/Types.py -> build/lib/SOAPpy
copying SOAPpy/URLopener.py -> build/lib/SOAPpy
copying SOAPpy/Utilities.py -> build/lib/SOAPpy
copying SOAPpy/version.py -> build/lib/SOAPpy
copying SOAPpy/WSDL.py -> build/lib/SOAPpy
creating build/lib/SOAPpy/wstools
copying SOAPpy/wstools/__init__.py -> build/lib/SOAPpy/wstools
copying SOAPpy/wstools/TimeoutSocket.py -> \
build/lib/SOAPpy/wstools
copying SOAPpy/wstools/UserTuple.py -> build/lib/SOAPpy/wstools
copying SOAPpy/wstools/Utility.py -> build/lib/SOAPpy/wstools
copying SOAPpy/wstools/WSDLTools.py -> build/lib/SOAPpy/wstools
copying SOAPpy/wstools/XMLname.py -> build/lib/SOAPpy/wstools
copying SOAPpy/wstools/XMLSchema.py -> build/lib/SOAPpy/wstools
running install_lib
garynuman:~/tmp/src/SOAPpy-0.11.4 btm$
以上で、SOAP.pyの導入が終わりました。
そしてもう一点の修正点は、SOAP.pyを上記の方法で導入したため、記事中とコードが変わる事です。importの方法が変わるので、それに合わせてコードを変更します。変更後のコードは以下になります。
#!/usr/bin/env python
import sys, calendar
#Import the SOAP.py machinery
#変更点(1)
import SOAPpy
CAL_NS = "http://uche.ogbuji.net/eg/ws/simple-cal"
class Calendar:
def getMonth(self, year, month):
return calendar.month(year, month)
def getYear(self, year):
return calendar.calendar(year)
# 変更点(2)
server = SOAPpy.SOAPServer(("localhost", 8888))
cal = Calendar()
server.registerObject(cal, CAL_NS)
print "Starting server..."
server.serve_forever()
変更点(1)では今回はWebServicesというパッケージを導入していないので、SOAPpyと直接インポートしています。変更点(2)では変更点(1)の変更に合わせてモジュール名を変更しています。
以上で変更は終わりです。解説はIBM DeveloperWorksをご参照ください(ひでぇ
では、さっそくサーバを動かしてみましょう。コンソールで以下のように打ちます。
garynuman:~/develop/soap/python btm$ python calendar_ws.py Starting server...
あとは、IBM DeveloperWorksのテストプログラムを動かすなりしてテストしてみましょう。ちなみに、テストプログラムはコピペしただけでは動作しません。
requestor.putheader("Content-Type", "text/plain; charset="utf-8"")
上記の部分でエスケープがちゃんとされてないのでエラーになります。
requestor.putheader("Content-Type", "text/plain; charset=\"utf-8\"")
とちゃんとエスケープすれば動作します。
さて、ここからが本題のMozilla上でのクライアント実装の話です。XULを用いてGUIを構築し、Javascriptを用いてSOAPの処理とGUIとの連携を取るという流れになります。
さっそくサンプルアプリケーションのXULプログラムとスクリーンショットをごらんください。
<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
<window id="root-window" title="soap sample"
xmlns:html="http://www.w3.org/1999/xhtml"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
>
<script src="./calendar.js" />
<groupbox id="getMonth-box">
<caption>
<label>getMonth</label>
</caption>
<hbox>
<label value="Year: " control="year" />
<textbox id="year" />
</hbox>
<hbox>
<label value="Month: " control="month" />
<textbox id="month" />
</hbox>
</groupbox>
<button id="run" label="run" onclick="runSOAP();" />
<textbox id="showCal" cols="10" rows="10" multiline="true" />
</window>
getMonthという項目の中にテキストボックスが二つあります。それぞれ、Year:とMonth:に関連づけられています。これはサーバ側のgetMonthメソッドのyearとmonthに割り当てるといった物になっています。
その下にrunと書かれたボタンがあり、これがcalendar.js内のrunSOAP関数を呼ぶトリガーになっています。
一番下のテキストボックスはサーバから返されてきた文字列を表示する部分となっています。
では、プログラムの実行部分となるcalendar.jsを見て行きましょう。
/*
calender.js
for calendar sample application by ibm developerworks
http://www-6.ibm.com/jp/developerworks/webservices/020125/j_ws-pyth5.html
written by Taro Matsuzawa AKA btm
*/
var transportURI = "http://localhost:8888/";
var CAL_NS = "http://uche.ogbuji.net/eg/ws/simple-cal"
var actionURI = "http://uche.ogbuji.net/eg/ws/simple-car";
function runSOAP(){
// create the parameters array
var params = new Array();
var x;
var method;
method = "getMonth";
x = document.getElementById("year").value;
params[0] = new SOAPParameter(parseInt(x), "year");
x = document.getElementById("month").value;
params[1] = new SOAPParameter(parseInt(x), "month");
callSOAP(method,params,showResults);
}
function callSOAP(method,params,callback){
try {
netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead");
} catch (e) {
alert(e);
return false;
}
var soapCall = new SOAPCall();
soapCall.transportURI = transportURI;
soapCall.actionURI = actionURI;
var headers = new Array();
soapCall.encode(0, method, CAL_NS, headers.length, headers, params.length, \
params);
soapCall.asyncInvoke(callback);
}
function showResults(resp, call, status){
var params = resp.getParameters(false,{});
var result = params[0].value;
document.getElementById("showCal").value = result;
}
では各部分に切って説明します。
var transportURI = "http://localhost:8888/"; var CAL_NS = "http://uche.ogbuji.net/eg/ws/simple-cal" var actionURI = "http://uche.ogbuji.net/eg/ws/simple-car";
冒頭の部分ではWebサービスが配置されているURI(transportURI)と、ターゲットとなるオブジェクトのNamespace URI(CAL_NS)と、SOAPアクセス時に指定するSOAPActionの値(actionURI)をそれぞれ指定しています。
なお、CAL_NSはSOAPサーバで指定されたものを同一ではないと動きません。サーバにアクセスしてもメソッドが見つからないという状態になってしまいます。また、通常はURIとしてはURN(Uniform Resource Names)を使うのが普通です。
function runSOAP(){
// create the parameters array
var params = new Array();
var x;
var method;
method = "getMonth";
x = document.getElementById("year").value;
params[0] = new SOAPParameter(parseInt(x), "year");
x = document.getElementById("month").value;
params[1] = new SOAPParameter(parseInt(x), "month");
callSOAP(method,params,showResults);
}
runSOAP関数ではアクセスするメソッド名(method)、引数となるパラメータの配列(params)を作成し、callSOAP関数に渡しています。パラメータの値はそれぞれDOMを使ってテキストボックスから取得しています。そして、SOAPParameterオブジェクトに名前付きで格納し、パラメータの配列にそれぞれ格納しています。。なお、parseIntとしているのはサーバにint型で送るように指定するためです。また、SOAPParameterの引数の順番が逆かと思うかもしれませんが、コレで正しいのでちょっと謎です(w
callSOAP関数の第三引数は実際のSOAPの処理を行うメソッド(SOAPResponseListener)を指定していて、複数のメソッドにアクセスするプログラムが用意に作成できるようにしています。今回は一つだけなので微妙ですが(w
function callSOAP(method,params,callback){
try {
netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead");
} catch (e) {
alert(e);
return false;
}
var soapCall = new SOAPCall();
soapCall.transportURI = transportURI;
soapCall.actionURI = actionURI;
var headers = new Array();
soapCall.encode(0, method, CAL_NS, headers.length, headers, params.length, \
params);
soapCall.asyncInvoke(callback);
}
callSOAP関数はSOAPサーバへのアクセスをするためのリクエストを作成する部分です。
まず最初のtry文で、外部へのアクセス許可(UniversalBrowserRead特権)を尋ねています。これを実行すると、以下のようなダイアログが出てきます。
ここでYesを押すと、try文を抜けて次の文へ、Noを押すと例外が出されて、catch以下に進み、例外の内容を表示して終了するという形になります。例外と言っても許可されていないという表示が出るだけです。
次にSOAPCallオブジェクトを生成し、最初に指定してあったtransportURIとactionURIをそれぞれSOAPCallオブジェクトのプロパティに入れます。
次にヘッダー(headers)を生成します。これは空の配列で問題ありません。
そして、SOAPCall.encodeで実際に送るデータをまとめあげます。引数は以下のようになっています。
<SOAPCall>.encode(<SOAPVersion>,<Method name>,<Target Object Namespace \
URI>,<Headers length>,<Headers>,<Parameters length>,<Parameters>)
SOAPVersionは今回はSOAP Version1.1を使うので0を指定します。(SOAP Version1.2を使う場合は1を指定します。) Method nameはcallSOAPに渡されたmethodを指定します。Target Object Namespace URIは冒頭で指定したCAL_NSを指定します。HeadersとParametersは配列の長さと配列自体を渡します。配列の長さを求めるには.lengthプロパティを使います。
あとはSOAPCall.asyncInvokeでSOAPのリクエストを処理するメソッド、SOAPResponseListenerに渡します。なお、SOAPResponseListenerは三つの引数を持つものでないといけません。
function <SOAPResponseListener name>(<SOAPResponse>,<SOAPcall>,<error>)
SOAPResponseオブジェクトはサーバからの返答により自動的に生成されるものです。
なお、SOAPResponseの処理が固定されているのなら、以下のように内部関数としてSOAPResponseListenerを扱う事も可能です。
SOAPCall.asyncInvoke(
function (response, soapcall, error)
{
//処理内容
}
);
では、最後のSOAPResponseを処理するSOAPResponseListenerであるshowResults関数を見てみましょう。
function showResults(resp, call, status){
var params = resp.getParameters(false,{});
var result = params[0].value;
document.getElementById("showCal").value = result;
}
まず、SOAPResponseからパラメータを取得します。<SOAPResponse>.getParametersは二つ引数を取ります。今回はRPC-styleでないメッセージを受け取るので、<SOAPResponse>.getParameters(false, {})とします。
つぎに得られたパラメータが配列なので、配列の一つ目から値を取り出します。今回はXMLドキュメントなどを返すのではなく、単純に文字列を一つ返すだけなので、.valueで取り出すだけでよいです。
最後にDOMを使ってテキストボックスの値に帰ってきた値を入れます。
これでjavascript側の実装は終了です。
では、実際に動かした例を見てみましょう。サーバを起動した状態でXULプログラムを起動します。そして適当な年と月を入れます。
次にrunを押して実行します。アクセス許可の警告が出たらYesを押して実行します。
無事、入力した年と月のデータが表示できたら成功です。
今回使用したサンプルはダウンロードできます。
アプリケーションの開発にはデバックはつきものです。今回扱ったSOAPはHTTP経由でやり取りを行うため、通常のデバッガ(MozillaならVenkman)以外にパケットモニタリングをすると比較的容易に問題点を把握できます。
パケットモニタリングソフトはUnixでよく使われる物でtcpdump、ngrep、etherealなどがありますが、MozillaではLiveHTTPHeadersというextensionを使えばMozillaがやりとりするパケットを簡単にモニタリングする事ができます。
LiveHTTPHeaderをまずインストールします。LiveHTTPHeadersのインストールページにてInstall version 0.x of LiveHTTPHeaders nowというリンクをクリックし、インストールを終えたらMozillaを再起動します。
LiveHTTPHeadersを起動するにはメニューのToolsからLiveHTTPHeadersを選択します。
画面の下にあるCaptureチェックボックスがオンにしていると、閲覧したWebサイトなどへのアクセスしたパケットが画面の中に表示されるようになります。普段はオフにしてデバッグの時だけオンにするという使い方がよいでしょう。
さて、さっそくパケットを見てみましょう。Captureチェックボックスをオンにして、クライアントを起動し、実行してみましょう。以下に成功したケースを表示します。
画面上部ではHTTPリクエストをサーバに送信している様子が表示されています。UserAgentやSOAPActionなどのヘッダーを出した後に、SOAP Bodyを投げているのがわかります。(横にスクロールしないと全体が見れないのが残念)
画面下部ではサーバからのレスポンスが表示されています。HTTP/1.x 200 OKとサーバ側が返していて、問題なくアクセスができた事を表しています。
では、今度はわざと失敗した例を見てみましょう。
これは実行後の画面ですが、クライアントのテキストフィールドに何も表示されていません。
ではLiveHTTPHeadersの画面下部を見てください。今度はHTTP/1.x 500 Internal errorとなっていて、サーバ側でエラーが発生したという表示になっています。一体何が原因なのでしょう?
こちらが出したリクエストのSOAP Bodyを見て行きます。一件何も問題ないような気がしますが、値を送る所で以下のような記述を発見しました。
yearの値は正しくint型で送られていますが、monthの値がstring型となっています。今回、わざと次のようにソースを書き換えてテストをしてみました。
- params[1] = new SOAPParameter(parseInt(x), "month"); + params[1] = new SOAPParameter(x, "month");
数字に変換できる文字列なら何でも良いのかと思い、parseIntを外してみたのですが、どうやらだめだったようです。
このようにLiveHTTPHeaders型の問題や、メソッドのNamespace URIが間違っていたとかそんな初歩的なミスを発見に役に立ちます。(ちなみに筆者は今回のサンプルを作る際、Namespace URIが間違ったの指定してるのに気づかず、小一時間悩んだあげく、LiveHTTPHeadersであっさり見つけてかなりしょんぼりしました(w)) LiveHTTPHeadersはWebアプリケーションの開発などでも活用できるのでインストールしておくと良いでしょう。ただし、パスワード見えちゃうので気をつけてください(w
今回のSOAPクライアントのサンプルはCreate Applications with Mozilla及び、Using the Mozilla SOAP APIを参考にしました。
今回作成したサンプルはメッセージのDOMの処理の例やSOAPFaultの処理が無いため、やや力不足の感がありますが、おおまかな処理の流れなどは大体把握できると思います。
Mozillaの場合、GUIをXMLで簡単に記述でき、クロスプラットフォームで動くという大きな利点があります。それを活用して、様々なリッチクライアントの世界が広がれば面白くなるのではないかと思います。
それではHappy Hacking!!