ラベル Java の投稿を表示しています。 すべての投稿を表示
ラベル Java の投稿を表示しています。 すべての投稿を表示

2014年10月22日水曜日

「Javaプログラマーなら習得しておきたい Java SE 8 実践プログラミング」を読みました。

Blogを書くのが遅くなってしまいましたが、「Javaプログラマーなら習得しておきたい Java SE 8 実践プログラミング」(http://www.amazon.co.jp/dp/4844336673)を読みました。

目次は以下の構成です。

  • 第1章 ラムダ式とは
  • 第2章 ストリームAPIの使い方
  • 第3章 ラムダ式を使ったプログラミング
  • 第4章 JavaFXによるGUIプログラミング
  • 第5章 日付と時刻の新たなAPI
  • 第6章 並行処理の機能強化
  • 第7章 Nashorn JavaScriptエンジンの活用
  • 第8章 その他のJava 8機能を理解する
  • 第9章 Java 7の機能を復習する
私はJava 5が出たあたりで、Javaをほとんど書かなくなってから、AndroidのプログラミングでJava 6のコードを書いているという状態でした。従って、Java 7の内容とか、Java 8の事は殆どわかりません。(以前、Java 8のハンズオンイベントに参加したので、Stream APIとラムダ式は教えて頂いたことがある。)

実は、今回縁あってImpress Japanさんから献本を頂きまして、本書を読むことができました。ありがとうございました。書籍を読む時間が取りづらいのと、貸してほしいとお願いされたこともあって、私は5章まで読んで、以降は流し読みになっていますが、私の印象を書いておきたいと思います。

読む前の印象としては、柴田さんの翻訳本の文章は「日本人には読みづらい」という印象がありました。Goフレーズブックとかが、わりと頭が痛くなる文章だった気がします。
ですが、本書に関しては、わかりやすい文章になっていると思います。

しかし、コード例や、説明がJava 6、Java 7の書き方をしっかり理解していないと、内容を理解するのが難しいと感じました。従って、Java 7の内容がわかっていないのであれば、一度9章を読んでからの方が良いのかもしれません。
(初心者の方は、恐らく途中で挫折してしまうと思いますので、最初の書籍に選択するのはおすすめしません)

ラムダ式の所は、型の記述を省略できるようになっているのですが、引数の型が見てわかるようであれば良いのですが、書籍の例のラムダ式のコードを見てもすぐに型が分からないこともあったので、ある程度、Javaのクラスの事が分かっている方がいいかもしれません。(もしくは、ドキュメントを見ながら読み進める必要があるかもしれません)

本書には(私はまだ取り組んでいませんが)練習問題が用意されていて、練習問題に取り組むことで、理解を深めることができるようになっています。ですから、よくわからなかった所は、練習問題でもう一度、内容を整理して理解するということが可能になっています。

書籍は260ページ程度で、比較的薄い本ですから読み終わるまでに時間はかからないと思います。

今のところ、Java 8の内容全体を見渡せる書籍は本書のみだと思いますので、全体を見渡したい方はお手に取って見てください。

2013年2月19日火曜日

Jenkins実践入門を読みました。(1)

Jenkins実践入門を読みました。


ただし、途中(5章 JUnitでテストする)までで中断しています。

その理由は、「実際に使ってみないとイメージが沸かない」というのがあって、
(サンプルコードを使えば動きは見れるのでしょうが)
実際のプロジェクトで試してみたいと思って、ひとまず、テストを実行させる所で
中断して、別の書籍を読んでいます。

今回はほぼ環境構築で苦戦しましたが、
Scientific Linux上に構築する場合はパッケージのTomcatを使わずに、公式からダウンロードした物を
利用した方が良いと思います。(パッケージだとtomcatユーザで動作しますが、ログイン不可であるため、gitのコマンドを実行させる事が”そのままだと”できません。)

この書籍はSubversionで動作させる前提で説明していますが、
私はGit Pluginを導入して動作を確認しました。
操作は全く違いますが、書籍と大幅に違う表示になるという事は無いのと、
10章にGitとの連携についても記述があるので、困ることはないと思います。

後は、ant、またはmavenの使い方がわからないと自分のプロジェクトで動きを確認することは
難しいので(antのbuild.xmlや、mavenプロジェクトのpom.xmlあたりの記述がわからない)
同時にantやmavenの書籍を読み、ある程度知っておいた方が良いかもしれません。

という事で、今後もまた読むことになると思うのでタイトルは(1)としました。

関連したGoogle+の投稿

https://plus.google.com/u/0/114183076079015753160/posts/GtRGRr85aQT

https://plus.google.com/u/0/114183076079015753160/posts/AxLauhRC2CG

JenkinsにGit Pluginを導入した。
https://plus.google.com/u/0/114183076079015753160/posts/E8L5bT1Cb3E

Tomcatはパッケージのやつを入れない方がハマらない…。
https://plus.google.com/u/0/114183076079015753160/posts/1949ByXFohW

読み方がわからん…。
https://plus.google.com/u/0/114183076079015753160/posts/Tq5Cx3fHiwf

Antタスクでハマる
https://plus.google.com/u/0/114183076079015753160/posts/cqoQk5kB1JZ
https://plus.google.com/u/0/114183076079015753160/posts/AwsYPtijrVH
やっと解決
https://plus.google.com/u/0/114183076079015753160/posts/3zshn4H4ZSB

2013年2月11日月曜日

JUnit実践入門を読みました。


JUnit実践入門を読みました。
この書籍はJavaで動作するユニットテストフレームワークのJUnitを利用した
テストコードの書き方が書かれていて、私も途切れ途切れに読んだので
この書籍もまた読もうと思っていますが、サンプルコードも掲載されているので
実際に入力して動かした方がいいかもしれません。

特に個人的に発見だったのが、カテゴリ化テストとパラメータ化テストで、
書き方を知らなかったのもありますが、これまではいろいろなテストが混ざってしまって
読みにくく、メンテナンスもしにくかったのですが、この辺りの書き方を利用すれば
もう少しデータの種類も増やせそうだし、テストの分類もできるので
「どのような視点で」テストをするのかが明確にできそうな気がしました。

最後に演習問題のページがあり、そこで如何にJavaの力がないのかを
思い知るのも良いかもしれないです。
(私はJDK1.4あたりで止まってるっぽい感じがしました…)

他の使い方としては、リファレンス的に参照しても使えると思います。
(恐らく私はこの使い方になりそうな気がします)

やっぱり書籍は繰り返し読んだ方が良いですね。

Two-legged OAuthを利用してGoogle Appsにアクセスするには

Google Appsの業務をやり始めてから初めて知ったTwo-legged OAuthの認証について
あまりいい記事がなかったので、書き残しておきます。

結論からいうと、毎度のごとく、+keisuke oohashiさんの助けにより
うまく動作させることができました。(ありがとうございます!)

Two-legged OAuthがGoogle Apps無料版でも利用できるのかどうかを
確認した時の投稿(あまり参考になりません
https://plus.google.com/u/0/114183076079015753160/posts/j4sHzchyHWs

Two-legged OAuthを検証し始めた時のGoogle+の投稿(参考にするならこちら)
https://plus.google.com/u/0/114183076079015753160/posts/8NCiJghLapC

Two-legged OAuthのポイント


  • 認証用のオブジェクトと、各種APIにアクセスするためのオブジェクトを生成する。(以下の例はCalendar API)

OAuthHmacSigner signer = new OAuthHmacSigner();
signer.clientSharedSecret = "<クライアントシークレット>";
    
OAuthParameters param = new OAuthParameters();
param.version = "1";
param.consumerKey = "<コンシューマキー>";
param.signer = signer;
    
Calendar calendar = new Calendar.Builder(httpTransport, jsonFactory, null)
      .setHttpRequestInitializer(param)
      .setCalendarRequestInitializer(new CalendarRequestInitializer("<Simple API AccessにあるAPI KEY>"))
      .build();
    
service = new CalendarService(calendar);

コンシューマーキーはGoogle Appsだと、取得したドメイン名のスーパーキーが標準で用意されているのでそれを使うのが簡単ですが、実際に運用する場合は自分で作った方がいいでしょう。作成する場所はGoogle Appsの管理コントロールパネルから高度なツールを選択して「サードパーティのOAuthクライアントアクセスを管理する」から作成して下さい。コンシューマーシークレットも同じページの「管理」リンクをクリックすることで見ることができます。次にSimple API AccessにあるAPI KEYですが、これは、「Google APIs Console」を表示してAPI Accessメニューをクリックすると表示されるKEYを指定します。(Google Appsの管理者アカウントでアクセスした方が無難です)

  • 編集する対象のユーザを指定する。


ArrayMap<String, Object> customMap = new ArrayMap<String, Object>();
customMap.add("xoauth_requestor_id", "<対象のアカウント>");

com.google.api.services.calendar.Calendar api = service.getService();
com.google.api.services.calendar.Calendar.Events.List eventList = api.events().list(target_email)
               .setTimeMin(new DateTime(fmt.format(Calendar.getInstance().getTime()) + "T00:00:00+09:00"))
               .setFields("items(id,summary)")
               .setMaxResults(1000);
   
eventList.setUnknownKeys(customMap);
   
Events events = eventList.execute();

Two-legged OAuthは管理用途で利用される機能(?)なので、編集する対象のユーザを指定しないとどのユーザのデータを編集するのかがわからないので対象のユーザを指定して下さい。

以上で、Two-legged OAuthを使った管理作業が可能になると思います。

2012年3月14日水曜日

Slim3のテストを行う時に気をつけること(Datastore編)

開発しているGAEのアプリケーションのデータのメンテナンスを行う必要があるので、
メンテナンス用のコントローラを作成してテストを行おうと思って、テストコードを
書いていました。

テストコードをそのまま掲載するわけにはいかないので、適当なコードを載せます。

public class HogeControllerTest extends ControllerTestCase {
     private HogeService service = new HogeService();

    @Override
    public void setUp() throws Exception {
        super.setUp();

        // データの登録
        Hoge hoge = new Hoge();
        hoge.setId(1);
        hoge.setName("abcd");

        service.put(hoge);
    }

    @Test
    public void run() throws Exception {
        ApiProxy.setEnvironmentForCurrentThread(new UnitTestEnvironment());
        
        tester.start("/hoge");
        HogeController controller = tester.getController();
        assertThat(controller, is(notNullValue()));
        assertThat(tester.isRedirect(), is(false));
        assertThat(tester.getDestinationPath(), is(nullValue()));
        
        List<Hoge> list = service.query().asList();
        assertEquals(1, list.size());                     // ここでエラー
    }
}

UnitTestEnvironmentクラスのソースです。
public class UnitTestEnvironment implements Environment {

    @Override
    public String getAppId() {
        return "Unit Tests";                // setupを使う場合はこれにしておかないとquery()でデータが取れない。
    }

    @Override
    public Map<string, object> getAttributes() {
        Map<string, object> map = new HashMap<string, object>();
        map.put("com.google.appengine.server_url_key", "dummy");
        return map;
    }

    @Override
    public String getAuthDomain() {
        return "gmail.com";
    }

    @Override
    public String getEmail() {
        return "admin@gmail.com";
    }

    @Override
    public String getRequestNamespace() {
        return "";
    }

    @Override
    public String getVersionId() {
        return "unittest";
    }

    @Override
    public boolean isAdmin() {
        return true;
    }

    @Override
    public boolean isLoggedIn() {
        return true;
    }

}

上記のUnitTestEnvironmentだとテストは正常終了します。
これは、setupでのデータストアの書き込み時のapplicationIdと
テスト実行時の呼び出しの時のapplicationIdが一致するからです。

失敗していた時というのが、TaskQueueのテストをしようとして参考にした
UnitTestEnvironmentのapplicationIdが"MyApplicationId"とかにしていたので
setUpの時とapplicationIDが異なってしまっていました。
その影響で、何度queryを実行してもデータが1件も取れないという状況に
陥っていました。

なので、setupを使う時というか、データストアを使ったテストを行う場合は
以下の点に注意した方が良さそうです。

  1. appidをデータ登録時と参照時で合わせておく。
  2. Slim3のTestEnvironment.javaで設定されているappIdは"Unit Tests"

2.はsetupを使わず、自分でデータを登録する場合は問題ないと思いますが、
初期データなど使う場合や、事前にlocal_db.binを用意する場合などは
気をつけておかないとデータが取れない現象に遭遇するかもしれません。

以下、参考までに当時の私のG+に投稿したコメントです。

TaskQueueに入っている事を確認するテストで苦戦している様子(Google+)
https://plus.google.com/u/0/114183076079015753160/posts/NUceVDbNKDK

データストアからフィルタなしのqueryでデータが取れなくて苦戦している様子(Google+)
https://plus.google.com/u/0/114183076079015753160/posts/MZhuGtjrwTS

Slim3 Source Code Reading #12

今日は、Slim3 Source Code Reading #12でした。

今回の内容はRouterを読み進めました。ソースファイルは以下の4ファイルです。

  • Router.java
  • RouterFactory.java
  • RouterImpl.java
  • Routing.java

少し前に、私が仕事で開発しているGAE/JのアプリケーションでRouterを導入したので
ソースコードを事前に読んでいました。
(Googleで検索せずにFrontController.javaを読み進めて実装方法を解析したのは、ソースコードリーディングをした影響でしょうか。)
また、@sinmetalさんと2人だったので、ざっと読んだ感じになりました。

最初にRouter.javaですが、Routerクラスはインターフェイスで、
静的ファイルの判定と、リクエストURLを変換した後のURL文字列を返すメソッドの2つがあります。

次に、RouterFactory.javaですが、これはFrontControllerからRouterを呼び出すために
インスタンスを生成するクラスです。
getRouter()は定義したRouterか、デフォルトのRouterを取得します。
ここで取得したRouterのインスタンスはservletContextに格納され、2回目以降は、キャッシュされたインスタンスが取得されます。
初回は、Routerのインスタンスを生成しますが、そのメソッドがcreateRouterメソッドです。
この中身を見れば分かるのですが、自前のRouterはRootPackage.controllerPackage.AppRouterクラス固定となっています。
配置する場所も、名前も間違っていると反映されないことになります。

RouterImplクラスは、Slim3を使う上ではデフォルトのRoutingの処理が書かれています。
仕組みは、ListにRoutingのルールを追加しておいて、Listの中に対象のURLがあるかどうかを判定するようになっています。URLがあれば、変換後のURL文字列を返します。
静的ファイルの判定(isStaticメソッド)は以下の様になっています。

  • /_ah/で始まらない。(管理画面などのURL)
  • 最後が拡張子ではない(.jpgなどで終わらない)か、拡張子が"s3"で始まらない

拡張子が"s3"で始まるファイルは静的ファイルではないと判定するようです。
そんなファイルがあるかな?と考えてみましたが、その時は思いつきませんでした。
クラスにはS3で始まるクラスもあるようですが、”拡張子”がs3で始まるファイル…。

最後にRoutingクラスですが、このクラスは、変換前のURL文字列と変換後のURL文字列を持ちます。
ざっと説明すると、"/hoge/fuga/{param}"を"/hoge/fuga?param={param}"のように
変換するという処理を行います。
fromの{}で囲まれた部分を、toの{}で囲まれた部分に配置するという感じです。

複数の{}を指定する事も可能です。

"*"を指定することも可能なのですが、今回はこの"*"の扱いにはまりました。
読書会での結論は"*"を使うと、それ以降は全てひとつのパラメータに格納され、
URLがエンコードされるようになるということでした。
また、{}の中に"*"を使うことはできません。

最後に、前回のMockXXX.javaもそうだったのですが、Router関連も
先頭にコメントがあって、CopyRightはSeasar Foundationになっていました。
Apache Licenceが適用されるようです。
(単純にSeasarからコピーしたということなのでしょうか?)

今回でひとまず、Slim3 Source Code Readingは最終回となりました。
振り返り、まとめの記事は後で、別途残そうと思いますが、
約3カ月、全12回の長い活動になりました。
最初から最後まで読み進めた結果、ある程度はSlim3でGAE/Jの
アプリケーションが作れるようになったような気がします。

今後、GAE/JのSDKのバージョンアップに合わせて
Slim3のバージョンアップも行われると思いますので、
新しくなった時に、また個人的にか、イベントとしてやるかは、
わかりませんが、再度読み進めてみたいと思います。


2012年3月7日水曜日

Slim3 Source Code Reading #11

昨日、Slim3 Source Code Reading #11でした。

今回もtesterパッケージを読みました。
今回はMockXXX.javaとHeaderUtil.java、UrlFetchHandler.javaでした。

MockXXX.javaに関しては、リクエスト、レスポンスを擬似的に再現するための
もので、中身もgetter/setterばかりで特にこれといったものはありませんでした。

ただし、MockHttpServletRequestに関してはHeaderなども変更できるようなので、
例えばjsonのリクエストをテストする場合は事前にHeaderを書き換えた状態で
テストを行うことも可能な感じがしました。
後は、cron jobの実行だとヘッダに”X-AppEngine-Cron: true”が付く事に
なっていますから、手動で実行した時は無視する処理をいれている場合は
Controllerの実行前にHeaderを追加してからテストするという事も
可能だと思います。

HeaderUtilも時刻のフォーマットが定義されているだけで、これも直接利用する事は
無さそうです。

UrlFetchHandlerはinterfaceで、中身を自分で実装する必要があります。
Slim3Demoにもこのクラスを使ったDemoは存在していないようなので、
そもそも利用していないのか?という話になりました。

本題に移ると、これは実際に動作の確認はしていませんが、
アプリケーションでGoogle App EngineのURL Fetch APIを利用した時の
外部サイトのテストで使うために利用するようです。
interfaceを実装する形になりますから、テストデータは自由に作ることができます。

恐らく、以下のようなテストコードになると思います。※以下のコードは未検証です。

@Test
public void URLFetchAPIを使うコントローラのテスト() throws Exception {
    tester.setUrlFetchHandler(new HogeUrlFetchHandler());

    tester.start("/urlfetchsample/");
    AddController controller = tester.getController();
    assertThat(controller, is(notNullValue()));
    assertThat(tester.isRedirect(), is(true));
    assertThat(tester.getDestinationPath(), is("/"));
}

URL Fetch API用のHandlerオブジェクト ※以下のコードは未検証です。
public class HogeUrlFetchHandler() implements UrlFetchHandler {
    public byte[] getContent(URLFetchRequest request) throws IOException {
        String strResponse = "何かのデータ";

        return strResponse.getBytes("UTF-8");
    }

    public int getStatusCode(URLFetchRequest request) throws IOException {
         return HttpServletResponse.SC_OK;
    }
}

昨日の段階では「これを利用する事はあるかな?」という話になりましたが、
既に動作中のWebAPIでテストを行う場合は、URLFetchHandlerは
不要なのかもしれませんが、
動作中のAPIだと実行時点でデータが変わったりするのでテストの結果が
実行するたびに変わってしまうという結果に陥りそうなので
作るべきかな?と思ったりしています。

あとは、WebAPIも作成途中でテストデータが無いと言った時も利用することに
なるでしょう。

今回は中身も大した内容ではなかったので、テストコードをどれくらい書くか?
みたいな話題になりました。
(読書会での結論は「不安な所をテストする」と言われているので、「不安がなくなるまで書け」ば良いようです。)

さて、次回(#12)はorg.slim3.controller.routerを読んで、最終回となります。
ご興味がありましたら、ご参加下さい。

2012年2月29日水曜日

Slim3 Source Code Reading #10

今日はSlim3 Source Code Reading #10でした。

前回に引き続き、testerを読み進めました。
今日は、以下のファイルを読みました。

  • ControllerTestCase.java
  • ControllerTester.java
  • ServletTester.java

ControllerTestCase.javaはAppEngineTestCaseと同じように中身は
testerというメンバ変数があるだけでした。ので、スルー。

testerはControllerTesterクラスの変数なので、そのまま、ControllerTester.javaに
進みました。

ControllerTesterはsetUpで、初期化パラメータの読み込みと、
FrontControllerの初期化を行っています。
これが、リクエストを受け取った時のFilterの初期化の代わりとなっているようです。

tearDownは後処理が行われて終わりです。

startメソッドが、実際にControllerを実行するメソッドですが、内部でRouterも
読み取るので、Routerの設定は反映されます。

以下の3メソッドはControllerに合わせてあるのだと思いますが、
中身も少ないので、概要だけ。
getDestinationPathは遷移先のパスを返します。
getControllerはrequestScopeに格納されているControllerを返します。
getErrorsはValidationエラーの情報を取得します。

最後にServletTester.javaですが、
setUpでは、元の設定を退避し、本番環境に合わせるように設定されるようです。
例えば、タイムゾーンは"UTC"をセットしていますし、ロケールはLocale.USがセットされます。

tearDownで、退避していた元の設定を復元しています。

param()はパラメータの取得/設定します。このメソッドを使ってテスト前の
パラメータを指定する事ができます。
paramValues()はパラメータを配列で取得/配列を設定します。

以下のメソッドは型変換を行った後の値を返すメソッドです。
Controllerクラスにもありました。パラメータの型もチェックしたい場合は
利用した方が良いでしょう。
asShort()
asInteger()
asLong()
asFloat()
asDouble()
asString()
asBoolean()
asDate()
asKey()

以下のメソッドは、それぞれのスコープの値を取得/設定します。
requestScope()
sessionScope()
applicationScope()

isRedirect()はリダイレクト先のパスが設定されていればTrueを返します。

今日は、薄い内容で終わるかと思ったのですが、最後の

addBlobKey()でハマりました。
これは、Blobstoreのテストを行うために、実行前にBlobstoreのKeyを設定
しておくものだと思うのですが、どうしても使い方がわからなかったので
slim3Demoのソースコードを見ました。

使い方としては、第1引数にブラウザからのパラメータ名を指定し、
第2引数には恐らく、BlobKeyの文字列を渡すのだと思っているのですが、
どうしてもテストがうまくいかない。
JUnitのトレースを見ると、以下のような表示になりました。
java.lang.ClassCastException: java.lang.String cannot be cast to java.util.List
at com.google.appengine.api.blobstore.BlobstoreServiceImpl.getUploads(BlobstoreServiceImpl.java:162)
at com.google.appengine.api.blobstore.BlobstoreServiceImpl.getUploadedBlobs(BlobstoreServiceImpl.java:135)
at slim3.demo.controller.blobstore.UploadController.run(UploadController.java:21)
at org.slim3.controller.Controller.runBare(Controller.java:111)
at org.slim3.controller.FrontController.processController(FrontController.java:491)
at org.slim3.controller.FrontController.doFilter(FrontController.java:277)
at org.slim3.controller.FrontController.doFilter(FrontController.java:237)
at org.slim3.controller.FrontController.doFilter(FrontController.java:199)
at org.slim3.tester.ControllerTester.start(ControllerTester.java:131)
at slim3.demo.controller.blobstore.UploadControllerTest.run(UploadControllerTest.java:20)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:44)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:41)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:28)
at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:31)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:76)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:193)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:52)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:191)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:42)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:184)
at org.junit.runners.ParentRunner.run(ParentRunner.java:236)
at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)
どうやら、キャストで失敗しているようで、バグなのでは?という結論で、
読書会としてはタイムアップ。

