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月19日日曜日

Goの新しいコマンドとパッケージ管理

最近Go言語がVer.1に向けて開発されているようなのですが、
Weekly-2012-02-07辺りからコマンドが変わったみたいなので、
変更されたコマンドを少し使ってみようと思います。

まず最初に、環境変数$GOPATHを定義します。

この$GOPATH変数で指定されたパスに、ビルドしたパッケージのモジュールと、ソースコード、テストコードをひとまとめにして置いておきます。
ちなみに、GOPATHはPATHと同じように複数の場所を指定することも可能なようなので、
ソースコードをまとめる場所は複数作る事もできます。
ただし、go installコマンド(後で実行例を紹介しますが)は指定した最初のパスにモジュールを反映するようなので、注意が必要です。

さて、最初にHello Worldから。以下のソースをhello.goに保存しておきます。

package main

import (
 "fmt"
)

func main() {
 fmt.Printf("Hello World!\n")
}

上記のおなじみのコードを実行するには、以下のコマンドを実行するだけです。
以前は、8g hello.go && 8l -o hello hello.8 && helloみたいな感じでした。

$ go run hello.go

このコマンドを実行すると、コンパイル、リンク、実行まで一気にやってくれます。
多分、無事"Hello World!"が表示されると思います。
ちなみに、

$ go build -o hello hello.go

だと、hello.goと同じディレクトリに実行ファイルが生成されます。
go runコマンドだと実行ファイルは作成されませんでした。
(正確にはファイルが残っていなかった。どうしているんだろう?)

例えば、以下のようにパッケージを含むプログラムを作りたい時。

package main

import (
 "fmt"
 "mymath"
)

func main() {
 fmt.Printf("Hello World!\n")

 fmt.Printf("簡単な計算:1 + 1 = %d\n", mymath.Add(1, 1))
}

パッケージのファイルを$GOPATH変数で指定したパス配下のsrcディレクトリに
格納します。今回の例ではmymath.goというファイルで保存しています。

package mymath

func Add(a, b int) int {
 return a + b
}

保存した後、以下のコマンドを実行すると、パッケージがコンパイルされ、pkgディレクトリに生成されます。GOPATH変数にパスを複数指定している場合は、最初のディレクトリに格納されていると思います。

$ go install mymath

go installコマンドを実行した後、再度、go run hello.goで実行すれば、
簡単な計算もされることと思います。

しかも、Go言語標準のテストパッケージについても簡単に実行できるようになりました。
今回は、パッケージのテストを実行しています。
まず、mymath_test.goというファイルをmymath.goと同じディレクトリに作ります。

package mymath

import "testing"

func TestAdd(t *testing.T) {
 const ans = 2
 if Add(1, 1) != ans {
  t.Errorf("Add(%d, %d) == %d", 1, 1, Add(1, 1))
 }
}

テストパッケージは先頭がTestで始まり、最初が大文字(例:TestXxxみたいな感じ)の必要があるので、関数名はTestAddとしています。
あとは、同じパッケージのメソッドを呼び出し、結果を比較しています。
基本的にNGの条件を記述すれば良いです。
JUnitだと、正しい条件を記述するのですが、GoのTestingは反対なので注意してください。(反対というより、エラーを通知する関数しかないのか。)

プログラムを作成した後は、以下のコマンドで結果を確認します。

$ go test mymath

結果が正しければ、okとだけ出ます。

という感じで、パッケージのテストコードも実行しやすくなりました。

実行結果はこのBlogには載せていませんので、皆さんの環境で実行した結果をみて
検証してみてください。

ちなみに、GAE/GのSDKではまだこの仕組は導入されていませんのでGo言語の
ソースコードをダウンロードしてコンパイルするか、バイナリをダウンロードしてみてください。

注意点としては、私の書籍を購入してGAE/Gのプログラムを動かした方は
SDKのディレクトリにパスを通していると思います。これが、今回は邪魔になるので、
$PATHからSDKのパスを外してから動かしてみてください。

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月13日月曜日

第12回中国GTUG勉強会@岡山を開催しました。

第12回中国GTUG勉強会@岡山
https://sites.google.com/site/chugokugtug/event/12

を無事開催してきました。

今回は、発表者が初めての方にお願いをしたのですが、
皆さん、私の最初の時と違って、上手に時間配分も話もこなしていたと思います。

しかも、岡山開催では、一番多い人数の24人の方にご参加いただきまして、
大盛況だったと思います。

まだ、資料は公開されていませんが、そのうち資料が公開されたり、
Blog記事等が公開されると思いますので、当日の様子はそれをお待ちいただきたいと
思います。

