より良い環境を求めて このページをアンテナに追加 RSSフィード

2011-07-26

[][][] Slim3のModelRefをGWTで使えるかどうかのユニットテスト

GWT上でModelRefが利用出来ることが分かったが、サーバー側で一度もgetModel()を実行せずにGWTで初めてgetModel()するともnullが返る。

気を付けていても忘れることがあって思わぬところでエラーが出る。

そんなときはテスト。


ModelRefは内部にModelインスタンスを保持していなければquery()を実行してデータストアから取得する。

そのquery()をmockitoのspyでチェックすれば良い。

class NodeServiceImplTest extends ServletTestCase {
    private NodeServiceImpl service = new NodeServiceImpl();

    @Test
    public void getForGWT() throws Exception{
        Node root = service.getTree("rootKey");
        InverseModelListRef<Node, Node> ref = spy(root.getChildrenRef());

        assertThat(ref.getModelList().size(), is(5));
        // getModelListはservice側で一度も呼ばれていなければquery()を呼び出す

        verify(ref, never()).query();
        // query()が呼ばれていなければGWTで利用可

こんな感じ。

2011-07-25

[][] Slim3 + GWTユニットテストで mockito を使ってみる

Slim3 + GWTクライアント側のテストをするために、Gin はちょっとオーバースペックな気がしたので mockito を使ってみてる。

    private ItemServiceImpl itemService = spy(new ItemServiceImpl());

    @Test
    public void myTest() throws Exception {
        ClientFactory clientFactory = mock(ClientFactory.class);
        when(clientFactory.getEventBus()).thenReturn(new SimpleEventBus());

        Activity topActivity = mock(Activity.class);
        when(clientFactory.getTopActivity()).thenReturn(topActivity);

        when(clientFactory.getItemService()).thenReturn(itemServiceAsync);


        ...

        topActivity.getItems();
        verify(itemService).getItems();

で、これのitemServiceAsyncなんだけども、これをどうやって作ろうかと。

コールバックのテストってどうやるんだろう?

最初はモックを使わずに new ItemServiceAsync(){ ... } とテストコードに書いていたんだけど、メソッドが増えたらいちいち書いてられない。


ちょっと迷った末にProxyを作ることにした。

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

import com.google.gwt.user.client.rpc.AsyncCallback;

public class AsyncServiceProxy {
    @SuppressWarnings("unchecked")
    public static <T1, T2> T1 getProxy(Class<T1> async, final T2 service) {
        return (T1) Proxy.newProxyInstance(
            AsyncServiceProxy.class.getClassLoader(),
            new Class[] {async},
            new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args)
                        throws Throwable {
                    Class<?>[] params = method.getParameterTypes();
                    Class<?>[] serviceParams = new Class<?>[params.length - 1];
                    for (int i = 0; i < params.length - 1; i++) {
                        serviceParams[i] = params[i];
                    }
                    Method serviceMethod = null;
                    try {
                        serviceMethod = service.getClass().getMethod(method.getName(), serviceParams);
                    } catch (SecurityException e) {
                        e.printStackTrace();
                        return null;
                    } catch (NoSuchMethodException e) {
                        e.printStackTrace();
                        return null;
                    }
                    Object[] serviceArgs = new Object[args.length - 1];
                    int i;
                    for (i = 0; i < args.length - 1; i++)
                        serviceArgs[i] = args[i];
                    AsyncCallback<Object> callback = (AsyncCallback<Object>) args[i];

                    Object ret = serviceMethod.invoke(service, serviceArgs);

                    callback.onSuccess(ret);
                    return null;
                }
        });
    }
}

やっつけ感満載のコードだけれども。

これで