その後、Twitterで、「slim3DemoがSDK1.6.0だと動作した」という報告が
あったので、私も同様にslim3Demoをダウンロードした後、実行してみると
同じ例外で失敗する結果となりました。

私の環境はSDK1.6.2.1なので、何か変わったのかと思ってSDKのソースをみると、
確かに変わっていました。

以下のリンクは、SDKのソースコードのDiffです。

元々は、Map<String, String>で扱っていたようなのですが、
Map<String, List<String>>に変わってしまったようで、
最新のSDKだとaddBlobKeyを使っても
テストは動作しないという結果になってしまいました。

現時点では、Slim3の更新を待つか、
requestScopeに以下のKeyで型をあわせてセットすれば動くかもしれません。
com.google.appengine.api.blobstore.upload.blobkeys

という事で、次回もtester(MockXXX)から読み進めます。

次回は3/6(火) 19:00〜を予定しています。ご興味がありましたらどうぞ。



2012年2月22日水曜日

Slim3 Source Code Reading #9

今日はSlim3 Source Code Reading #9でした。

今回は読むのを忘れていたModelRefとAppEngineTestCase、AppEngineTesterの
辺りを読みました。

最初にModelRef.javaについて。
読み方は上から順番に読む感じでした。
まず、ModelRef.javaで気にしたのが、getModel()です。
例えば、Slim3本から引用すると、以下のような記述になります。
person.getAddressRef().getMode();
このgetModel()が、Memcacheから取得したインスタンスの場合
どうなるかが気になっていたからです。(課金的な面で。)

