navigateToURLでAMF3を送る

最近悩んでいたのが、「Flex上のボタンをクリックしたら別ウィンドウで帳票を出す」という機能の実現方法ですが、なんとか実現できたのでまとめてみました。


AS2のころは、getURLメソッドを使用して、リクエストパラメータのvalue部分にエンコードしたXMLをつっこんでサーバにPOST、というアクロバットなやりかたをしていました。
AS3からはAMF3が使えるようになったので、できればnavigateToURLのリクエスト時に、RemoteObjectで送信するのと同じメッセージを送りたい。
なぜなら、サーバサイドでのフレームワークの処理と、ユーザーが書くハンドリング処理が、通常のRemoteObjectを使用したときと同様のコーディングで出来るからです。


T2の場合、Pageクラスの戻り値をNavigationで返せることになっています。
このため、通常のRemoteObjectを使用した通信ではAMF3をレスポンスにするNavigationを返しますが、PDFを返す場合は、PDFのバイトをレスポンスにするNavigationを返してあげれば、うまく処理できることになります。
Pageクラスをイメージで書くと、こうなります。

//通常のAMFハンドリング
@Page("amf")
public class AmfPage { 
 @Amf
 public Navigation hoge(HogeDto dto){
   return AmfResponse.to("AMF3で返すよ");
 }
}

//リクエストはAMF3、ただし別ブラウザで帳票を出すため戻り値はPDFデータ
@Page("amf.pdf")
public class AmfPdfPage { 
 @Amf
 public Navigation hoge(HogeDto dto){
   byte[] pdfData = PdfFactory.create();//なんらかのPDF生成ロジック
   return Pdf.to(pdfData);
 }
}

だいぶ分かりやすいのではないでしょうか。私は分かりやすい。


ということでこれを実現するべく、ひとまずnavigateToURLメソッドを調査しました。

 navigateToURL(request:URLRequest, window:String):void

http://livedocs.adobe.com/flex/201_jp/html/wwhelp/wwhimpl/common/html/wwhelp.htm?context=LiveDocs_Book_Parts&file=passingarguments_086_10.html
シグネチャはこんな具合です。
第一引数はURLRequest,第二引数はウィンドウ名です。ウィンドウ名は"_blank"とか"_self"とかのおなじみのやつですね。


ということでキモになるのがURLRequestですが、このような定義になっています。

URLRequestのパブリックプロパティ(Flexで使えるもの)
contentType:String    dataプロパティのコンテンツの MIME コンテンツタイプ。 
data:Object           URLリクエストで送信されるデータを含むオブジェクトです。
digest:String         Flash Player キャッシュに保存される
                     (または Flash Player キャッシュから取得される) 
                     署名付き Adobe プラットフォームコンポーネントを一意に識別するストリング。
method:String         HTTP フォーム送信メソッドを制御します。
requestHeaders:Array  HTTP リクエストヘッダの配列が HTTP リクエストに追加されます。
url:String            リクエストされる URL です。

http://livedocs.adobe.com/flex/3_jp/langref/flash/net/URLRequest.html


dataプロパティが、リクエスト時に送るデータです。dataプロパティには、

URLRequest API は、ストリング以外に、バイナリ POST と URL エンコード変数も
サポートします。データオブジェクトには、ByteArray、URLVariables、または
String オブジェクトを指定できます。

と書いてあり、このプロパティにAMF3の入ったByteArrayを入れればバイナリPOSTができるため、うまく行きそうです。


オブジェクトをAMF3にシリアライズするには、ByteArrayクラスが使用できます。こんな感じです。

var data:HogeDto = new HogeDto();//データクラス
var byteData:ByteArray = new ByteArray();
byteData.objectEncoding = ObjectEncoding.AMF3; 
byteData.writeObject(hogeDto);

JavaのObjectOutputStreamと似た感じですね。
ここまでくれば、あとはメッセージを組み立てるだけです。