        ItemServiceAsync itemServiceAsync = AsyncServiceProxy.getProxy(ItemServiceAsync.class, itemService);

と書けば AsyncService => ServiceImpl => AsyncCallback#onSuccess と変換して呼び出すコールバックが生成できる。


なんだかmockitoや他のライブラリにちゃんとしたやり方がありそうな気がするが、見つからなかったのでとりあえずこれで。


mockito 参考:

http://d.hatena.ne.jp/Naotsugu/20101109/1289304795

http://d.hatena.ne.jp/takahashikzn/20101001/1285897862

http://tech.cm55.com/wiki/mockito/General

2011-07-19

[][] GWTで使えるクラスのメモ

twitterにも書いたんだけど、どうしてGWTでKeyクラスが使えるんだろうと思っていた。

それから、Datastore.keyToString(model.getKey())をGWTクライアント側でやりたいよねと。


でも試しにmodel.getKey().getName()を実行してみたらちゃんと取得できた。

GWTクライアント側でもKeyクラスのメソッドは使えるようだ。

ということは、KeyがStringで生成されていることが確定しているならgetNameを呼び出せば元の文字列が取得できるわけだ。

わざわざサーバーに問い合せてサービスの方でkeyToStringを呼び出すか、Keyの文字列をコピーして別のフィールドに持たせようかと思っていたところだった。


何故だろうとソース探索。

GWTにはGAE/Jのコードは入ってなさそうだ。

それからslim3のコードを見ていくと、org.slim3.gwt.emul.S3Emulation.gwt.xmlのファイルがある。

そこには前に書いた super-source *1 があり、ルートが指定されている。

改めてよく見るとSlim3に com.google.appengine.datastore.Key_CustomFieldSerializer などのクラスがある。

KeyなどのappengineのクラスはソースファイルがないのでGWT用のJavaScriptは生成できないが、ここでシリアライザを使ってGWTでも使えるようにしているようだ。

公式にGWTがサポートされているっていうのはいいね。

2011-07-18

[][][] Slim3の本の途中まで読んだ感想と代わりのチュートリアル

一昨日書いたこの本。

1/3までじっくり読んであとは流しでしか読んでないけど、これは要らなかった。

読んでいて全然頭に入ってこない。

というか写経用なので、ただ読んでるだけっていう使い方は間違っているんだろう。

サンプルを元に読みながら作っていかないと意味ないっぽい。


だだ…変数名とかエンティティ名が気になる。

変数を大文字で始めていたりアンダースコアが入っていたり。

GWTの説明も特にない。コードは書いてあるのだが、何故そうなっているかの説明が全然ない。

それからUiBinderを使うならGWT Designerを使えばいいのに何故か自作ツールを勧めている。

そして、何故かマルチモジュール構成で作っている。一つの画面にhtmlが一つ。GWTではこういう作り方は普通やらないんじゃないかな。普通はHistoryHandlerを使うと思うんだけど。

switch文やif文の羅列も良くない。ボタンのクリック処理も何故UiHandlerを一カ所にまとめてClickEvent#getSourceで分岐するんだろう。GWT Designerを使っていればそんなことにはならず、ちゃんとメソッドが分かれてくれる。


この本は写経用に書かれているのに、GWT+Slim3のための写経には全く適していないのだ。

Slim3の部分に関してはそんなに変なことは書いていないと思うのだが、全体的にGWTが絡んでくるサンプルになっているのでSlim3の部分だけ読むのも難しい。どこからどこまでがGWTSlim3かを意識させない作りになっていて、分からないままやっつけるのにはいいが理解の助けにするには向いていない。






これで終わるのも気が引けるので、自分的GWT+Slim3の始め方を書いておこう。


まず http://code.google.com/webtoolkit/doc/latest/DevGuideMvpActivitiesAndPlaces.html この辺のPart I から読む。

本はダメだと思う。どうしてもGWTの更新速度に追いつけない。本に載っているものがどんどん古い書き方になっていく。なので基本的に英語版Webのものを見る。

冗長でめんどくさいが、とりあえずMVPで作る。新しいGWTプラグインならMVPの新規作成に対応していて一気に複数のファイルを作ってくれるのでだいぶ楽になった。

このとき、UIGWT Designerを使う。あとRootPanelとRootLayoutPanelの違いを別途調べる。後でハマると面倒なことになる。


一通り理解したつもりになったら、Slim3へ。


Slim3https://sites.google.com/site/slim3documentja/getting-started-with-gwt ここを写経。ただしGWTのコードはさっきの書き方に従う。

例えば