結果としては、Memcacheに入れてもModelRefに関してはデータストアへのオペレーション回数は減りません。以下のようにトランザクションがない、getを実行するからです。

model = Datastore.getWithoutTx(getModelMeta(), key);

ただし、これは初回だけで、2回目以降の呼び出しではメンバ変数を参照するため、getは実行されません。

また、getAddressRef()は、modelを更新する時にmodelMeta#modelToEntity()メソッド(これは自動生成される)を介してデータストアに保存されますが、データストアにはm.getAddressRef().getKey()で取れるKeyが格納されます。

次に、InverseModelRef.javaについて
これも上から順番に読み進めたのですが、ModelRef.javaと大体同じだったのですが、
getModel()が以下のようになっていました。

        model =
            Datastore.query(getModelMeta()).filter(
                mappedPropertyName,
                FilterOperator.EQUAL,
                key).asSingle();

これは、参照先のModelにプロパティを付加して、それを条件にQueryでデータを取得しています。ただ、1件ならKeyで取れば良いのに…という話になりましたが、よくわからなかったので、ひとまずスルー。

最後に、InverseModelListRef.javaについて
これも上から順番に読み進めました。このクラスは上記2つと違って、内部で、ModelQueryクラスのインスタンスを生成し、modelListを取得します。
複数のデータを取得するので、Filter、Sortも使えます。In-memoryのFilterとSortも使えます。

InverseModelRef、InverseModelListRefの両方とも、指定したプロパティ名のプロパティに格納されているKeyを取得するようになっています。

        public List getModelList() {
            Key key = getOwnerKey();
            if (key == null) {
                modelList = new ArrayList();
            } else {
                query.filter(mappedPropertyName, FilterOperator.EQUAL, key);
                if (!sortsSet) {
                    query.sort(defaultSorts);
                }
                modelList = query.asList();
            }
            return modelList;
        }