RemoteObjectは、サーバ呼び出しをする場合、「RemotingMessage」というクラスを使用します。
このクラスの中に、サーバ側のあて先や呼び出しメソッドメソッド引数のデータを格納しています。
ということで、おもむろにRemotingMessageを作成してみます。

var dto:HogeDto = new HogeDto();

var remotingMessage:RemotingMessage = new RemotingMessage();
remotingMessage.destination = "amf.pdf";//あて先
remotingMessage.operation = "hoge";//呼び出しメソッド
remotingMessage.body=[dto];//メソッド引数

上記のAmfPdfPageを呼ぶ場合は、このような感じでしょう。


送信するデータもできたので、早速送信してみます。
通信相手としてS2Flex2のAMF3実装をお借りしてみました。

var dto:HogeDto = new HogeDto();

var remotingMessage:RemotingMessage = new RemotingMessage();
remotingMessage.destination = "amf.pdf";//あて先
remotingMessage.operation = "hoge";//呼び出しメソッド
remotingMessage.body=[dto];//メソッド引数

var byteData:ByteArray = new ByteArray();
byteData.objectEncoding = ObjectEncoding.AMF3; 
byteData.writeObject(remotingMessage);

var request:URLRequest = new URLRequest("amf.pdf");
request.method="POST";
request.data = byteData;
navigateToURL(request,"_blank");

結果はというと。。。だめでした。何かが足りないようです。


調べたところ、データ先頭部分の読み込みでエラーになっていました。どうやらヘッダーデータ的なものが必要なようです。
調べてみると、AMF3にはデータの先頭にエンベロープ部があり、それが必要なようです。
(AMF3の仕様書にはデータフォーマットは書いてありますが、エンベロープの記述がない。)
エンベロープに関しては、このページを参考にさせてもらいました。大変わかりやすいです。
http://cgi39.plala.or.jp/klove/w/k.cgi?page=amf3%A4%CE%A4%DE%A4%C8%A4%E1
http://osflash.org/documentation/amf/envelopes/remoting
AMFバージョンやボディ部のサイズ指定などがいるようです。

ひとまずこのエントリの内容を元にByteArrayにバイトを書いていき、送信してみました。

var dto:HogeDto = new HogeDto();

var remotingMessage:RemotingMessage = new RemotingMessage();
remotingMessage.destination = "amf.pdf";//あて先
remotingMessage.operation = "hoge";//呼び出しメソッド
remotingMessage.body=[dto];//メソッド引数

var bodyByte:ByteArray = new ByteArray();
bodyByte.objectEncoding = ObjectEncoding.AMF3; 
bodyByte.writeByte(0x0A);//AMF0配列データ型
bodyByte.writeInt(1);//AMF0配列の長さ RemotingMessage1つなので1
bodyByte.writeByte(0x11)//AMF3データ型
bodyByte.writeObject(remotingMessage);


var target:String = "amf.pdf.hoge";//あて先+メソッド
var response:String = "/1";//レスポンスID 帳票の場合はどの道使わない

var messageByte:ByteArray = new ByteArray();
messageByte.writeShort(0x03);//AMFバージョン
messageByte.writeShort(0x00);//Header数 ヘッダーはいらないので0
messageByte.writeShort(0x01);//Body数 AMF0の配列を1ついれるので1
messageByte.writeShort(target.length);//targetのサイズ
messageByte.writeUTFBytes(target);//target文字列
messageByte.writeShort(response.length);//responseのサイズ 
messageByte.writeUTFBytes(response);//response文字列
messageByte.writeInt(bodyByte.length);//シリアライズしたBodyのサイズ
messageByte.writeBytes(bodyByte);//Bodyのシリアライズデータ

var request:URLRequest = new URLRequest("amf.pdf");
request.method="POST";
request.data = messageByte;
navigateToURL(request,"_blank");

肝は、ボディデータの先頭にAMF0の配列を書くこと、その後にAMF3のデータタイプを示す0x11を書くことのようです。
(それに気がつくまでに相当かかりました。。。)
最終的には、こんな感じのコードでうまく通信できました。


ということでAMF3でポップアップに帳票を出す作戦は大成功です。やったよ母さん!