    @UiHandler({"tweet", "refresh"})
    void handleClick(ClickEvent e) {
        if (e.getSource() == tweet) {
            tweet();
        } else if (e.getSource() == refresh) {
            getTweetList();
        }
    }
    
    void tweet() {
        service.tweet(content.getText(), new AsyncCallback<Void>() {
            
            public void onSuccess(Void result) {
                content.setText(null);
                getTweetList();
            }
            
            public void onFailure(Throwable caught) {
                Window.alert(caught.getMessage());
            }
        });
    }
    
    void getTweetList() {
        service.getTweetList(new AsyncCallback<List<Tweet>>() {
            
            public void onSuccess(List<Tweet> result) {
                table.clear();
                for (int i = 0; i < result.size(); i++) {
                    table.setHTML(i, 0, result.get(i).getContent());
                }
            }
            
            public void onFailure(Throwable caught) {
                Window.alert(caught.getMessage());
            }
        });
    }
https://sites.google.com/site/slim3documentja/getting-started-with-gwt/listing-tweets

ここのコードは

    @UiHandler("tweet")
    void onTweetClick(ClickEvent e){
        presenter.tweet(content.getText());
    }
    @UiHandler("refresh")
    void onTweetList(ClickEvent e){
        presenter.getTweetList();
    }

    public void showError(String err){
        Window.alert(err);
    }
    public void cleanContent(){
        content.setText(null);
    }
    public void setTweetList(List<Tweet> result) {
        table.clear();
        for (int i = 0; i < result.size(); i++) {
            table.setHTML(i, 0, result.get(i).getContent());
        }
    }

こんな感じになる。

presenterのgetTweetListが非同期呼び出しを実行し、viewのsetTweetListを呼び出す。

ViewにServiceを書かないようにするとだいぶすっきりする。


Activityはこんな感じ。

