【第1シリーズ 第2回】FlexでWebサービスクライアントを作成する
第1シリーズ RESTful Webサービスとクライアントアプリケーション
第2回 FlexでWebサービスクライアントを作成する
こんにちは、hiroです。第1回から少々時間があいてしまいました。理由(いいわけ)はただ一つ!忙しかったからです。こうして言い訳する大人が出来上がるわけですね。だめだなぁ。
さて、今回は、前回作成した REST インターフェイスの Web サービスにいくつかの機能追加を行い、クライアントアプリケーションからアクセスすることにしようと思います。前回の最後に Silverlight でクライアント開発を行うとしましたが、思うところあって、Flex で開発することにしました。
1. ツールのダウンロード
まず、ツールのダウンロードから。今回、Flex SDK を使って、クライアントアプリケーションを開発しました。以下の URL からダウンロードしてお使いください。なお、Flex SDK は、別途 Java SE 5 以上を必要としますので、あわせてダウンロードしてください(第1回の時にインストールした方は再度インストールする必要はありません)。
http://www.adobe.com/products/flex/flexdownloads/
Flex SDK はコマンドラインベースのコンパイラ・リンカのセットなので、コマンドラインベースでの開発がおっくうな方は、FlashDevelop などの統合開発環境をお使いください。FlashDevelop のページは以下の URL からどうぞ。
http://www.flashdevelop.org/
2. サービスを定義する
さて、第2回では、第1回で作成した「顧客一覧の参照」機能だけだったサービスに「顧客情報の保存」機能を追加しようと思います。
● 顧客情報を保存する
a) 顧客1件の情報をデータベース中に「保存」する。
b) 「保存」とは「新規追加」と「更新」である。
c) 顧客1件のスキーマは、第1回で定義したスキーマを使用する。
d) 次の URL に対し、HTTP の POST メソッドでアクセスする。
http://<サーバ:ポート>/Yumewaza1/resource/customer
c) POST でアップロードするデータは、XML文字列であり、以下の構造をとる。
<customer id="<ID>"> <name><名前></name> <birthday><生年月日></birthday> <address><住所></address> <sex><性別></sex> </customer> ※ ただし、「新規追加」時は id 属性は指定しない。
実にシンプルですね。これで、顧客マスタ編集アプリケーションが作れそうです。
3. サービスを実装する
今回、サーバ側の実装において作成するものは以下の通りです。
(1) 顧客情報を「保存」する処理(test.CustomerResourceクラスに追加)
(2) 入力された顧客情報1件を表すクラス:test.CustomerInput
(1) について、今回はクラスに追加するだけですので、メソッド部分のみを抽出して以下に掲載します。
1: @POST 2: @Produces("application/xml") 3: @Consumes("application/xml") 4: public Response store(CustomerInput customer) { 5: if (customer == null && !customer.isValid()) { 6: return Response.notModified().build(); 7: } 8: 9: String insertSql = 10: "INSERT INTO CUSTOMER ( " + 11: " NAME, " + 12: " BIRTHDAY, " + 13: " ADDRESS, " + 14: " SEX) " + 15: "VALUES (" + 16: " ?, ?, ?, ?)"; 17: 18: String updateSql = 19: "UPDATE CUSTOMER SET " + 20: " NAME = ?, " + 21: " BIRTHDAY = ?, " + 22: " ADDRESS = ?, " + 23: " SEX = ? " + 24: "WHERE " + 25: " ID = ?"; 26: 27: Database database = new DatabaseImpl(); 28: 29: try { 30: 31: if (customer.id == 0) { // id が 0 の場合、新規追加とする 32: 33: boolean result = database.update(insertSql, 34: customer.name, 35: customer.birthday, 36: customer.address, 37: customer.getSex()); 38: 39: if (result) { 40: database.commit(); 41: return Response.ok().build(); 42: } 43: else { 44: database.rollback(); 45: return Response.serverError().build(); 46: } 47: 48: } 49: else { // id が 0 より大きい場合、更新とする 50: 51: boolean result = database.update(updateSql, 52: customer.name, 53: customer.birthday, 54: customer.address, 55: customer.getSex(), 56: customer.id); 57: 58: if (result) { 59: database.commit(); 60: return Response.ok().build(); 61: } 62: else { 63: database.rollback(); 64: return Response.serverError().build(); 65: } 66: 67: } 68: 69: } 70: catch (SQLException ex) { 71: database.rollback(); 72: return Response.serverError().build(); 73: } 74: finally { 75: database.close(); 76: } 77: }
まず、「保存」とは、「新規保存」と「上書き保存」のどちらもさします。しかし、SQL 文は新規保存に相当する INSERT 文と、上書き保存に相当する UPDATE 文が分かれているため、コードレベルでは処理を分けて実装する必要があります。
このメソッドの流れを上部から見ていきましょう。まず、4行目、このメソッドの入り口と出口を見ましょう。引数の CustomerInput クラスは、クライアント側から送信された顧客1件分のXMLデータをバインドしたオブジェクトの型です。クライアントからXMLデータを POST メソッドで送信されると、このメソッドがコールされ、XMLデータは CustomerInput オブジェクトに入った状態で渡されます。出口の Response クラスの完全なクラス名は javax.ws.rs.core.Response です。これは、HTTP レスポンス情報を作成するためのクラスです。6行目にあるように、notModified() メソッドの実行で、このレスポンスを「304 Not Modified」にすることができます。
9 – 16 行目は新規追加時の SQL 文で、18 – 25 行目は更新時の SQL 文です。新規追加時の SQL 文中には、ID フィールドの値指定がありません。これは、ID フィールドが AUTO_INCREMENT 属性を持ったフィールドのため、ID 値が自動付与されるためです。そもそも、ID をコード側で指定するとなれば、トランザクションにおいて複雑なロック処理が必要になりますが、この記事の趣旨からすると本質ではありませんので省略するためにもこの属性をつけてあります。
31 行目の if 文により、新規追加と更新の処理を分けています。CustomerInput クラスの id フィールドの値が 0 であれば新規追加となる旨がコメントに書かれています。POST で送信される XML においては、新規追加時は id 属性は指定されないことになっていますが、CustomerInput オブジェクトにバインドされる際、id 属性が存在しなければ、クラスの定義に含まれる初期値がそのまま残ります。これによって、データベースの制約として、ID フィールドは、1 以上の値を取るということになります。
33 – 46 行目が新規追加時の処理です。データベースアクセスを容易にするライブラリ「DBTackle」を使用し、INSERT 文を実行しています。パラメータ指定において、37行目で getSex() メソッドを使用している理由は、パラメータ指定においては、XML で指定された文字列ではなくて、Sex オブジェクトが必要なためです。getSex() メソッド内では、XML で指定された文字列を、Sex 列挙子のオブジェクトに変換しています。41 行目は、レコードの追加に成功した際のレスポンスを作成しています。ok() メソッドで、HTTP レスポンスコードを 200 としています。
49 – 67行目が更新時の処理です。新規追加時と同様にパラメータを指定して UPDATE 文を実行します。成功すれば result が true となっていますので、成功時はレスポンスコードを 200 に、失敗時は 304 としています。
70 – 73行目は、データベースエラーとなった場合の例外処理です。これは、外部から見るとサーバの内部エラーですので、レスポンスコード 500 となるよう、serverError() メソッドを使用しています。
次に、(2) CustomerInput クラスの定義です。以下のコードをご覧ください。
1: package test; 2: 3: import java.util.Date; 4: import javax.xml.bind.annotation.*; 5: 6: @XmlRootElement(name = "customer") 7: public class CustomerInput { 8: @XmlAttribute 9: public int id = 0; 10: 11: public String name; 12: 13: public Date birthday; 14: 15: public String address; 16: 17: public String sex; 18: 19: @XmlTransient 20: public Sex getSex() { 21: return Sex.valueOf(this.sex); 22: } 23: 24: @XmlTransient 25: public boolean isValid() { 26: return 27: this.id >= 0 && 28: this.name != null && this.birthday != null && 29: this.address != null && this.sex != null; 30: } 31: }
このクラスは、第1回で定義した Customer クラスとほぼ同様です。これは、私の経験ですが、データベースや XML とのバインディングを行うに当たり、そのバインディングの方向によって、やれフィールドでは対応できないだの、やれメソッドでないと動的に生成できないだのといった諸問題の種類が異なるケースとよく出会います。そのため、入力用の構造と出力用の構造に求められる対応も若干異なってきます。それを1つのクラス上に表現すると、ソースコードの見通しが悪くなる傾向にあると感じています。ですので、Customer は出力用、CustomerInput は入力用として、それぞれ XML とのバインディングを行っています。
たとえば、17 行目は、顧客情報を表す XML では文字列で指定されることを表します。つまり XML 文字列から、Java の String 型へのバインドを前提とした構造です。しかしクエリ発行時のパラメータとしては、Sex オブジェクトが求められるため、19 – 22 行目に文字列表現から Sex オブジェクトに変換する getSex() メソッドを定義しています。このメソッドは、JAXB のバインディング機能の対象外とするため、@XmlTransient アノテーションを付与しています。
さぁ、これで、サーバ側の実装は完了です。
4. クライアントを定義する
お待たせしました。今回の目玉は何と言っても、クライアントアプリケーションから REST インターフェイスにアクセスすることです。そして、顧客マスタの編集を想定したアプリケーションを作成し、アプリケーションのステートをクライアント側で管理することです。
今回、Flex SDK 3.3 を使用して開発します。Flex では画面の構成を MXML という XML をベースとした宣言型の言語で記述します。Flex の標準ライブラリ中に含まれる、mx.controls パッケージ内のコントロールをマークアップで記述でき、ユーザーインターフェイスの開発が行えます。
前回 Silverlight で実装しますと言いましたが、Silverlight、もっと言えば、Microsoft が新しくユーザーインターフェイス構築ライブラリとして定義している Windows Presentation Foundation (WPF)のユーザーインターフェイス記述言語 XAML も、これまた XML をベースとした言語です。どうやら最近はアプリケーションのユーザーインターフェイスは XML で記述するもののようです。
話がそれました。以下にアプリケーションの完成イメージを示します。
画面上部から順に説明します。クライアント領域には、「顧客一覧」と「顧客編集」というエリアがあります。顧客一覧エリアには、グリッド状のリストとデータをロードするためのボタンがあります。
顧客編集エリアには、顧客1件のデータを表示・編集するために、以下のコントロールが配置されています。
(1) IDを表示させるラベルを表す Label コントロール
(2) 名前を表示・入力させるテキスト入力枠を表す TextInput コントロール
(3) 誕生日を表示・入力させる入力枠を表す DateField コントロール
(4) 住所を表示・入力させるテキスト入力枠を表す TextInput コントロール
(5) 性別を表示・入力させるコンボボックスを表す ComboBox コントロール
また、顧客データを新規作成するための新規ボタンと、入力された内容をサーバ側に保存するための保存ボタンがあります。
このアプリケーションは、ブラウザ上で稼働していますが、一度ブラウザにロードされると、ページの更新なしにサーバ側とデータの送受信が行えます。そこで、顧客データの新規作成時や更新時にサーバ対し、XML の送受信だけで通信を行います。
5. クライアントを実装する
それでは、実装に入りましょう。実装を行うのは以下の2つ。
(1) ユーザーインターフェイスを定義する MXML ファイル:Main.mxml
(2) REST インターフェイスにアクセスするコードを定義する ActionScript 3 ファイル:Service.as
Flex で開発するに当たり、ユーザーインターフェイスの定義は、主に、データとのバインディングを中心に記述することとなります。データバインドには大きく分けて 2 種類あります。コレクションなどのオブジェクトを(A)コントロールのデータソースとしてバインドする方法と、(B)コントロール間のプロパティのバインドです。前者は外部データソースからコントロール内にデータを取りこみ表示させる方法であり、後者はエンドユーザの操作によるコントロール間のデータの受け渡しに関するコードを簡潔に記述する方法です。
最初に顧客一覧を表示するリストを定義します。DataGrid コントロールで一覧表示を行います。1行が顧客1件分のデータを表し、カラムで1件中の各項目を表示します。ここではこのコントロールの ID を customersGrid としています。以後、ActionScript 3 のコード中からは、この名称を変数名としてアクセス可能です。
1: <mx:DataGrid id="customersGrid" width="100%" > 2: <mx:columns> 3: <mx:DataGridColumn dataField="@id" headerText="ID" /> 4: <mx:DataGridColumn dataField="name" headerText="名前" /> 5: <mx:DataGridColumn dataField="birthday" headerText="誕生日" /> 6: <mx:DataGridColumn dataField="address" headerText="住所" /> 7: <mx:DataGridColumn dataField="sex" headerText="性別" /> 8: </mx:columns> 9: </mx:DataGrid>
DataGridColumn は、各カラムの定義を示しています。注目すべきは dataField 属性です。これが、コレクションとコントロールとのデータバインドの要です。バインドされる値は、前回定義した顧客一覧を示す XML の customer タグの配列です。つまり、DataGrid にとって、顧客情報1件ごとに XML 文字列のインスタンスがバインドされ、customer タグがルートとなります。dataField="@id" は、customer タグの id 属性の値をバインドするという意味であり、dataField="name" は、customer タグの子タグである name タグの値をバインドするという意味となります。
次に、「ロード」ボタンの宣言を見てみましょう。
1: <mx:HBox horizontalAlign="right"> 2: <mx:Button id="loadButton" label="ロード" click="loadCustomers();" /> 3: </mx:HBox>
ボタンが押下された時のイベント処理を記述する属性に click がありますが、ここで、loadCustomers() というメソッドを実行しています。このメソッド中では、test.Service クラスのインスタンスを生成し、loadCustomers(DataGrid) というメソッドを実行しています。この Service.loadCustomers() メソッドは引数に DataGrid を取り、ネットワークを介して取得した顧客情報の XML データを DataGrid にバインドしています。以下にそのメソッドのコードを示します。
1: public function loadCustomers(dataGrid : DataGrid) : void 2: { 3: var loader : URLLoader = new URLLoader(); 4: var request : URLRequest = new URLRequest(this.base + "/customer"); 5: 6: loader.addEventListener(Event.COMPLETE, function (event : Event) : void { 7: dataGrid.dataProvider = null; 8: dataGrid.dataProvider = XML(loader.data).descendants("customer"); 9: loader.close(); 10: }); 11: 12: try { 13: loader.load(request); 14: } 15: catch (error : Error) { 16: trace(error); 17: } 18: }
このコードは見た目の順序と実際の実行順序が異なりますので、実行される順に見ていきましょう。3 行目で URLLoader クラスのインスタンスを生成しています。HTTP プロトコルで通信するためのプロトコルラッパークラスです。以後、このクラスを用いて、REST インターフェイスの Web サービスを利用します。
4 行目では、リクエストを表す URLRequest クラスのインスタンスを生成しています。コンストラクタにはリソースの URL を指定します。base フィールドは、Web サービスのベースURLを示し、Service クラス内に定義されています。
6 – 10 行目では、URLLoader オブジェクトに、HTTP によってデータが取得された際に発生するイベントのイベントハンドラを指定しています。イベントハンドラは匿名メソッドを定義して指定しています。この点が ECMAScript の流れをくむ ActionScript らしいですね。
13 行目で load() メソッドをコールし、実際の HTTP 通信を始めます。HTTP 通信が完了し、データを取得し終わったら、先ほど指定した匿名メソッドがコールされます。
8 行目で取得した XML 構造を、customer タグの配列に変換し、DataGrid オブジェクトの dataProvider プロパティに指定しています。このプロパティに指定することで、DataGrid オブジェクト内部で、自動的にタグ等をバインドし、コントロール上に表示してくれます。
データの取得と、コントロールへのバインドは以上です。
次に、顧客情報の詳細表示を定義します。ここでは、名前項目のみを取り上げますが、他の項目も同様です。
1: <mx:HBox> 2: <mx:Label width="100" text="名前 : " textAlign="right" /> 3: <mx:TextInput width="200" text="{customersGrid.selectedItem.name}" id="nameField" /> 4: </mx:HBox>
今回、顧客リストが選択されたら、選択された 1 レコード分の情報が詳細表示エリアに表示されるように実装します。そのためには、バインディング機能を用います。text 属性の値を見てください。中括弧で囲まれた部分がデータバインドです。ここでは、customerGrid.selectedItem.name と書かれています。selectedItem が、DataGrid オブジェクトの、「現在選択されている値」を示し、そのオブジェクトは先ほど dataProvider で指定したように XML オブジェクトです。ECMAScript には、XML 構造へのアクセスを容易にするための構文が定められています。ここでは selectedItem.name とすることで、子タグである name タグの値を参照しています。
さて、最後に、データ更新を行います。「保存」ボタンの click イベントでは store() メソッドをコールしています。以下にコードを示します。
1: public function store() : void { 2: var id : int = parseInt(this.idField.text); 3: var date : Date = new Date(); 4: date.setTime(Date.parse(this.birthdayField.text)); 5: 6: var customer : XML = 7: <customer> 8: <name>{ this.nameField.text }</name> 9: <birthday>{ toDTFString(date) }</birthday> 10: <address>{ this.addressField.text }</address> 11: <sex>{ toSexString(this.sexField.selectedIndex) }</sex> 12: </customer>; 13: 14: if (id != 0) { // 更新保存 15: customer.@id = id; 16: } 17: 18: new test.Service().storeCustomer(customer, this.customersGrid); 19: }
特徴は、6 – 12 行目の、XML データの形成です。XML が構文としてサポートされており、また、タグや属性の値がバインディングによって指定できます。18 行目で、Service.storeCustomer() をコールし、XML データを引数としています。顧客一覧を示す DataGrid オブジェクトを指定しているのは、データを保存したのち、リストを更新するためです。
次に、Service.storeCustomer() メソッドを見てみましょう。
1: public function storeCustomer(customer : XML, dataGrid : DataGrid) : void 2: { 3: var saver : URLLoader = new URLLoader(); 4: var request : URLRequest = new URLRequest(this.base + "/customer"); 5: 6: request.method = "POST"; 7: request.contentType = "application/xml"; 8: request.data = customer; 9: 10: saver.addEventListener(Event.COMPLETE, function (event : Event) : void { 11: loadCustomers(dataGrid); 12: }); 13: 14: try { 15: saver.load(request); 16: } 17: catch (error : Error) { 18: trace(error); 19: } 20: }
取得の時と同様、 URLLoader オブジェクト、URLRequest オブジェクトを用意します。特徴は 6 行目。HTTP メソッドを「POST」にしているところです。Web サービスの定義で @POST というアノテーションを付与しました。つまり、顧客情報1件を保存するには、POST メソッドでサーバに送信しなければなりません。10 行目は HTTP プロトコルによる送受信が終了した際のイベントハンドラを定義します。11 行目で loadCustomers() をコールしているため、顧客情報の保存が完了したのち、顧客情報一覧リストが更新されます。
とても駆け足でお送りしました第 2 回。とても長い文章にお付き合いいただきありがとうございました。おかげでこんなアプリケーションが作れました。
本記事で作成したソースコード等は、以下のリンクからダウンロードしていただけます。なお、この記事で作成したソースコード、ライブラリ等の使用により起こった事に、筆者はなんら責任を負わないものとします。自己責任でお使いください。
ダウンロード
第 3 回は…まだ決めていません。また記事を書くころに内容を変えてしまいそうなので(笑)。