最後に、ControllerTestCaseか、AppEngineTestCaseをどちらを先に読むかを迷ったのですが、AppEngineTestCaseから読むことにしました。一般的に先にModelとServiceの作成、テストをするような気がしたので。

AppEngineTestCase.javaは、内部にAppEngineTesterのメンバ変数をもつ、
一般的なJUnitのメソッド(setUp、tearDown)があるだけでした。

したがって、AppEngineTesterを引き続き読み進めました。

AppEngineTesterは、色々ありました。

最初にstaticの初期化がありましたが、AppEngineUtil.isServer()の動きがよくわからなかったので、いろいろテストコードを書いて動かしつつ理解を深めました。
AppEngineUtil.isServer()については、SystemPropertyの"com.google.appengine.runtime.environment"がnullでなければtrueを返します。
これは、開発サーバだと"Development"、本番環境だと"Production"を返します。
(※詳細はドキュメント参照)
しかし、JUnitTestだとnullになります。(たしかに、JUnitTestはサーバではない。)
appengine-web.xmlに記述してみましたが、この値に関しては書き換えることができませんでした。(確認したのはJUnitTestでの実行のみです)
したがって、JUnitTestの時はサーバの環境が取り込まれていないので、初期化時に
必要なライブラリを取り込んでいます。
取り込んでいるのはApiProxyLocalImplとLocalDatastoreService、LocalServerEnvironmentのようです。

setUpでは、ApiProxyのEnvironmentとDelegateを退避しています。これは、恐らく、テストごとに初期化するためだと思われます。
メンバ変数のEnvironmentとDelegateはtearDownで元に戻されます。

tearDownでは、GlobalTransactionのロールバック、アクティブなトランザクションのロールバック、Memcacheのクリア、データストアに書き込みを行ったKeyのエンティティの削除、メールの削除が行われています。

ここで、最初に議論になったのが、「Commitしてしまうと削除されないんじゃないのか?」と
言う話をしました。パッと見た感じだと、deleteKeyを取得している部分がないからです。
が、結論を先に言っておくと、deleteKeyはDatastore#putを呼び出し、実行した時点で
登録されます。


これは、457行目の

ApiProxy.setDelegate(this);

で、自分のインスタンスをApiProxyのDelegateに登録しています。
これを行うことで、APIが実行されるたびに、Delegate<Environment>#makeSyncCall()が呼び出されるようになります。
Delegateはインターフェイスなので実装したクラスである必要がありますが、
AppEngineTesterはDelegateインターフェイスを実装しているので、
515行目のmakeSyncCall、599行目のmakeAsyncCallが呼び出される事になります。
あとは、中身を見ていくと、Service名で条件分岐してそれぞれのAPIに対する処理が
行われています。
Datastoreに関してはmakeSyncCall内でKeyを全て拾っているということになります。

それから、もう一つ重要だったのが、AppEngineTesterのenvironment変数です。
これは、ユーザの情報を持っていて、JUnitTestでUserServiceを利用する時に
重要です。
environment#setEmail()を使ってメールアドレスを設定しておくと、UserService.isUserLoggedIn()がTrueになり、認証済みの状態を作る事ができます。
注意する必要があるのは、emailを空にして、setAdmin(true)にすると、
”ログインしていないが、管理者権限がある”状態が作られてしまいますので、気をつけてください。
これを使うことで、Controllerのテストも可能になります。
同様にFederated Loginの情報も事前にセットしておけば、同じくテストすることが
できるでしょう。(※ただし、自分でセットすることになるので事前にFederated Loginした時の
環境は調査しておく必要があります)
他にもattributeにkeyと値をセットすれば同様に環境を作ることができます。

以下が、環境の動作確認で利用したテストコードです。
public class SampleTest extends AppEngineTestCase {
    @Test
    public void サーバチェック() {
        assertFalse(AppEngineUtil.isServer());
        assertNull(SystemProperty.environment.value());
    }
    
    @Test
    public void ユーザ認証テスト() {
        tester.environment.setEmail("test@example.com");
        tester.environment.setAdmin(true);
        
        UserService user = UserServiceFactory.getUserService();
        assertTrue(user.isUserLoggedIn());
        assertEquals("test@example.com", user.getCurrentUser().getEmail());
        assertTrue(user.isUserAdmin());
    }
}

他にも色々attributeにセットすると面白い事ができそうですが、
今日はここまでにしておきます。

次回は、2/28(火)です。続きでtesterを読み進める予定です。
https://sites.google.com/site/chugokugtug/sourcereading/slim3/10
ご興味がありましたらどうぞ。

2012年2月15日水曜日

Slim3 Source Code Reading #8

Slim3 Source Code Reading #8を開催しました。

先に言っておくと…
今回はJSPを読むのみで終わってしまいました。Seasarを使っている人には「ほぼ常識」と言わんばかりの内容しかないと思いますが、よろしければ読んで頂ければと思います。

ちなみに、私に取っては新発見のものもあって、適用しようと思っています。

jspで記述する場合はf:h()など"f:"を付けて呼び出します。

h()
h()は引数がString型の場合は、エスケープして出力します。Keyの場合はKeyFactory.keyToString()の結果が出力されます。
String、Key以外の型の場合は引数.toString()が実行されます。
したがって、エスケープの必要がある場合は、必ずh()を使いましょう。

url()
引数に":"が含まれる場合はそのまま出力します。(恐らく、URLが書かれていると判断する)
ここでハマったのですが、中でrequest.getContextPath();を取得しています。
これはコンテキストパスを取得するのですが、GAEだとコンテキストルートの設定が
ないので、必ず空になるのでは?という疑問がでました。
実際、開発環境でTwitterサンプルを動かして、中身を見てみたのですが、やはり空でした。
必ず空になるなら無視しても良いような気もするのですが、疑問が残ったままとなりました。
処理としては、":"が付いていればそのまま返す(URLが書かれている)
引数が空の場合、
requestの中に"slim3.basePath"が含まれていれば、その値を、
含まれていなければ、サーブレットパスを取得し、
コンテキストパス+取得した値を連結して返す。

引数の先頭が"/"で始まっている場合、
コンテキストパス+引数を連結して返す。

空でもなく、先頭が"/"で始まらない場合、
コンテキストパス+"slim3.basePath" or サーブレットパス+引数の値を連結して返す。
返すURLはエンコードされたURLになる。

blobstoreUrl()

BlobstoreServiceからBlobStoreへのアップロードURLとリダイレクト先URLを設定した
URLを返す。リダイレクト先URLの生成ルールはurl()と同じ。
※このメソッドではコンテキストパスを取得していない。

nbsp()
空白を&nbsp;に置き換える。

br()
改行を<br />に置き換える。¥r¥n→¥n→¥rの優先順位で置き換える。
エスケープと改行、空白の変換を全て適用したい場合は、
f:br(f:nbsp(f:h(contents)))としないといけない。brとnbspが入れ替わるとおかしくなる。
f:hを最後にしても<br>がでてしまうのでNG。

locale():ロケールを取得
timeZone():タイムゾーンを取得

text()
引数で指定したnameに対する値をrequestから取得し、name="key" value="value"という文字列を出力する。valueの方は値がエスケープされる。inputタグなどで利用すると便利
※XXXArrayという名前だと例外が発生する。

hidden()
中身はtext()

checkbox()
checkboxで利用する。
引数で指定した名前がrequestに存在すればchecked属性が付加される。
※XXXArrayという名前だと例外が発生する。


multibox()
複数のcheckboxで利用する。value="value"が付加される。値はエスケープされる。
引数で指定した名前がrequestに存在し、引数で指定した値が存在すればchecked属性が付加される。requestのにはListを入れておかなければならない。Listでなければ例外が発生する。
※XXXArrayという名前でなければ例外が発生する。

radio()
radiobuttonで利用する。value="value"が付加される。値はエスケープされる。
引数で指定した名前がrequestに存在し、引数で指定した値が存在すればchecked属性が付加される。
※XXXArrayという名前だと例外が発生する。

select()
optionタグで利用する。value="value"が付加される。値はエスケープされる。
引数で指定した名前がrequestに存在し、引数で指定した値が存在すればselected属性が付加される。
※XXXArrayという名前だと例外が発生する。

multiselect()
複数選択時のoptionタグで利用する。value="value"が付加される。値はエスケープされる。
引数で指定した名前がrequestに存在し、引数で指定した値が存在すればselected属性が付加される。
※XXXArrayという名前でなければ例外が発生する。

errorClass()
引数で指定した名前のvalidationのエラーがあれば、
引数で指定したCSSクラス名を設定する。
エラーがなければ空を返す。

errors()
validationのエラーなどを出力する。検証エラー全てが表示される。

key()とhiddenkey()はDeprecatedとなっています。
それぞれ、h()かhidden()を使いましょう。

request()
protectedのメソッドなので直接利用する事はありませんが、
HttpServletRequestオブジェクトを取得しています。