    private abstract class MyCallback<T> implements AsyncCallback<T> {
        @Override
        public void onFailure(Throwable caught){
            view.showError(caught.getMessage());
        }
    }
    public void tweet(String text) {
        service.tweet(text, new MyCallback<Void>() {
            @Override
            public void onSuccess(Void result) {
                view.cleanContent();
                view.getTweetList();
            }
        });
    }

MyCallbackはユーティリティ的なクラス。毎回エラー用処理を書くのが大変なので、復帰用の特別なコードがないなら共通化する。

これで一通り終了。


このあとはWindow.alertをGwtEventとEventHandlerに置き換えたり新しいPlaceを作って動きを確かめたり、Slim3トランザクションを試したり、など。GWTSlim3マニュアルから適当なところをかいつまんで書けば大体の動きが分かると思う。



ここまで勢いで書いたが、まだGWTSlim3アプリを作ったことはない。むしろ教えて欲しい。(そのために本を買ったのだった)

2011-07-14

[][] GWTとS2JDBCの連携

GWT + Seasar2 で困ったことが一つ。

S2JDBCのentityをGWTクライアントに受け渡せない。


で、今まではDTOを作って詰め替えをしていたのだけれど…。


Slim3を少し触ってみたら、こっちはGWTに完全対応なのでDatastoreからのModel(S2JDBCでいうEntityのようなもの)の受け渡しがスムーズだ。

何故?と思ってソースを眺めてみると、Modelアノテーションを独自に作って、それもGWTのコンパイル対象になってるのね。なるほど、アノテーション自体がダメなわけではないのか。

http://code.google.com/webtoolkit/doc/latest/RefJreEmulation.html#Package_java_lang_annotation




というわけでS2JDBCのEntityもGWTクライアントで使えるようにトライ。


追記:すごく遠回りしたっぽい。

色々触っていて勘違いしていたが、どうやらアノテーションのソースは不要のようだ。

どうもgwt.xmlの書き方を間違えて、そこでエラーが出ていた模様。

<source path="client" />
<source path="server/entity">
    <exclude name="*Names.java" />
</source>

これだけ書けばentityのクラスがGWTクライアントでも使える。

source path を追加した場合はデフォルトのclientも書く必要があることと、パスの指定方法のスラッシュ区切りがドット区切りと間違えていたことが原因でエラーになってたっぽい。


Gearsも使っていてキャッシュが残っている可能性があるのでgit pullからやり直してみたが、たぶんこれで大丈夫。

ここから下は、もしGWTクライアント側でも何か処理が必要になった場合のためのメモということで。

追記終了


まずはjavax.annotation.*を持ってくる。

Debianならaptで入れる。

aptitude install sun-java6-source

dpkg -L sun-java6-source してみるとsrc.zipがあるので解凍。

javax/annotation/ 以下のファイルをプロジェクト直下のsuperディレクトリに移動。このとき javax/annotation/processing は不要。というか他のパッケージを参照しているので、あると動かない。

このやり方は超ソースの記事 http://d.hatena.ne.jp/bufferings/20100109/1263052743 を参考にした。


あとjavax.persistence.Entityアノテーションのソースも必要。geronimo-jpa_3.0_spec-1.0.jar にあるようだ。これはエンティティクラスのEntityアノテーションの上でCtrl+クリックで表示されたソースをコピペした。


それからApp.gwt.xmlの編集。

<source path="client" />
<source path="server/entity">
    <exclude name="*Names.java" />
</source>

<super-source path="super" />

これでserver/entityとプロジェクトルートのsuperがGWTのコンパイル対象になる。*Names.java はseasarのPropertyNameを参照しているし使わないだろうから外しておく。

この辺の書き方は http://d.hatena.ne.jp/soundTrick/20110116/1295156155 ここを参考にした。


これで完成。テストでちょっと試しただけなので不都合があればまた書く。

2011-05-18

[] GWT2.3.0 + Gears

GWTを2.1.1から2.3.0にバージョンアップしたらGearsが動かなくなったのでメモ。


GWT2.2.0からはWEB-INF/deployディレクトリが生成されるようになって、容量節約のためにそれを削除したいとかいう問題もあるのだが、Gearsを使うにあたってはエラーが出て動かなくなった。

WEB-INF/deploy以下のファイルもGearsのOffline管理対象に入ってしまうようだ。


で、取り敢えずGWT2.3.0対応予定のgwt-gears-1.3.1-rc1が出ている*1のでダウンロードして内部Mavenリポジトリにアップする。

mvn deploy:deploy-file -DgroupId=com.google.gwt.google-apis -DartifactId=gwt-gears \
  -Dversion=1.3.1-rc1 -Dpackaging=jar -Dfile=gwt-gears.jar -DgeneratePom=true \
  -Durl=http://mynexus.example.com/content/repositories/thirdparty/ -DrepositoryId=mynexus.example.com

あとはpublicフォルダのGearsManifest.jsonにフィルターを追記。

    // @filter rpcPolicyManifest.*
    // @filter .*symbolMap

rpcPolicyManifestとsymbolMapをGears対象から外す。

これで動いた。

2011-05-14

[] GWT DesignerがGeneratorのクラスを読み込めない

gwt.xml


    <generate-with class="mydomain.rebind.service.TransServiceGenerator">
        <when-type-assignable class="mydomain.client.service.TransService" />
    </generate-with>

こんなふうに書いて、ソースコード生成を試している。

エラーなく動くことを確認したが、GWT Designerを表示しようとすると落ちるようになった。


ログにはClassNotFoundExceptionが出ていてTransServiceGeneratorを見つけられないらしい。

色々値を変えてみると、jarのクラスは見つけられるがソースパスのクラスは見つけられないっぽい。

GWT Designerのクラスパスの設定?ってそんなのないよな…。


gwt.xml解析中に落ちるので、when-property-is とか書いてもそれを読みに行く前に落ちる。


GWT Maven Plugin を使っているからなのか GWT 2.3.0 にバージョンを上げたからなのかWindowsのEclipseだけなのかは不明。

取り急ぎ、Generatorのファイルだけjarで外に出してpom.xmlにsystemスコープで追加しておいたらDesignerは落ちなくなった。


参考:Error 404 (Not Found)!!1(キャッシュ)