今回はこういうイベントが重要だと思ったのが、Slim3 Source Code Readingなど
小さいイベントに対する意見が聞けたり、新たな発見が多いと思いました。

ちなみに、Slim3 Source Code Readingは最近開催時間が速いから参加できないという
指摘を受けました。
実は、小さいイベント(参加者も5名まで)なので場所も当日決定するスタイルを取っています。
その時の申し込みのメンバーを見て、岡山駅前なのか、もう少し南の方の場所なのか、
開催時間も早くしても良いのかを判断しています。
(例えば、2人の申し込みで、2人が揃っていれば待つ必要はないと思うので)

という感じでやっているので、参加してみようと思った方はお手数ですが、早めの
登録を行うか、私宛に連絡があれば、柔軟に対応するつもりです。

それと、「途中からだと参加しづらい」という事に対しては、一応、パッケージ毎で
区切って読み進めているので、途中からでも大丈夫だと思います。
(関連する所は、再度戻って読んでいたりします)

毎週やっているSlim3のソースコード読書会ですが、1周回ったら別のソースコードを読んでみるというのも良いかな?なんて思っていたりします。

読みたいソースコードはいろいろあって、Chromeとか、今日も触りましたが、BootstrapなどのHTML/CSSフレームワークとかですね。

あと、小さいイベントという事で、平日の夜の開催で2時間程度でハンズオンか、発表を小さくやってみるみたいなのもいいかなと思ったり。

基本、「オレオレ」勉強会のプランを考えているので、発表の講師は私が担当の予定です。(当然、内容も私がやりたい事をやる感じで)
ただ、ご意見等は取り入れようと思っています。

小さなイベントのプランを考えたきっかけは、岡山だとコミュニティが沢山できてきて、
週末(特に土曜日)のイベントの開催日の設定が難しいと感じているからです。
参加する人も毎週のように勉強会があると大変だと思いますし、
(懇親会で本気を出す方だと、毎週あると財布がブレイクすることになりますし…)
中国GTUGへの参加を回避するという選択に至る可能性もでてくるかもしれません。
(これに関しては、私自身がいろいろあってそういう状況に陥ってたりするわけですから文句は言えません。。)

もう、「重複覚悟」も視野にいれないとだめなのかもしれません。
(これは本当に切羽詰った時、(GTUGの場合だと3ヶ月の壁が迫ったとき等)に考える事になりますが…)

日曜日開催という意見もいただいたのですが、開催できても、前日に別の勉強会に
参加して次の日も!とはなりにくいかなと、個人的には思っています。
(私は独身ですから、プライベートな用事以外にも家の事もしなければいけませんし…)

という感じで、いろいろ考えてみたのをBlogに書き残してみました。
次回はまだ決めていませんが、思いついたら告知したいと思います。

2012年2月10日金曜日

CSS3でアニメーションさせる。

今日はPHPの授業で作れそうなものを探している途中に、
アニメーションを試してみました。
これは、Chromeだと動きますが、Firefox(10でも確認しましたが)だと動かないようです。

@keyframesの設定と、-webkit-animation-nameを指定することで、
スタイルを指定したElementがアニメーション
するようになります。

サンプルコードです。
sample.html
<!DOCTYPE html>
<html>
  <head>
 <meta charset="UTF-8" />
 <title>テキストアニメーション</title>
 <link rel="stylesheet" href="style.css" />
  </head>
<body>
  <h1>テキストアニメーション</h1>
  <div class="slide slide1">
 テキストアニメーション1
  </div>
  <div class="slide slide2">
 テキストアニメーション2
  </div>
</body>
</html>

style.css
.slide1 {
    position: relative;
    -webkit-animation-name: "slide";
    animation-name: "slide";

    -webkit-animation-duration: 1s;
    -moz-animation-duration: 1s;
    -o-animation-duration: 1s;
    animation-duration: 1s;
}

.slide2 {
    position: relative;
    -webkit-animation-name: "slide";
    animation-name: "slide";

    -webkit-animation-delay: 200ms;
    animation-delay: 200ms;

    -webkit-animation-duration: 1s;
    -moz-animation-duration: 1s;
    -o-animation-duration: 1s;
    animation-duration: 1s;
}

@-webkit-keyframes "slide" { 
    from {
        left: 200px;
    }
    to { 
        left: -200px;
    }
}

今回のポイントは、@-webkit-keyframesです。
この属性は、要素のアニメーションの動き方を指定します。
fromが、開始位置、toが終了位置となります。