後は、JavaScriptの話題になりましたが、私もまだまだJavaScriptのスキルが足りないようなので、微妙な感じで終了しました。

私のWebの技術が普通の人レベルになるにはまだまだ遠いようです。

次回のSlim3 Source Code Reading #9は2/21(火)です。
ModelRefとtesterを読み進める予定です。
ご興味がありましたらどうぞ。



2012年2月7日火曜日

Slim3 Source Code Reading #7

今日はSlim3 Source Code Reading #7でした。

今日の範囲はValidatorの辺りを読みました。
Validatorは入力チェックを行うためのクラスで、以下の型のチェックと、
範囲チェック、長さチェック、必須チェック、正規表現チェックができるようになっています。

  • byte(Validators#byteType())
  • short(Validators#shortType())
  • int(Validators#integerType())
  • long(Validators#longType())
  • float(Validators#floatType())
  • double(Validators#doubleType())
  • Number(Validators#numberType())
  • Date(Validators#dateType())
型チェックはそれぞれ、Byte.valueOf()等を呼び出し、例外がスローされるかどうかで
判定されます。ただし、NumberとDateはそれぞれ、DecimalFormatとSimpleDateFormatクラスでparseした結果、例外がスローされなければOKとなります。

実は、dateType()に関しては気を付けないといけないケースがあります。
以下のテストコードで確認したのですが、日付が不正なケースでもOKと判定される事があります。

    @Test
    public void 日付の確認() {
        input.put("arg1", "2012-02-07");
        input.put("arg2", "2012-02-30");
        input.put("arg3", "2012-02-1a");
        input.put("arg4", "2012-02-0a");
        input.put("arg5", "2012-02-01");
        input.put("arg6", "2012-02-29");
        
        Validators v = new Validators(input);
        v.add("arg1", v.dateType("yyyy-MM-dd"));
        
        assertTrue(v.validate());
        
        // 存在しない日付
        try {
            Validators v2 = new Validators(input);
            v2.add("arg2", v2.dateType("yyyy-MM-dd"));
            
            assertFalse(v2.validate());
        } catch(Exception e) {
            assertTrue(true);
        }
        
        // 日付が不正(2012-02-1aは正しいと判定される)
        try {
            Validators v3 = new Validators(input);
            v3.add("arg3", v3.dateType("yyyy-MM-dd"));
            
            assertTrue(v3.validate());
        } catch(Exception e) {
            assertTrue(true);
        }
        
        // 日付が不正(その2)(2012-02-0aは間違いと判定される)
        try {
            Validators v4 = new Validators(input);
            v4.add("arg4", v4.dateType("yyyy-MM-dd"));
            
            assertFalse(v4.validate());
        } catch(Exception e) {
            assertTrue(true);
        }
        
        // 日付チェック(正しくはこう)
        try {
            Validators v5 = new Validators(input);
            v5.add("arg3", v5.dateType("yyyy-MM-dd"), v5.regexp("^\\d{4}-\\d{2}-\\d{2}$"));
            
            assertFalse(v5.validate());
        } catch(Exception e) {
            assertTrue(true);
        }
        
        Validators v6 = new Validators(input);
        v6.add("arg5", v6.dateType("yyyy-MM-dd"), v6.regexp("^\\d{4}-\\d{2}-\\d{2}$"));
        
        assertTrue(v6.validate());

        Validators v7 = new Validators(input);
        v7.add("arg6", v7.dateType("yyyy-MM-dd"), v7.regexp("^\\d{4}-\\d{2}-\\d{2}$"));
        
        assertTrue(v7.validate());

    }

arg3の"2012-02-1a"という文字列はSimpleDateFormatで変換すると、日付は正しくありませんが、例外がスローされず、日付のインスタンスが返されます。
従って、本来は正しくない日付なのに、正しい日付という判定結果が返ることになります。
したがって、後半のチェックのように、正規表現でのフォーマットチェックも行う必要があります。
※これは両方必要です。正規表現のみだと、arg2のような、存在しない日付を通してしまうからです。

範囲チェックは、Validators#longRangeとValidators#doubleRangeがあります。(小さい範囲の型も同様にこれでチェックすることになると思います)

使い方は、v.longRange(3,10);と言う感じになります。
これだと、3以上、10以下(3と10を含む範囲)がOKとなり、それ以外の値の場合はNGとなります。doubleRangeに関しても使い方は同じです。

必須チェックはValidators#required()を指定する事でチェックがかかります。
nullか""(空文字)の場合、NGとなります。

長さチェックは、Validators#maxlength()、minlength()を利用します。
これは、文字数(バイト数ではない)をカウントして、引数で指定した文字数を超える(maxlength()の場合)か、下回る(minlength()の場合)かすると、NGとなります。
カウントの方法は、String#length()の値を比較しています。

Validationについてはこんな感じで、大した量ではありませんでしたが、ここから、使い方の話題に移りました。

これも今日見つけたのですが、ControllerクラスにasInteger()など、それぞれの型でrequestパラメータを取得する便利なメソッド達があります。
本来の使い方としては、Validatorでチェックをした後、asXXX()を使って、
変数に取り込む使い方が正しい使い方だと思います。
※nullを許容する場合はnullの扱いについても考慮は必要なので、プリミティブ型ではなく、クラスの変数に代入しましょう。
request.getAttribute("param").toString()をわざわざ変換したり、requestScope("param")をわざわざ変換するのは、カッコ悪いという事でした。
(※私のプログラムを見ると、混ざった状態でひどい事が判明したのでリファクタリングします)

asXXX()には、asKey()とasMap()もあります。asKey()はKeyFactory#keyToString()の結果をKeyに変換し、asMap()はHttpServletRequestをRequestMapに変換してくれます。

その後、少しjspのFunctionを見ていたのですが、そこで、blobstoreUrl()を見つけました。
前回の#6の時にBlobstoreのサンプルを見せたのですが、最初のBlobstoreに格納するURLを取得する部分がjsp側でf:blobstoreUrl(リダイレクト先URL)と書くだけで実現
できそうです。

それから、このBlogの記事を+1してくれていた、shin1ogawaさんのGoogle+のコメントがある事に気がついて(今日言われて気がついた)、FileServiceとか、Google Cloud Storageなんかの話題も触れる機会になりました。

それから、Blobstoreの最大ファイルサイズですが、前回32Mと言っていたのですが、
Goの場合(http://goo.gl/1lvU2)だけで、Python/Javaだと2GBまでいけそう(http://goo.gl/mX5Dy)です。

また、私が作ったGAEのアプリケーションで妙なリダイレクト(302のレスポンスを返す)が発生しているケースがあったので、聞いてみたのですが、
どうやら、リクエストのURLの最後の"/"が怪しいようでした。
ログを詳細に分析すると、IndexControllerを実行するURLで、最後に"/"があると、
200を返すのに、"/"がない場合は302を返していました。
IndexControllerでなければ、そういう現象は見受けられませんでした。

したがって、IndexControllerを使う場合は、URLの最後に必ず"/"を入れておかないと不具合の元になりそうな予感がしました。
特に、requestScope的な部分で。

次回は、jspをもう少し見るのと、ModelRef、testerと順番に読んでいく予定です。

2012年2月1日水曜日

Slim3 Source Code Reading #6

今日はSlim3 Source Code Reading #6でした。

今日は、FileUploadの辺りを読む予定でしたが、
最近Slim3で開発を始めたので、それに対する疑問を聞いてみたり、BlobstoreAPIの
使い方を説明したりしました。

あとは、最近Google App EngineのアプリケーションをSlim3を使って
開発しているので、その時に疑問に思ったことを聞いてみたりしました。
先週と同じく、言いだしっぺの2人だったので、すぐ脱線してしまいました。

ちゃんと、FileUploadの部分も読みました。

が、HTTPのリクエストに対する知識がないと、意味不明な部分もあったりして、
Chromeのデベロッパーツールでhttp headerの中身をみたりしました。
個人的には十分な内容だったと思います。

では、一つ目のBlobstoreAPIを利用したファイルのアップロードについては、
以下の手順でファイルをBlobstoreに格納します。

  1. アプリケーションのファイル選択画面を表示する。
    1. ここで、POST先のURLはBlobstoreService#createUploadURL(url)を呼び出して、BlobstoreAPIで利用するURLを指定する。
    2. 引数のURLはBlobstoreにデータを格納後にリクエストされるURLを指定する。
  2. アップロード後に呼び出されるURLで、BlobstoreService#getUploadsを呼び出し、Blobstoreに格納したデータのBlobkeyを取得する。(複数ファイルに対応しているため、戻り値がMap<String, List<BlobKey>>の型になっている。)
    1. この時取得したBlobKeyは忘れてはいけない。(BlobKeyで格納しているデータにアクセスするため、BlobKeyを参照できないとデータを参照する事も、削除することもできなくなる。※ただし、admin consoleからなら削除可能)
  3. BlobKeyを引数にBlobstoreService#serve()を呼び出すことで、HttpResponseにBlobstoreに格納されているデータを書きこんでくれる。
    1. 画像であれば、<img>タグのsrcに指定することで、画像が表示される。
    2. 動画であれば、<video>タグのsrc
    3. 音声であれば、<audio>タグのsrc
  4. 同じく、Blobkeyを引数にBlobstoreService#delete()を呼び出すことで、Blobstoreのデータを削除する。ただし、Entityに格納している場合は、Entityの中身(BlobKey)を読みだした後に削除しないと、BlobKeyが参照できないのでdeleteできなくなる。
ちなみに、私のGoの本を参考にサンプルプログラムを作りましたが、(私だけかもしれませんが)意外と作れます。(Go→Javaができたという事は、その逆もまた可能なはず)

BlobstoreAPIはファイルサイズが最大32Mバイトまでとなっていますので、
大きなデータは格納できません。

脱線した話として、

  1. 自作のControllerに親クラスを作ったりするかどうか。
    1. これはやる。Sessionなど、全体に渡って取得する場合はやる。
  2. データストアから読みだした後、表示用に編集した値を保持するクラスをどこに作るか。
    1. 特にルールはないので、どこでもいいんじゃないか。
    2. Modelで編集用のデータを返すメソッドを定義する
  3. Keyのnameに名前を格納しておくと結構いい感じ。
    1. エンティティを読み出すのとKeyのみ読み出すのとでは、コスト的に7分の1
  4. とりあえず、良く使うデータはmemcacheに突っ込んどけば良いんじゃないかな?
    1. 更新頻度の問題がある。
      1. インスタンスが停止する場合。→所詮その程度なので安く収まるのでは?
      2. 結局は運用してみないとわからない。
  5. 課金設定にしたら、9ドル/月
    1. 最大9ドル/日と勘違いしていた。(1日9ドルだと月2万以上…)
    2. 無料枠を使い切る→9ドルの枠でがんばる→枠を越えたら…。
  6. Javaで作ったアプリケーションの最初の起動は遅い…
    1. Javaは初回のアクセス(インスタンスを起動する)時は一呼吸あるけど、Goだとそれはない。
最後にFileUploadの話題

  1. FrontControllerで、Content-Typeを確認して、"multipart/"で始まるかどうかをチェック。
    1. "multipart/"の場合、ファイルがアップロードされてきた判定。
      1. この場合、MultipartRequestHandlerのインスタンスが生成される。
    2. その他は、普通の(?)リクエスト
  2. MultipartRequestHandler#handleで、FileUploadクラスのインスタンスを生成する。
    1. FileUploadクラスで、ファイルのサイズの制限設定を読み込む。
      1. 現時点のblankプロジェクトの設定はコメントアウトされているので初期値が使われる。
      2. 初期値は-1なので、「制限なし」
    2. この中で、いろいろな処理をして、FileItemクラスのインスタンスを生成する。
      1. MultipartRequestHandlerはかなり頑張ってます。
      2. ちなみに、ファイルを読み込む時のバッファは8 * 1024(8192)バイトです。ファイルが大きいと実行されるステップ数も増えます。
    3. 最終的にControllerでFileItem item = requestScope("formFile")の1行でファイルが取得できる。
  3. Serviceでアップロードされたデータを格納する処理を実行すれば、Datastoreにファイルが格納される。
個人的に思うこと。

  1. BlobstoreAPIを利用したほうが良い。
  2. ただし、BlobstoreAPIはファイルの中身をチェックできない
    1. UploadはAPI用のリンクへのリクエスト
    2. downloadは直接HttpResponseに書きこまれる
  3. したがって、中身をチェックしたい場合は、Slim3のFileUploadを使うほうが良い。
  4. あと、課金状況で判断するのも必要かもしれない。
  5. 32Mバイトを超えるファイルを格納する可能性があれば、BlobstoreAPIは使えないのでSlim3のFileUpload一択。(※ただし、そんなに大きなファイルを格納しようと思うと、30秒の時間制限を考える必要がある)

次回の#7は2/7(火)19:00〜の予定です。
少人数で行っているので制限が厳しいですが、ご参加をお待ちしております。

2012年1月25日水曜日

Slim3 Source Code Reading #5

今日は、Slim3 Source Code Reading #5の日でした。

今回も@sinmetalさんと2人での開催となりました。
内容は、

  1. 私の仕事でGoogle App Engineを使ったアプリケーションを作ることになったのでその質問
  2. Memcache.javaを読み進める。
  3. UserServiceを使った認証についての雑談
  4. org.slim3.datastoreパッケージのCreationXX.javaについて

でした。

1.は単純に仕事で使う事になった(した)ので、アプリケーションを開発している話を少ししました。

2.が本題のMemcacheの機能です。
方向性として、Memcacheの存在確認、MemcacheへのGet、Putを中心に読みました。
(今、deleteも確認しましたが、deleteもラッパーでした)
読み進めた所、Memcacheの存在確認はcontains(key)、Memcacheへの登録はput()を
それぞれ呼び出しますが、中身は、MemcacheServiceのメソッドを呼び出している
だけでした。

しかし、Getだけはそのままではなく、MemcacheDelegate.javaの214行目から
始まる、getInternal()でいろいろ処理されています。
MemcacheServicePb.MemcacheGetRequestクラスのインスタンスと、
MemcacheServicePb.MemcacheResponseクラスのインスタンスを生成し、
233行目のmakeSyncCall()を呼び出します。

このmakeSyncCall()は中で、ApiProxy.makeSyncCall()を呼び出し、
(恐らくSDKよりも下の)APIを呼び出し、Memcacheに登録しています。
MemcacheはKey-Value形式で、しかも、Valueはbyteデータを格納する事に
なっていますので変換処理が必要ですが、オブジェクトをbyteデータに変換する処理も
このあたりで行われているようです。

なぜ、このようにGetだけ、ただのラッパーではなかったのかは、分かりませんでした。
(恐らく、AppEngineのAPIを実行すると処理が遅いとか、無駄な待ち時間がかかるなど
あるのだろうと判断して終わらせました)

その次は、Memcacheの次に読み進めるのをどれにするか選んでいた時に、
ユーザ認証の話になり、私が、UserServiceのすばらしさを一方的に話した感じでした。

利点として、

  1. Googleの認証機構に全てお任せできる。
  2. 認証済みであれば、ユーザ情報が取れるので、利用できる。(Mail APIによる通知や、ユーザの識別、データの管理、アクセス管理など)
  3. データストアに(多分)SessionIDなどを登録する必要がなくなる。


1つ目は自力で認証処理を作る手間が省ける。かつ、(多分)自分よりも質の良いセキュリティが確保できる。しかも、Gmailが見れる状態(すでにログイン済み)だと、
アプリケーションの認証の処理が不要な場合もでてくる(入力の手間が減る)。
2つ目は自力認証の必要が無いので、ユーザ情報の有無で表示の制御ができる。何らかの処理の完了通知などにも利用できる。限られたユーザのみ見せるという機能も簡単に作れる。しかも、認証情報をデータストアに残す必要もないので、データストアのオペレーションの削減に繋がる。
3つ目はSlim3だとSession情報をデータストアに格納している(?)ということなので、
cronなどで、データストアの不要な情報を削除する処理を実行しなければならないが
その手間が省ける。データストアのオペレーションにも影響する。

ということが考えられます。

最後に4つ目のCreationXX.javaについてですが、org.slim3.datastoreパッケージに
CreationUser.java
CreationDate.java
CreationEmail.java
ModificationUser.java
ModificationDate.java
ModificationEmail.java

というファイルが存在しています。
このファイルはAttributeListenerインターフェイスを実装しているのですが、
何のためにあるのかよくわからないファイルでした。

が、実は(ドキュメントにも書かれているのですが)、アノテーションを設定すると、
ModelMetaで、データストアにPutする前に、現在の日付や、ユーザ情報を自動的に
取ってきて、プロパティに設定してくれます。
(Serviceで設定する必要がありません)

    @Attribute(listener = CreationUser.class)
    private User user;

上記のようにすることで、勝手に登録されます。
CreationXXとModificationXXの違いは、
CreationXXはすでに設定済みかどうかを判定し、設定されていない場合のみ登録されます。
ModificationXXの場合は、内容にかかわらず、値を上書きします。

次回は1/31(火) 19:00〜で、util系を読み進める予定です。

2012年1月18日水曜日

Slim3 Source Code Reading #4

今日はSlim3 Source Code Reading #4を開催しました。
参加者が私と@sinmetalさんの2人だったので、当初想定していたメンバーになりました。

今回は、

  1. EntityGroupとQueryについての前提知識の共有(というよりは、前回間違った説明があったのでその辺りの共有)
  2. Transaction、GlobalTransactionのソースコードを読み進めました。
  3. EntityGroupに関する実装上の話題。構成とか。Ancestorクエリにするケースなど。
  4. sinmetalさんの「サービス単体でテストコードを動かすと、Cross Group Transactionが正常に動作するのに、プロジェクト全体のテストを行うとテストに失敗する件」

を行いました。

Transactionについては、Datastore.javaにあるDatastore#beginTransaction()を読み進めました。
ただ、この中身は、Google App EngineのTransactionAPIを利用しているだけで、Slim3で特別何かをしているということはありませんでした。
ただし、TransactionのOptionでSystem.getProperty("slim3.useXGTX")の結果がTrueである場合、Cross Group Transactionが有効になります。
System.getProperty("slim3.useXGTX")はappengine-web.xmlに記述してある値を取得します。※現時点ではデフォルトでは記述がないので、値はfalseです。

GlobalTransactionに関しては、かなり大変な事をしてあるので、概要だけ説明を書いておきます。
(sinmetalさんが検証コードを書いて試してくれるという事なので、その結果を待ちます)
実際、GlobalTransactionが必要になる事はこれからは少なくなってくると思いますので
(現時点で既にDeprecated)Cross Group Transactionを利用した方が良いと思います。

具体的にはどうしているかと言うと、

  1. トランザクションを開始する。Datastore.beginGlobalTransaction();
  2. エンティティをGet or Putする時、Lock用のキーMapに、現在のキーの祖先が存在しているかを確認する。存在していれば、例外をスローする。ただし、存在していても、キーのTimestamp+30秒を超えていれば無視する。
  3. GlobalTransaction#Get or Putを呼び出す。その時、対象のエンティティの祖先のキーを取得し、Lock用のMapに退避しておく。キーにはTimestampを保存しておく。また、Getの場合はKeyのみ、Putの場合はKeyとEntityのペアをJournal用のMapにPutする。
  4. トランザクションをCommit or Rollback(GlobalTransaction#commit、rollback)する。この時、GlobalTransactionの場合は、Journal用のMapからGet用のKeyを削除し、Put用のEntityをslim3.GlobalTransactionカインドにPutする。Get用のKeyにはEntityが登録されていないので、Entity==nullであれば、Get用のKeyと判定する。ただし、単一EntityGroupのみの場合(Lock用のMapが空)はLocalTransactionと判断しそのままCommitする。
  5. GlobalTransactionの場合、TaskQueueにキューを登録する。登録するキューはGlobalTransactionServletへのリクエストとして登録する。
  6. GlobalTransactionServletで、RollForwardトランザクションの場合、Commitされた場合は、GlobalTransaction用のカインドにある、KeyとEntityのペアを取り出し、本来のカインドのエンティティにPutする。Rollbackトランザクションの場合は、カインドから対象のエンティティを削除する。

大体の処理は上記のようになっています。
正確な処理については、大変ですが、ソースコードを読んでみてください。

最後にsinmetalさんから相談された、「単体テストでHRDのテストが動作しない件」ですが、見せていただいたテストコードには、setUp()でSystem.setProperty("slim3.useXGTX", "true");の
記述がありましたが、この設定を反映するAsyncDatastoreDelegateが何度もインスタンスが生成されるようであれば良いのですが、メンバとして保持している、
Datastoreクラスがstaticであるため、実行開始後、1回しか設定を読み込まないようになっているようです。したがって、System.setProperty()が呼び出される前にDatastoreクラスのメソッドが呼び出されてしまうとその時点でデフォルトのトランザクションオプションが反映されてしまって、System.setProperty()が意味をなさなくなってしまいます。

これについても検証コードの結果を待ちましょう。

ただ、この後にも話をしたのですが、開発環境HRDのオプションの遅延は、
かなり大きいので、Queryのテストをしたい時にまでHRDの遅延が発生すると
テストが通らない問題に直面してしまいます。
一つは、遅延確率を設定するオプションを0にして遅延しなくするという方法を
提案しましたが、テストクラスの本数が多くなってくると面倒という問題があるようです。
また、Thread.sleep(2000)とか入れてみるという提案もしたのですが、
これも同様に本数が増えてくると、テストの実行時間が長くなってしまうので
全体のテストを行う時に問題になる。ということでした。

今日は、時間も長めにやりましたが、内容も濃かったです。
疲れました。

2012年1月11日水曜日

Support Packageを使ってFragmentを利用する

先日、私が公開している「サポセン前の問診票」アプリケーションにFragmentを適用した
形に作り替えたので、その時に発生した現象と、Framgentの使い方をメモしておきます。

前提:
※Support Package v4を利用する。

以下が大体の手順です。

  1. レイアウトXMLにFragmentを含むレイアウトを定義する。
  2. FragmentActivityを継承したActivityのクラスを作成する。
  3. 普通に起動。


だけです。終わり。というと内容が薄いので、

レイアウトは以下のような感じになります。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <FrameLayout
        android:id="@+id/framelayoutmain"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" >
     <fragment
         android:id="@+id/fragment1"
         android:name="com.sample.ui.MainFragment"
         android:layout_width="fill_parent" android:layout_height="fill_parent" />

    </FrameLayout>
</LinearLayout>

Activityのソースコードはこれまで通りとなります。

public class MainActivity extends FragmentActivity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }
}

Activityが実行された時点でFragmentも呼び出されて表示されますので、
Fragment外で処理する事が無ければ、Activity側では何も記述しなくても良いです。
ポイントとしてはFragmentActivityを継承しないと実行時にエラーになってしまいます。

あとは、Fragmentのプログラムの方で処理するプログラムを
実装すれば良いです。

次に、FragmentをBackStackに積む方法は以下のコードです。

SubFragment fragment = new SubFragment();
FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.replace(R.id.framelayoutmain, fragment);
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
ft.addToBackStack(null);
ft.commit();

FragmentTransactionクラスがFragmentのBackStackの状態を管理しているようです。
ft.replaceでレイアウトに配置されているFragmentを置き換えます。
これは、Fragmentが複数配置されている場合でも、それぞれのFragmentが配置されているViewを指定して置き換える事ができます。
setTransitionは置き換え時のアニメーションを指定します。
addToBackStackは置き換え後の状態(アクティビティ全体)をBackStackに積みます。
最後にcommitを実行して確定します。

注意点として、ft.replaceした場合、下にあるFragmentの内容が見えてしまいます。
(背景が透明になる)
したがって、背景を画像や色を指定して、下のFragmentが見えないようにしないと
いけません。(これはSupport Packageのみの現象?)
しかも、ListFragmentを利用した時(他のFragmentでも?)、画面をタッチすると、下に配置されたFragmentもタッチを検出しますので、処理が実行されてしまいます。
BackStackに積む前に下のFragmentの処理が実行されないようにEnabled = falseにするか、
getFragmentManager().getBackStackEntryCount()をチェックして、積まれていたら
処理をスキップするなど工夫をしないといけないようです。

Google+に書き残したコメントです。

https://plus.google.com/u/0/114183076079015753160/posts/cyodhHmgRy9
https://plus.google.com/u/0/114183076079015753160/posts/3kDyzNe1mej
https://plus.google.com/u/0/114183076079015753160/posts/UHcTUgp7Hky

Slim3 Source Code Reading #3

Slim3ソースコード読書会(3)を開催しました。

今回は、前回のPutとかぶっていますが、以下のソースコードを読みました。
Datastore.java
DatastoreDelegate.java
AsyncDatastoreDelegate.java
ModelQuery.java
AbstractQuery.java
DatastoreUtil.java

そこで話題になったポイントをメモしておきます。

1点目。
AsyncDatastoreDelegate.javaの642行目で、

mm.validateKey(key);を呼び出し、KeyのKindとModelMetaで保持しているKindを
比較し、一致しなければIllegalArgumentExceptionをスローしている件。

これは、以下のように、取得しようとしているModelとKeyが一致しない時などに、
エラーにするための防波堤ではないか。という結論になりました。

例えば、以下のようにTweetクラスとTweet2クラスが存在したとして、
プロパティの構成が全く同じ場合で、データストアにKeyが存在している場合。

Tweet tweet = new Tweet();
Datastore.put(tweet);
Tweet2 tweet2 = Datastore.get(Tweet2.class, tweet.getKey());    // 指定したKeyとModelが不一致

データストアからのGetは成功しますから、構成が同じであれば、Tweet2クラスの
Modelとして値が取得されます。しかし、これは本来Tweetのデータであるため、
Modelが取れてはいけないのですが、データストアの仕組み上防ぐことができません。
したがって、ModelMetaで定義しているKindとKeyのKindを比較して一致しているか
どうかを確認していると思われます。

2点目。
ModelQuery.javaの241行目のasList()で最後にfilterInMemory()とsortInMemory()を
呼び出していることについて。

最初は、「DatastoreのQueryでsortやFilterを指定したらメモリ上でのソートやFilterは不要だろう」と
いう意見だったのですが、よく読んでみると、呼び出し元でfilterInMemory()を
呼び出していなければ、inMemoryCriteriaのSizeが0なので、実際には
Filter処理は実行されません。同様に、sortに関してもinMemorySortCriteriaの
Sizeが0であればSort処理も実行されません。

ただし、データストアでSortを実行したあと、sortInMemoryを呼び出していれば、
両方とも実行されることになります。

恐らく、inMemoryFilterについてはOR条件で抽出したい場合があれば、
それを実現するために利用するのではないかという結論になりました。
(AND条件での検索であれば、QueryにFilterを設定するだけで良いが、データストアでOR条件は指定できない。)

次回は1/17(火)19:00〜の予定です。

2012年1月10日火曜日

Slim3 Source Code Reading #3の前に(その2)

午前中にGetについての記事を書いたのですが、
Queryについてもメモを残しておきたいと思います。

実はQueryについては読む予定は無かったのですが、
開発環境での問題なのか、Putした結果が反映されるのが遅いという現象が
発生したので、Datastore.query(t).asList();という書き方がおかしいのか?と
思って、読み進めてみました。
実際はGetと同じくデータストアの読み取りなのでこちらも読んでおくべきと
思ったのでメモしておく事にします。

先に概要をメモ

  • Datastore.query(class)を呼び出すとDatastore.java3965行目が呼び出される。
    • 中ではDatastoreDelegate.query(class)を呼び出すので、DatastoreDelegate.java2397行目が実行される。
      • この中身はAsyncDatastoreDelegate.query(class)を呼び出すだけ。
        • AsyncDatastoreDelegate.java1940行目が実行される。
          • 中身はModelQueryクラスのインスタンスを生成してreturn
            • コンストラクタの引数は、AsyncDatastoreServiceとModelMetaクラス
            • ModelQueryクラスのコンストラクタでappengineAPIのQueryクラスを生成する。
まとめると、「Datastore.query()はModelMetaで指定したKind名のQuery」を生成する。
という簡単なものでした。

では、asList()メソッドを呼び出すとどうなるか。
ModelQuery.java241行目が呼び出されます。

最初にappengineAPIのPreparedQuery#asList(options)が呼び出され、結果が返ります。(トランザクションがセットされていればトランザクションも使われます)

そして、PreparedQuery#asList()で取得された結果(Entity)をModelMetaに変換し、
ModelMetaからModelクラスへの変換を行います。(取得された件数分全て)
ここで、Model#postGet()も呼び出されますので、このタイミングで編集処理が
実行されます。
よって、データ数が多いときには、適切にLimit指定を行わないと、
「表示しない(使わない)データを処理する」ことになるケースもあるということになります。

Slim3 Source Code Reading #3の前に

本日、Slim3 Source Code Reading #3を開催します。

恐らく、今日はGetとTransactionを読む予定になると思いますが、
Queryについても読み進めておく必要がありそうです。

まず、Getから。
GetはDatastore.java835行目の
public static <M> M get(Class<M> modelClass, Key key)
から読み進める事になると思います。

getメソッドの中では、メンバに持っているDatastoreDelegate#getが呼び出されます。(838行目)
これは、putの時と同じです。

DatastoreDelegate#getはいくつかメソッドがありますが、
748行目→769行目→771行目でカレントのトランザクションを取得し、
カレントのトランザクションを利用して1099行目を呼び出します。

1099行目のgetも、中身は非同期のgetAsync()を呼び出すだけです。
return FutureUtil.getQuietly(async.getAsync(tx, modelMeta, key));

FutureUtil.getQuietly()はFuture#get()を呼び出し、非同期の結果を受け取る+例外をキャッチするのみですので、結局「非同期で処理されるのを待つ」という感じでしょうか。
(これは2回目で読んだPutと同じです)

async変数はAsyncDatastoreDelegateクラスなのでそちらに進みます。

AsyncDatastoreDelegate#getAsync()はAsyncDatastoreDelegate.javaの630行目に
あります。
この中では、560行目のgetAsync()をwrapしているだけです。
560行目のgetAsync()は、中で1439行目のgetAsMapAsync()を呼び出します。
ここでは、DatastoreUtil#getAsMapAsync()を呼び出しているだけです。
DatastoreUtil#getAsMapAsync()はAsyncDatastoreService.getを呼び出し、データストアから実際に読み取ります。

ここまでの流れです。

  • Datastore#get()からDatastoreDelegate#get()を呼び出す
    • 748→769→771で、カレントのトランザクションを取得し、1099行目
      • FutureUtil.getQuietly(async.getAsync())
        • AsyncDatastoreDelegate.javaの630→560
          • 1439行目のgetAsMapAsync()を呼び出す。
            • DatastoreUtil.getAsMapAsync()を呼び出す。
              • AsyncDatastoreService#get(tx, keys)
まとめると、カレントのトランザクションで非同期APIを呼び、データストアからエンティティを呼び出す。

エンティティ→modelへの変換はAsyncDatastoreDelegateの640行目で
行われています。
この時点で、データストアから読みだしたタイミングで値を編集するpostGet()も呼び出されます。

明日以降の授業の準備もあるので、今日はここまで。

2011年12月27日火曜日

Slim3 Source Code Reading #2

今日は、Slim3 Source Code Reading #2を開催してきました。
今回はModel関連でDatastore#putを読み進めました。

読み進めたソースコードは以下のファイルです。

  • Datastore.java
  • DatastoreDelegate.java
  • FutureUtil.java
  • AsyncDatastoreService.java
  • DatastoreUtil.java


以下、その時のメモです。
DatastoreUtil.java 952行目
modelmeta.assignKeyToModelRefIfNecessary(ds, model);

エンティティ間の紐付けを行うための処理を個別に実装する。
(自動生成された時点では、中身は空)
※現時点では紐付けたサンプルコードがないので確認できず。。

ModelMeta.java 518行目
prePut()

データストアに格納する前に何か処理をしたい場合は、個別に実装する。
(自動生成された時点では、中身は空)
多分、postGet()とペアで見せる時とデータの中身が変換されないと
いけないケースが記述する?

DatastoreUtil.java 532行目
assignKeyIfNecessary()
エンティティにキーがなければキーを生成している。
ここで、親エンティティが存在している場合は、親のキーを設定して
紐付けしたキーを生成する。
Idは自動生成。

Datastore.put()で例外が発生した場合、(データストアのPutで例外が発生した時)
どうするの?(例えば、データストアの書き込みタイムアウトなど)

Slim3本のP.212中段にデータストアのリトライをSlim3が行うとなっているが?

DatastoreTimeoutException.javaはSDKに存在しているが、
RuntimeExceptionの扱いになっているのでドキュメントにthrowsの記述がない。

Slim3 1.0.6の時点ではリトライ処理(最大10回)が存在したようだが、
今日のソースコードを読んだ範囲では見つけられなかった。
(どこにもcatchがない?)

------

解散後の話題として@sinmetalさんが、Slim3のリポジトリから過去のソースコードを
取得して確認してくれたのですが、メモにある記事のように10回のリトライの処理は
確かに存在していました。しかし、現時点ではその処理は存在していません。
恐らく、AsyncDatastoreServiceの中(よりネイティブに近い所)でリトライ処理と
等しい事が行われていると予想されます。
(そうでなければ、処理を削除する理由がない)

次回#3は1/10(火)19:00〜の予定で、トランザクションに行く前に、Getを読みすすめます。

2011年12月25日日曜日

Slim3ソースコード読み会(2)の前に。

次回のSlim3 Source Code Reading #2が12/27(火)に開催されるのですが、
次回はmodel周りを読んでいく予定になっています。

ただ、model周りはデータストアへの読み込み、書き込みがあるため、
奥が深いので、事前に少し読んでおこうと思って読んでみました。

ひとまずメモを残しておきます。

  • Slim3はAsyncDatastoreServiceを使ってデータストアに書き込みを行っている。
非同期での書き込みなので、何もしなければ更新がすぐに反映されない(表示されない)時がある。

  • ModelクラスからModelMetaクラスを生成してデータストアに書き込みを行う。
    • ModelMetaクラスはVersionを持っている(データストアに書き込まれる)
      • Versionはputのたびにカウントアップされる。これはなぜ?
    • 同じく、SchemaVersionも持っている(データストアに書き込まれる)
      • 生成された直後は固定で"1"を書きこむ
      • SchemaVersionはプログラムの更新により、エンティティの内容が混在する時に新旧のバージョンのエンティティを判定するのに使う?
      • Modelのプロパティが追加される時にSchemaVersionも変更すべき?
  • Model→Modelの生成手順
例:slim3.model.Tweet
1. クラス名(フルパス)から".model."を探す。
2. 見つかった場合(lastIndexOf()の戻り値が0以上)
".model."より前、(今回の場合、"slim3") + ".meta." + "Tweet" + "Meta"にする
3. 見つからない場合は、そのまま+"Meta"にする
4. 2、3で生成したクラス名でインスタンスを生成する。
5. 1度生成されたMetaクラスは変換前のクラス名をキーにしてConcurrentHashMapに保存される。(2度目以降はHashMapにあるインスタンスを読みだして再利用する)