ブログトップ 記事一覧 ログイン 無料ブログ開設

Kazzzの日記 このページをアンテナに追加 RSSフィード

2014-09-15

[]ChannelServiceをローカルテストする

開発中のGAE/JのサービスではSlim3を使用している。
Slim3にはAppTester(ControllerTester)(ServletTester)と呼ばれるGAEのサービスを包含するテスタークラスが用意されており、これを保持したテストケースクラスを書くだけでGAEの各種サービスをローカルでテストできるのだが、少し設計が古くSlim3の開発時にはなかったサービス等は含まれていない。

ChannelServiceも同テスターには含まれていないので、これをテストするにはGoogleが提供しているLocal〜TestConfigと呼ばれるローカルテスト用のクラスを使う必要がある。(〜にはサービス毎に用意されている※)

ChannelServiceをテストするユニットテストクラス (LocalServiceTestCase.java)
private LocalServiceTestHelper helper = new LocalServiceTestHelper(new LocalChannelServiceTestConfig());
private ChannelManager channelManager;

@Before
public void setUp() {
    helper.setUp();
    channelManager = LocalChannelServiceTestConfig.getLocalChannelService().getChannelManager();
}
@After
public void tearDown() {
    helper.tearDown();
}
@Test
public void testSendMessage() {
    //クライアントを識別する一位の文字列でトークン生成(ここではUUID乱数を使用)
    String token = channelService.createChannel(UUID.randomUUID().toString());
    //クライアントからの接続をエミュレート
    String clientId = channelManager.connectClient(token); 
    service.sendMessage(token, "テストメッセージ");
    //送信されるであろうメッセージをアサートする
    String message = channelManager.getNextClientMessage(token, clientId );
    assertThat("テストメッセージ", equalTo(message));
}

実際のコードでは使用しないChannelManagerクラスにより、クライアント(Webブラウザ)側から接続エミュレートできるのがミソ。

本当ならここでこの機能をSlim3のAppTesterに組み込むべきなのだが、時間が無い。折を見て組み込もうと思っている。

※LocalBlobstoreServiceTestConfig, LocalTaskQueueTestConfigなど、全てのサービスのローカルテスト用クラスが用意されている。
https://developers.google.com/appengine/docs/java/tools/localunittesting/javadoc/com/google/appengine/tools/development/testing/LocalServiceTestConfig

2014-08-09

[]コントローラ間のイベント通知では$broadcastが使えない場合がある

以前、AngularJSで複数のコントローラ間でデータを共有する手段として、$broadcastを使ったイベントの通知を紹介したことがある。
[AngularJS][Javascript]他の要素に変更を通知する

これならスマートにデータを共有できる。しかし、この方法を様々に使って見ると想定通りの動きをしないケースがあることが分かった。

サンプルとしては、1年前のエントリ同様にDOM上で親子関係を持つ二つのコントローラ、があるとしよう。

parentController.js
function ParentController($scope, $broadcast, $log) {
    //リソース取得後にブロードキャスト
   $scope.user = User.getUserFromResource( {},
        function(data){
            $scope.user = data;
            $scope.$broadcast('event:userReceived', data);
        }
    });
}

ParentControllerでは$resource等を使って外部リソース取得後に$broadcastで他のコントローラにリソース取得を通知する。なので$broadcastを実行する際にChildControllerはロードされた後に$onが実行されて$broadcastを受ける準備が整っている必要がある訳だ。

childController.js
function ChildController($scope, $log) {
    $scope.$log = $log;
    $scope.$on('event:userReceived', function(event, data) {
        $scope.$log.log('++++ userReceived broadcast');
    });
}

しかし、例えばChildControllerがng-viewディレクティブでロードされる構造になっている場合、親のコントローラの間でロード(ダイジェスト)の順序が変わるのか、1度目は問題無いのに、その後、Webブラウザによりページをリロードした場合には、ChildController中の$onの実行($broadcastの監視開始)が$broadcastより遅れてしまうことがある。その場合、ChildControllerの$onは決して呼ばれない。

このケースのように、子のコントローラで親のコントローラのイベントを確実にハンドルした場合は、子のコントローラの準備が出来たことを親コントローラが知る必要がある。

このようなケースでは$emitを使う。

childController.js (改)
function ChildController($scope, $log) {
    $scope.$log = $log;
    $scope.$on('event:userReceived', function(event, data) {
        $scope.$log.log('++++ userReceived broadcast');
    });
    //自身がロードを完了したことを親に通知する
    $scope.$emit('event:onChildControllerLoaded', this);
}

parentController.js (改)
function ParentController($scope, $broadcast, $log) {
   $scope.$on("event:onChildControllerLoaded", function(event, controller){
        $scope.user = User.getUserFromResource({},
            function(data){
                $scope.user = data;
                //リソース取得後にブロードキャスト
               $scope.$broadcast('event:userReceived', $scope.user);
            }
        });
   }
}