slide1、slide2両方とも、animation-name:"slide"が指定されていて、このanimation-nameで指定した名前のkeyframesでアニメーションされます。

animation-durationはアニメーションが始まって、終わるまでの時間です。1sは1秒を
表しています。これが、500msになると、500ミリ秒になります。
動きを見ていると、animation-durationはまだ使えなくて、ベンダープレフィックスが
必要なようです。

-webkit-animation-delayは、アニメーションを開始するまでの待ち時間です。
時間の指定方法はanimation-durationと同じです。

これをChromeで動かすと、左から200pxから-200pxまで移動するアニメーションが
表示されます。端まで移動した後も止まらずアニメーションします。

おもしろいなと思ったのですが、授業では使えそうにないな…。
「Chromeでしか動かない」と言っても、IEやFirefoxで動かしたりするし…。

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月3日金曜日

Google App Engine Go SDK(Go1ベータ版搭載版)が公開されています

Go1betaが搭載されたGoogle App Engine SDKが公開されています。
http://code.google.com/p/appengine-go/downloads/list

先日公開されたGoogle App Engine SDK 1.6.2はr60.3という
ちょっと古めのバージョンのままでしたが、
もうすぐGo1がリリースされるのと、最新のGo言語と書き方も違うというのも
開発者からすると大変ですので、ここで、どのような違いがあるかを
まとめておきたいと思います。
(※恐らく、正式版でも大きな変更はないと思いますが、あくまでもGo1ベータでの
違いの確認です。また、ベースは私の書籍のプログラムをベースに見ていきます。)

なんとなくですが、近いうちに公式リリースもされそうな気がするので、
先にこのGo1betaで慣れておく方が良いかもしれません。

最新ドキュメントのURL
http://weekly.golang.org/

私の書籍「Go言語プログラミング入門 」でもリリース後は
読み替える必要がありますのでご注意下さい。

  • app.yamlのapi_versionは"3"ではなく、"go1beta"となります。
    • ※これに関しては、正式なリリースになれば変更されるでしょう。
  • httpパッケージ→"net/http"に変更
    • 通信関係のパッケージは大体netパッケージ以下に移動しています。したがって、これまでは、import "http"でしたが、import "net/http"となります。
  • os.Error→"error"に変更
    • Errorは、osパッケージで定義されたインターフェイスでしたが、パッケージ内のインターフェイスではなく、Go言語全体(?)で使えるインターフェイスになりました。また、エラーメッセージを取得するメソッド名がString()からError()に変わっています。
  • time.SecondsToLocalTime()がtime.Unix()に変更
    • これまで、ローカル時間を取得するメソッドはSecondsToLocalTime()でしたが、Unix()に変わっています。Unix()メソッドは、第1引数が秒までを指定し、第2引数でナノ秒(小さい時間)を指定するようになっています。
  • time.Seconds()がtime.Now()に変更
    • 現在日時を取得するメソッドが変更されています。Seconds()はメソッドになりました。秒を取得します。また、time構造体の中身が秒、ナノ秒、Location位置の3つで管理されるようになっています。
  • templateパッケージがtext/templateに変更
    • 使い方は変わっていませんが、templateパッケージが移動しているようです。
  • template.ParseFile()が、template.ParseFiles()に変更(2012/02/03追記)
    • template.ParseFile()でファイルを1つのみ解析していましたが、引数に複数のファイル名を渡す事で、複数のファイルを解析できるようになっています。これまで通り、1つでも実行できますので、メソッド名のみ変更しておけば問題ありません。
  • template.Must(template.New("html").ParseFiles("sample.html"))→template.Must(template.ParseFiles("sample.html"))にする。
    • template.ParseFiles()がtemplate構造体のメソッドと関数の2つに増えています。単純にhtmlを読み込んで実行するの(書籍の例など)であればtemplate.ParseFiles("sample.html")など、New()を呼び出す必要はありません。
  • datastore.Timeが削除されています。go1beta以降では、time.Timeを利用すれば良いようです。
    • フォーマットを指定したい場合は、Format()が用意されていますので、書籍では、fmt.Sprintfを使って書式指定していましたが、go1betaでは、comments_view[pos].Date = comment.Date.Format("2006/01/02 15:04:05")という感じになります。
    • 以前書いたBlog記事(Goでの日付変換)も参考にして下さい。
  • datastore.SecondsToLocalTime()も削除されています。
    • datastore.Timeで管理する必要がなくなりますので、time.Timeで定義し、time.Now()で取得して下さい。


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〜の予定です。
少人数で行っているので制限が厳しいですが、ご参加をお待ちしております。