ちょっと面倒だが、これでChildControllerはParentControllerの$broadcastを受けることができる。

2014-07-27

[][]開発環境をJetBrains IntelliJ IDEA Ultimateに切り替える

f:id:Kazzz:20140727092511p:image
開発中のアプリケーションは、フロントエンドにHTML+Bootstrap(CSS)+AngularJS、バックエンドにGAE/J+Slim3を利用しているが、それぞれWebStormEclipseと別々なIDEを利用しており、互いを行き来するのが面倒だと常々思っていた。 

そんな中、JetBrains社はAndroidStudioを開発者向けに無償提供しており、これが同社のIntelliJ IDEAがベースということで、元々歴史があり評価も高い同IDEが更に評価を受けている。
ということで私もフロントエンドとバックエンドの開発環境を統合したいと思い、まずはIntelliJ IDEAを評価してみることにした。
幸いにも評価期間が1か月あり、十分に使い込んでから購入するかどうかを決めることができる※

フロントエンドは元々WebStormを使っているのでIDEAに適宜プラグインを導入すればいけそうである

IntelliJに導入したプラグイン(デフォルト有効ものは除く)

AngularJS
File Watchers (Closure Compiler)
Karma
NodeJS

問題はバックエンドだ。Slim3は元々サポートしていたEclipseでも少々怪しい所が出てきており、特にAPT部分が動作するかどうか心配していたのだが、案の定IntelliJのAnnotation ProcessorはSlim3のModel Processorで使用されているcom.sun.mirrorパッケージでの実装はサポートされておらず、動かすためにはJSR-269 ("Pluggable Annotation Processing API")ペースに変えなくてはならないことが判明した。

slim3のModel Processorの実装やんのかぁ?...と途方に暮れていたのだが、twitterで愚痴っていた所slim3のgithub上には既にJSR269版のModel Processorがアップされているという!
slim3/slim3-gen-jsr269 at master · Slim3/slim3 · GitHub
これで一番の懸念は解決した訳で、アプリケーション全体をIDEA一本で開発できるようになった。(@vvakameさんありがとう。少なく見積もっても10日は節約できました)

サーバサイドはIDEに頼らずmavenJenkinsなどでCIしとけっていう考え方には一理あるが、どうせ一人で開発してデプロイしている訳でIDEに頼っても困らないので、当面はIDEA一本で開発していくことにした。

※一か月も使い込むと間違いなく購入することになる

2014-07-26

[][]Collection.disjoint

最近とんとご無沙汰だが、日々これ忙しく、特に現在は開発中のサービスに対して

「1週間に一度は新機能デプロイする」

という目標を掲げているので、日記を書いている時間が全く取れない。(全て一人で開発している)
今まで当日記を購読して頂いている方には大変申し訳なく心苦しいのだが、時間がとれるようになったら、また昔のように、更新したいと思っているのだが。

さて、表題の件だが、現在作っているGAE/Javeアプリケーションで、利用者メールを送るか否かの判定で

セットAの要素中にセットBの要素が一つでも含まれているか

という条件の判定が必要になっている。

ループで回せば簡単な話なのだが、

for (Object elementB : collectionB) {
    if ( collectionA.contains(elementB) ) {
        //OK
        break; //一つでも合致すればループ継続の必要なし
    }
}

なにか負けた感じがする。 JavaのCollection Frameworkで使えるメソッドが無いか調べてみた所、そのもの自体は無いが、Collection.disjointメソッドが素集合つまり互いに素であることを検査するメソッドがあるので、このメソッドの戻り値が偽であれば良さそうだ。

if (!Collections.disjoint(collectionA, collectionB) ) {
    //OK
}

結果は同じになりそうだが、大量のデータを扱う場合問題は性能。 disjointがショートカットしていない場合単純なループよりも遅くなる可能性があるのでテストが必要だろう。

ところで素集合の否定ってベン図ではどう書くんだろう?

2014-06-29

[][]GCSにアップロードをすると不定期に503(Service Unavailable)が戻る

GAEを経由したGCSへのファイルアップロード処理だが、Out of Memoryを回避し、日本語文字化けを回避し、ようやくテストまでこぎ着けた。
しかし、実際にデプロイ環境でテストを行うとアップロードは上手くいくのだが、まれにHTTPステータス503(Service Unavailable)が戻る時がある。

調べてみるとこの問題も既知のようだ。
Issue 10083 - googleappengine - Google HTTP Upload Server responds with HTTP 500 internal server error when uploading using the blobstoreservice - Google App Engine - Google Project Hosting

問題なのはこのエラー、GAEのログに残らないことだ。クライアント(Webブラウザ)に503が戻っているのは確かだが、ログに残らないので調べようが無い。

一難去って、一難去ってまた一難というところか。

2014-06-28

[][]BlobstoreAPIを介するGCSへのアップロードファイル名に日本語が使えない(その2)

他にも試してみたが、どうやらGAE側(GCS)はISO-8859-1でエンコードされていることを前提にしており、マルチバイト考慮していない。ASCII文字以外は使えないようだ。

TFS(Text Full Search)の時もそうだったが、日本語が通らないのであればURLエンコードしてしまうという手がある。
この処理の場合、GCS上のファイル名はユーザには見えないのでどんな記号になっても支障は無いからだ。

GCSにアップロードする前にファイル名をURLエンコードするように修正したコード(JavaScript)
uploadFile: function(file, urlForUpload, success, onProgress) {
    var encoded = encodeURIComponent(file.name.trim());//UNICODE文字は通らないので..
    delete file.name;
    file.name = encoded;
    var upload = $upload.upload({ //$uploadはangular-file-uploadのサービス
        url     : urlForUpload,
        method  : 'POST',
        transformRequest: null,
        file : file,
        fileFormDataName : 'uploadFile'
    }).then(function(response) {
        file.name = decodeURIComponent(encoded); //ブラウザに表示するのでデコード
        var blobContent = response.data[0];
        if ( success != void 0) {
            success(file, blobContent);
        }
    }, function(response) {
        //失敗時の処理
    }, function(evt) {
        var progress = Math.min(100, parseInt(100.0 * evt.loaded / evt.total));
        if ( onProgress != void 0 ) {
            onProgress(progress, file);
        }
    }).xhr(function(xhr){
        xhr.upload.addEventListener('abort', function() {console.log('abort complete')}, false);
    });
    〜

FileオブジェクトはImmutableなのかnameプロパティを書き換えることが出来なかったので、一旦deleteで削除して作り直した。
汚いコードだが、これ以外のやり方を知らない。

この問題、最初は開発環境のみの現象(開発環境上ではDatastoreでGCSをシミュレートしている)かと思ったのだが、実際のGCS上でもやはり日本語のファイル名をアップロードすることはできなかった。残念だ。

2014-06-27

[][]BlobstoreAPIを介するGCSへのアップロードファイル名に日本語が使えない

アップロードをコンベンショナルなマルチパートプロトコルにすることでOOM回避することが出来たのだが、この際にGCSにアップロードする際のファイル名に日本語(厳密にはASCII文字以外のUNICODE)が通らないことが判明した。

アップロード処理自体はAngularJS内からFileAPIを使用してマルチパートで行っていると書いた。ソースコードは著名なライブラリであるangular-file-upload(https://github.com/danialfarid/angular-file-upload)を利用しており、INPUT Type="file"要素のchangeイベントで選択されるファイルオブジェクトを以下のようなコードで処理している。

angular-file-uploadによるファイルのGCSへのアップロード
uploadFile: function(file, urlForUpload, success, onProgress) {
    var upload = $upload.upload({ //$uploadはangular-file-uploadのサービス
        url     : urlForUpload,
        method  : 'POST',
        transformRequest: null,
        file : file,
        fileFormDataName : 'uploadFile'
    }).then(function(response) {
        var blobContent = response.data[0];
        if ( success != void 0) {
            success(file, blobContent);
        }
    }, function(response) {
        //失敗時の処理
    }, function(evt) {
        var progress = Math.min(100, parseInt(100.0 * evt.loaded / evt.total));
        if ( onProgress != void 0 ) {
            onProgress(progress, file);
        }
    }).xhr(function(xhr){
        xhr.upload.addEventListener('abort', function() {console.log('abort complete')}, false);
    });
    〜

サーバ(GCS)側へアップロードを行っているのは$upload.uploadだが、例えばパワーポイント形式のファイル(日本語パワーポイント.pptx)をアップロードするとペイロードであるヘッダは以下のようになる。

送信されているmultipart/form-dataのバウンダリ先頭
------WebKitFormBoundary81eC58cFjoRiNCWg
Content-Disposition: form-data; name="uploadFile"; filename="日本語パワーポイント.pptx"
Content-Type: application/vnd.openxmlformats-officedocument.presentationml.presentation

filename属性UTF-8エンコードされており、GAE経由のGCS側はこのファイル名を正しくデコードしてくれれば良いだけなのだが、完全に文字化けしてしまう。

アップロードした後にGCSからGAEにコールバックされた際のBlobInfoにセットされたファイル名
{p[|Cg.pptx

他にも試してみたが、どうやらGAE側(GCS)はISO-8859-1でエンコードされていることを前提にしており、マルチバイト考慮していない。ASCII文字以外は使えないようだ。
ServletAPI黎明期にはよくあったパラメタが文字化けする問題の原因はまさにこれだったのだが、今更このような問題に遭遇するとは思わなかった。

一難去ってまた一難。