2011年8月26日金曜日

『テスト駆動開発入門』読書会 in 秋田

『テスト駆動開発入門』読書会 in 秋田を一人で開催しました。


その成果物をここで上げていきます。

第一章


1.テストのみの状態のMoneyTest.java

1.コンパイルが通るようになったMoneyTest.javaとDollar.java

1.当然落ちるMoneyTest.java

1.強引にテストを通過させるDollar.java

1.重複を取り除いたDollar.java

第二章


2.新たな振る舞いを記述したMoneyTest.java

2.解決策が思いつかないのでVariableを導入したMoneyTest.java

2.とりあえずコンパイルエラーをなくしたDollar.java

2.正しいと思われるコードに修正して動きを確認できたDollar.java

第三章


3.全てにたいする等価性を検証するMoneyTest.java

3.等価性の仮実装Dollar.java

3.不安を三角測量で表すMoney.java

3.一般化の実施Dollar.java

第四章


4.情報が豊富になってきたのでリファクタリングしたMoneyTest.java

4.さらにインライン化したMoneyTest.java

4.Dollar.javaオブジェクトだけがamountフィールドを扱えるようになったのでprivate化する

第五章


5.フランのテストと実装

なにテキストで公開して欲しい?本を買うか、図書館で借りてください。


2011年8月24日水曜日

Gradle で スローテスト問題を解決する。


今、『Building and Testing with Gradle』(O'Reilly)という本を読んでいます。

Gradleのスローテスト問題への対応


さて、この本の一節に次のような記述がありました。

When JUnit tests reach a certain level of proliferation within a project, there is a motivation to run them in parallel to get the results faster. However, there would be a great overhead to running every unit test in its own JVM. Gradle provides an intelligent compromise in that it offers a maxParallelForks that governs the maximum simultaneous JVMs that are spawned.

In the same area of testing, but with a different motivation, is the forkEvery setting. Tests, in their quest to touch everything and exercise as much as possible, can cause unnatural pressure on the JVM's memory allocation. In short, it is waht Java developers term a "leak". It can merely be the loading of every class causing the problem. This isn't really a leak since the problem stems from the fact that loaded class definitions are not garbage collected but instead are loaded into permgen space. The forkEvery setting causes a test-running JVM to close and be replaced by a brand new one after the specified number of tests have run under an instance.


まあ、訳すのが面倒なので、大雑把にまとめると、
  • maxParallelForks … テストの並列実行数
  • forkEvery … JVMの再起動の頻度(OutOfMemoryExceptionを回避するためにJVMを再起動する。)
ということになります。

使い方はこんな感じになります。
build.gradle

apply plugin: 'java'
repositories {
    mavenCentral()
}
dependencies {
    testCompile 'junit:junit:4.8.2'
}
test {
    maxParallelForks = 5
    forkEvery = 30
}


この例ではテストが5個同時に実行されて、30個のテストクラスが実行される度に一度JVMが再起動されるということになります。
これにより並列でテストを行い、かつOutOfMemoryExceptionを回避して、スローテスト問題に対応してくれるということです。


テストの準備

まあ、こういう本は実際動かしてみてなんぼですので、テストをやってみることにしましょう。

まずはテストを強引に作ります。

CreateTest.groovy

import static groovyx.gpars.GParsPool.*;

def packagePath = 'C:/Users/mike/IDEA_Project/GradleSample/src/test/java/orz/mikeneck/gradle/sample/boxunbox/test'

def head = $/
package orz.mikeneck.gradle.sample.boxunbox.test;
import org.junit.Before;
import org.junit.Test;
import java.util.Arrays;
import java.util.List;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
/$

def body = $/
    public static final int SIZE = 400;
    private List<Integer> intList;
    private List<Long> longList;
    @Test
    public void testInteger() {
        int[] array = new int[SIZE];
        int position = 0;
        for(Integer item : intList)
            array[position++] = item;
        for (int i : array)
            assertThat(i, is(intList.get(i)));
    }
    @Test
    public void testLong() {
        long[] array = new long[SIZE];
        int position = 0;
        for (Long item : longList)
            array[position++] = item;
        position = 0;
        for(long item : array)
            assertThat(item, is(longList.get(position++)));
    }
    @Before
    public void setUp() throws Exception {
        Integer[] integers = new Integer[SIZE];
        Long[] longs = new Long[SIZE];
        for(int i = 0; i < SIZE; i++)
            integers[i] = new Integer(i);
        intList = Arrays.asList(integers);
        for (int i = 0; i < SIZE; i++)
            longs[i] = new Long(i + Integer.MAX_VALUE);
        longList = Arrays.asList(longs);
    }
}
/$

def numbers = []
(1..400).each {
    numbers << it
}

withPool {
    numbers.collectParallel { number ->
        def className = "BoxUnboxTest${number}"
        def name = "${className}.java"
        def fileName = "${packagePath}/${name}"
        def define = "public class ${className} {"
        def content = new StringWriter()
        content << head
        content << define
        content << body
        println ' ---- '
        println "now processing : $fileName"
        println ' ---- '
        new File(fileName).write(content.toString(), 'UTF-8')
        assert new File(fileName).exists() == true
    }
}


Groovyで書いていますが、まあヒアドキュメントで書かれているので、どういうテストかすぐにわかると思います。
大量(400 x 2 = 800個)のオブジェクト生成および基本型のintlongのボクシング・アンボクシングというコストのかかるようなテストを400個作ります。

ちなみに単体でテストするとこれくらいの速度です。


ここから単純に計算すると 0.014s x 400 -> 5.6s くらいかかることが想定されます。


テストの実行(並列しない)


まずは並列実行しない場合のテスト
build.gradle

apply plugin: 'java'
repositories {
    mavenCentral()
}
dependencies {
    testCompile 'junit:junit:4.8.2'
}


実行結果

C:\Users\mike\IDEA_Project\GradleSample>gradle test
:buildSrc:compileJava UP-TO-DATE
:buildSrc:compileGroovy UP-TO-DATE
:buildSrc:processResources UP-TO-DATE
:buildSrc:classes UP-TO-DATE
:buildSrc:jar UP-TO-DATE
:buildSrc:assemble UP-TO-DATE
:buildSrc:compileTestJava UP-TO-DATE
:buildSrc:compileTestGroovy UP-TO-DATE
:buildSrc:processTestResources UP-TO-DATE
:buildSrc:testClasses UP-TO-DATE
:buildSrc:test UP-TO-DATE
:buildSrc:check UP-TO-DATE
:buildSrc:build UP-TO-DATE
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:test UP-TO-DATE

BUILD SUCCESSFUL

Total time: 6.945 secs
C:\Users\mike\IDEA_Project\GradleSample>


約6.9秒くらいですかね。

何回か実施しましたが、だいたい同じくらいの時間でした。

テストの実行(並列する)


並列実行( 3並列 : 50回に一回JVMをリロード )する場合。
build.gradle

apply plugin: 'java'
repositories {
    mavenCentral()
}
dependencies {
    testCompile 'junit:junit:4.8.2'
}
test {
    maxParallelForks = 3
    forkEvery = 50
}



実行結果

C:\Users\mike\IDEA_Project\GradleSample>gradle test
:buildSrc:compileJava UP-TO-DATE
:buildSrc:compileGroovy UP-TO-DATE
:buildSrc:processResources UP-TO-DATE
:buildSrc:classes UP-TO-DATE
:buildSrc:jar UP-TO-DATE
:buildSrc:assemble UP-TO-DATE
:buildSrc:compileTestJava UP-TO-DATE
:buildSrc:compileTestGroovy UP-TO-DATE
:buildSrc:processTestResources UP-TO-DATE
:buildSrc:testClasses UP-TO-DATE
:buildSrc:test UP-TO-DATE
:buildSrc:check UP-TO-DATE
:buildSrc:build UP-TO-DATE
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:test UP-TO-DATE

BUILD SUCCESSFUL

Total time: 7.943 secs
C:\Users\mike\IDEA_Project\GradleSample>


あれ、7.943sもかかっている!

何回か挑戦…


:buildSrc:test UP-TO-DATE
:buildSrc:check UP-TO-DATE
:buildSrc:build UP-TO-DATE
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:test UP-TO-DATE

BUILD SUCCESSFUL

Total time: 7.025 secs
C:\Users\mike\IDEA_Project\GradleSample>gradle test
:buildSrc:compileJava UP-TO-DATE
:buildSrc:compileGroovy UP-TO-DATE
:buildSrc:processResources UP-TO-DATE
:buildSrc:classes UP-TO-DATE
:buildSrc:jar UP-TO-DATE
:buildSrc:assemble UP-TO-DATE
:buildSrc:compileTestJava UP-TO-DATE
:buildSrc:compileTestGroovy UP-TO-DATE
:buildSrc:processTestResources UP-TO-DATE
:buildSrc:testClasses UP-TO-DATE
:buildSrc:test UP-TO-DATE
:buildSrc:check UP-TO-DATE
:buildSrc:build UP-TO-DATE
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:test UP-TO-DATE

BUILD SUCCESSFUL

Total time: 6.964 secs
C:\Users\mike\IDEA_Project\GradleSample>


う~ん、大して変わらないですね。





これはひょっとしてもっとテストケースを作らなければならないのかな?

とすると、今の手元にある環境ではちょっと実験できないので、
続きは家に戻ったらやってみます。


実験環境
OS : Windows 7
CPU : Intel Core i7 L640 (クアッドコア)
RAM : 8.00GB


2011年8月21日日曜日

Slim3でValidationのテスト

Slim3でValidationのテスト


元ネタは次のページです。

う~ん、いい感じですね

フォームを作成していい感じの値が入ってきた場合のテストの書き方と、Validationを使った場合のエラーメッセージの取得の実装方法。非常に参考になります。
では、Validationのテストを行っていい感じでない値が入ってきた場合のテストをどう書こうかな?

というわけで、Validationでエラーとした場合のテストを書いてみることにしました。

簡単に設計

こんな感じの設計とテスト設計です。

そうだTODOを作成しよう

というわけで、上のストーリーにしたがって、ToDoリストを作成しましょう。
  • メールアドレスを送ったあとに同じ画面に戻ってくる。
  • Validation Errorとなった場合に、エラーメッセージが作成される。
  • Emailは必須。
  • EmailはEmailとして妥当な文字列。

早速テストとControllerを作成

さっきのフォームの作成を参考に、Controllerとかテストとかを作ります。

名前はMailControllerMailControllerTestにしておきます。

なお、元の画面に戻したいので、MailControllerTestでは、すでにリダイレクト先を変更しておきます。

MailController

package orz.mikeneck.mail;

import org.slim3.controller.Controller;
import org.slim3.controller.Navigation;

public class MailController extends Controller {

    @Override
    public Navigation run() throws Exception {
        return null;
    }
}


MailControllerTest

package orz.mikeneck.mail;

import org.junit.Test;
import static org.junit.Assert.*;
import static org.hamcrest.CoreMatchers.*;
import org.slim3.tester.ControllerTestCase;

public class MailControllerTest extends ControllerTestCase {

    @Test
    public void run() throws Exception {
        tester.start("/mail/mail");
        TweetController controller = tester.getController();
        assertThat(controller, is(notNullValue()));
        assertThat(tester.isRedirect(), is(true));
        assertThat(tester.getDestinationPath(), is("/mail/"));
    }
}


おもむろにテスト実行

で、まあ、これは
「"TweetControllerTest.java"を実行してください。テストが失敗するでしょう。なぜなら TweetController の run メソッドが null を返すからです。コントローラを以下のように変更してみましょう。 」(原文ママ)『フォームの作成』

というわけで、コントローラーをテストが通るように実装


MailController

package orz.mikeneck.mail;

import org.slim3.controller.Controller;
import org.slim3.controller.Navigation;

public class MailController extends Controller {

    @Override
    public Navigation run() throws Exception {
        return redirect(basePath);
    }
}


これでテストは通ります。

TODOをひとつやっつけた


  • メールアドレスを送ったあとに同じ画面に戻ってくる。
  • Validation Errorとなった場合に、エラーメッセージが作成される。
  • Emailは必須。
  • EmailはEmailとして妥当な文字列。

Emailは必須


次はEmailが入力されていることを検証します。
このToDoから想定されるのは、Emailが入っていない時は、
  • Emailが入力されていないとエラーメッセージが表示される。
  • 何かしらのメール用のアトリビュートがある。
  • 何かしらのエラーメッセージ用のアトリビュートがある。

画面とのアトリビュートに関する打ち合わせはデザイナーさんとお話ししてください。

ここでは、こういう感じで決まったことにします。
  • emailはアトリビュート : e_mailに入る
  • email用のエラーメッセージはアトリビュート : err_e_mailに入る

ちなみに、Slim3ではアトリビュート名にフィールド名を使う方法があるらしいですが、すいません、まだ学習が足りなくて知りませぬ。誰か教えて下しあ…orz

気をとりなおして!ハイ、ハイ、ハイ、ハイ、テスト追加


やることがきまったら早速テストを追加しましょう。

MailControllerTest

package orz.mikeneck.mail;

import org.junit.Test;
import static org.junit.Assert.*;
import static org.hamcrest.CoreMatchers.*;
import org.slim3.tester.ControllerTestCase;

public class MailControllerTest extends ControllerTestCase {

    // 省略

    @Test
    public void testEMailShouldBeFilled() throws Exception {
        HttpServletRequest request = tester.request;
        request.setAttribute("e_mail", "");
        tester.start("/mail/mail");
        TweetController controller = tester.getController();
        assertThat(controller, is(notNullValue()));

        assertThat(request.getAttribute("err_e_mail"), is(notNullValue()));

        assertThat(tester.isRedirect(), is(true));
        assertThat(tester.getDestinationPath(), is("/mail/"));
    }
}


テストっつたら実行でしょ


というわけで、テストを実行するとレッドになります。
いいですね。レッド。レッドたんかわいいよ、(^ω^)ペロペロ

テストを通るように実装する


じつはこれ(結構単純なんですけど)単純ではないんです。いくつかやることがあります。
  • アトリビュートe_mailに対して値が設定されていることのValidationを加える。
  • Validationの結果がエラーだったら、アトリビュートerr_e_mailにエラーメッセージを加える。

Validationについては『バリデーション』に詳しく記載されています。

というわけで、Validationを追加する。

MailController

package orz.mikeneck.mail;

import org.slim3.controller.Controller;
import org.slim3.controller.Navigation;

public class MailController extends Controller {

    @Override
    public Navigation run() throws Exception {
        Validators valid = new Validators(request);
        valid.add("e_mail", valid.required());
        return redirect(basePath);
    }
}


グ、たったの二行。すばらしいれす。

  • アトリビュートe_mailに対して値が設定されていることのValidationを加える。
  • Validationの結果がエラーだったら、アトリビュートerr_e_mailにエラーメッセージを加える。

つぎに、エラー判定を加えます。

MailController

package orz.mikeneck.mail;

import org.slim3.controller.Controller;
import org.slim3.controller.Navigation;

public class MailController extends Controller {

    @Override
    public Navigation run() throws Exception {
        Validators valid = new Validators(request);
        valid.add("e_mail", valid.required());
        if(valid.validate()){
            // TODO When valid
        } else {
            Errors errors = valid.getErrors();
            request.setAttribute("err_e_mail", errors.get(key));
        }
        return redirect(basePath);
    }
}


若干行数が増えました。しかもifとか付いているし…
まあ、気にせず(←)これでテストしてみましょう。

テストする


はい、通りますね。
というわけで、ToDoをまたひとつやっつけました。

…え、OKの場合?!、後でね(ToDoに追加する)。

  • メールアドレスを送ったあとに同じ画面に戻ってくる。
  • Validation Errorとなった場合に、エラーメッセージが作成される。
  • Emailは必須。
  • EmailはEmailとして妥当な文字列。
  • Emailが妥当な場合はエラーメッセージが作成されない。

あっ!

よく考えたら「Validation Errorとなった場合に、エラーメッセージ…」も実装されちゃいましたね。

  • メールアドレスを送ったあとに同じ画面に戻ってくる。
  • Validation Errorとなった場合に、エラーメッセージが作成される。
  • Emailは必須。
  • EmailはEmailとして妥当な文字列。
  • Emailが妥当な場合はエラーメッセージが作成されない。

EmailはEmailとして妥当な文字列。


これもテスト書きましょう。
とりあえず、ありえないEmailアドレスを設定して、エラーメッセージがあることを確認しましょう。

MailControllerTest

package orz.mikeneck.mail;

import org.junit.Test;
import static org.junit.Assert.*;
import static org.hamcrest.CoreMatchers.*;
import org.slim3.tester.ControllerTestCase;

public class MailControllerTest extends ControllerTestCase {

    // 省略

    @Test
    public void testEMailAddressShouldBeValid() throws Exception {
        HttpServletRequest request = tester.request;
        request.setAttribute("e_mail", "hoge");
        tester.start("/mail/mail");
        TweetController controller = tester.getController();
        assertThat(controller, is(notNullValue()));

        assertThat(request.getAttribute("err_e_mail"), is(notNullValue()));

        assertThat(tester.isRedirect(), is(true));
        assertThat(tester.getDestinationPath(), is("/mail/"));
    }
}


もちろんテストは落ちます。

Emailの正規表現


Emailとして妥当というのは、う~ん、難しいですね。
こういう場合はネットで正規表現パターンを探してきましょう。

^([a-zA-Z0-9])+([a-zA-Z0-9\._-])*@([a-zA-Z0-9_-])+([a-zA-Z0-9\._-]+)+$

こんな感じらしいです。

というわけで、Validationに突っ込みましょう。


MailController

package orz.mikeneck.mail;

import org.slim3.controller.Controller;
import org.slim3.controller.Navigation;

public class MailController extends Controller {

    @Override
    public Navigation run() throws Exception {
        Validators valid = new Validators(request);
        valid.add("e_mail", valid.required(),
        validation.regexp("^([a-zA-Z])+([a-zA-Z0-9\\._-])*@([a-zA-Z])+([0-9a-zA-Z\\._-])+[a-z]+$"));
        if(valid.validate()){
            // TODO When valid
        } else {
            Errors errors = valid.getErrors();
            request.setAttribute("err_e_mail", errors.get(key));
        }
        return redirect(basePath);
    }
}


テストをすると通ります。
これでエラーの場合は完成ですね。

後はOKパターン


  • メールアドレスを送ったあとに同じ画面に戻ってくる。
  • Validation Errorとなった場合に、エラーメッセージが作成される。
  • Emailは必須。
  • EmailはEmailとして妥当な文字列。
  • Emailが妥当な場合はエラーメッセージが作成されない。

本当はEmailが妥当な場合はデータを保存してとかになるのですが、それはServiceでテストするということにしておいてですね、ここではあくまでエラーメッセージについてやります。
(Serviceのテストはまだ勉強中…)

MailControllerTest

package orz.mikeneck.mail;

import org.junit.Test;
import static org.junit.Assert.*;
import static org.hamcrest.CoreMatchers.*;
import org.slim3.tester.ControllerTestCase;

public class MailControllerTest extends ControllerTestCase {

    // 省略

    @Test
    public void testEMailValid() throws Exception {
        HttpServletRequest request = tester.request;
        request.setAttribute("e_mail", "hoge@hoge.com");
        tester.start("/mail/mail");
        TweetController controller = tester.getController();
        assertThat(controller, is(notNullValue()));

        assertThat(request.getAttribute("err_e_mail"), is(nullValue()));

        assertThat(tester.isRedirect(), is(true));
        assertThat(tester.getDestinationPath(), is("/mail/"));
    }
}


で、これは何もしなくても通ります…

というわけで、完全Done!


  • メールアドレスを送ったあとに同じ画面に戻ってくる。
  • Validation Errorとなった場合に、エラーメッセージが作成される。
  • Emailは必須。
  • EmailはEmailとして妥当な文字列。
  • Emailが妥当な場合はエラーメッセージが作成されない。

所感


テストってやっぱり難しいと思いました。
特にまだまだ学習途中のフレームワーク・プラットフォームについてはかなりきついですね。
フレームワーク・プラットフォームで何がどうなる、そして何を利用することができるといったあたりのノウハウがないとテストを書くのはなかなか難しいと思います。
というわけで、学習あるのみと思いました。


2011年8月20日土曜日

Interfaceの定数でMapを使う

これは知らなかった。

InterfaceStringや、intの定数を持たせることができるのは知っていましたが、java.util.Map<K, V>も持たせられるのですね。
これは知りませんでした。

元ネタはここ。
How to Initialise a static Map in Java - stackoverflow


public interface AlarmDate {

    public static final String KEY_YEAR = "YEAR";

    public static final String KEY_MONTH = "MONTH";

    @SuppressWarnings("serial")
    public static final Map<String, Utility> KEY_ENUM =
        Collections.unmodifiableMap(new HashMap<String, Utility>(){{
            put(KEY_YEAR, Utility.ATTRIBUTE_YEAR);
            put(KEY_MONTH, Utility.ATTRIBUTE_MONTH);
    }});

    enum Utility {
        ATTRIBUTE_YEAR {
            @Override
            public String attribute() {
                return KEY_YEAR;
            }
        }, ATTRIBUTE_MONTH {
            @Override
            public String attribute() {
                return KEY_MONTH;
            }
        };

        abstract public String attribute();
    }
}


enumからStringは簡単にアクセスできたのですが、
Stringからenumにどうやってアクセスできるか悩んでいたんですよ。
switch文を書くのも嫌だし(というか、switch文を書かないために戦略的enumパターンを使っている)、どうしようか悩んでいたので、すっきりしました。


2011年8月19日金曜日

Interfaceに内部enumをつけて、そのenumが他のinterfaceを実装していて、自己満足なコードができている。

Interfaceに内部enumをつけて、そのenumが他のinterfaceを実装していて、自己満足なコードができている。


と、ツイートしたので、その自己満足なコードを晒してみる。


package orz.mikeneck.gae.slim3.model.alarm;

import java.util.Date;
import java.util.TimeZone;

import org.slim3.controller.validator.Validator;
import org.slim3.controller.validator.Validators;

import orz.mikeneck.gae.slim3.util.AttributeKeyValue;
import orz.mikeneck.gae.slim3.util.AttributeValidation;

public interface AlarmDate {

    public static final TimeZone TIME_ZONE = TimeZone.getTimeZone("Asia/Tokyo");

    public static final String KEY_YEAR = "YEAR";

    public static final String KEY_MONTH = "MONTH";

    public static final String KEY_DATE = "DATE";

    public static final String KEY_HOUR = "HOUR_OF_DAY";

    public static final String KEY_MINUTE = "MINUTE";

    public static final long MONTH_MAX = 12;

    public static final long MONTH_MIN = 1;

    public static final long DATE_MAX = 31;

    public static final long DATE_MIN = 1;

    public static final long HOUR_MAX = 23;

    public static final long HOUR_MIN = 0;

    public static final long MINUTE_MIN = 0;

    public static final long MINUTE_MAX = 55;

    public int year();

    public int month();

    public int date();

    public int hour();

    public int minute();

    public Date getCreatedAt();

    enum AttributeMapper implements AttributeKeyValue<AlarmDate>, AttributeValidation {
        ATTRIBUTE_YEAR {
            @Override
            public String key() {
                return KEY_YEAR;
            }

            @Override
            public <T> T value(AlarmDate model, Class<T> klass) {
                return klass.cast(model.year());
            }

            @Override
            public Class<?> type() {
                return Integer.class;
            }

            @Override
            public Validator[] validation(Validators valid) {
                return new Validator[] {
                    valid.required(),
                    valid.integerType()};
            }
        }, ATTRIBUTE_MONTH {
            @Override
            public String key() {
                return KEY_MONTH;
            }

            @Override
            public <T> T value(AlarmDate model, Class<T> klass) {
                return klass.cast(model.month());
            }

            @Override
            public Class<?> type() {
                return Integer.class;
            }

            @Override
            public Validator[] validation(Validators valid) {
                return new Validator[] {
                    valid.required(),
                    valid.integerType(),
                    valid.longRange(MONTH_MIN, MONTH_MAX)};
            }
        }, ATTRIBUTE_DATE {
            @Override
            public String key() {
                return KEY_DATE;
            }

            @Override
            public <T> T value(AlarmDate model, Class<T> klass) {
                return klass.cast(model.date());
            }

            @Override
            public Class<?> type() {
                return Integer.class;
            }

            @Override
            public Validator[] validation(Validators valid) {
                return new Validator[] {
                    valid.required(),
                    valid.integerType(),
                    valid.longRange(DATE_MIN, DATE_MAX)};
            }
        }, ATTRIBUTE_HOUR_OF_DAY {
            @Override
            public String key() {
                return KEY_HOUR;
            }

            @Override
            public <T> T value(AlarmDate model, Class<T> klass) {
                return klass.cast(model.hour());
            }

            @Override
            public Class<?> type() {
                return Integer.class;
            }

            @Override
            public Validator[] validation(Validators valid) {
                return new Validator[] {
                    valid.required(),
                    valid.integerType(),
                    valid.longRange(HOUR_MIN, HOUR_MAX)
                };
            }
        }, ATTRIBUTE_MINUTE {
            @Override
            public String key() {
                return KEY_MINUTE;
            }

            @Override
            public <T> T value(AlarmDate model, Class<T> klass) {
                return klass.cast(model.minute());
            }

            @Override
            public Class<?> type() {
                return Integer.class;
            }

            @Override
            public Validator[] validation(Validators valid) {
                return new Validator[] {
                    valid.required(),
                    valid.integerType(),
                    valid.longRange(MINUTE_MIN, MINUTE_MAX)
                };
            }
        };

        @Override
        abstract public String key();

        @Override
        abstract public <T> T value(AlarmDate model, Class<T> klass);

        @Override
        abstract public Class<?> type();

        @Override
        abstract public Validator[] validation(Validators valid);

        @Override
        public void validate(Validators valid) {
            valid.add(key(), validation(valid));
        }
    }
}


こやつは、Slim3のModelクラスのインターフェースなのですが、何がいいかというと、requestのマッピングとか、Validationのコードなどが劇的にキレイになる。

Mapping

package orz.mikeneck.gae.slim3.controller.alarm;

import org.slim3.controller.Controller;
import org.slim3.controller.Navigation;

import orz.mikeneck.gae.slim3.model.alarm.AlarmDate;
import orz.mikeneck.gae.slim3.model.alarm.AlarmDateModel;

public class IndexController extends Controller {

    @Override
    public Navigation run() throws Exception {
        AlarmDate model = new AlarmDateModel();
        for(AlarmDate.AttributeMapper mapper : AlarmDate.AttributeMapper.values())
            request.setAttribute(mapper.key(), mapper.value(model, mapper.type()));
        return forward("index.jsp");
    }
}


Validators

package orz.mikeneck.gae.slim3.controller.alarm;

import org.slim3.controller.Controller;
import org.slim3.controller.Navigation;
import org.slim3.controller.validator.Validators;

import orz.mikeneck.gae.slim3.model.alarm.AlarmDate;

public class WakeController extends Controller {

    @Override
    public Navigation run() throws Exception {
        Validators valid = new Validators(request);
        for(AlarmDate.AttributeMapper mapper : AlarmDate.AttributeMapper.values())
            mapper.validate(valid);
        return redirect(basePath);
    }
}


Validationとかの部分が二行で書けるので、コントローラーにモデルに関する複雑なことを書かんでよくなる。
まあ、もっといいやりかたがあるんだろうけど、コレが今のオレの程度…orz


2011年8月15日月曜日

OAuth2.0について調べてまとめてみた。

OAuth2.0について調べてまとめてみた。

Google Data Apiを触ってみようと思ったけど、3-legged OAuthよりもOAuth2.0が推奨されていて、それが何のことかよくわからなかったので調べてみることにした。
(Google Data ApiのJavaライブラリーは3-legged OAuthを使っているみたいだけど…)

まあ、詳しくは調べていないので、雑です。

OAuthのフロー(1)
UserはClientを起動する。

OAuthのフロー(2)
ClientはProviderに対して、ProviderにあるUserのリソースにアクセスする許可をUserからもらうように依頼する。

OAuthのフロー(3)
ProviderはClientがUserのリソースにアクセスすることを許可するかどうかをWebページにて確認する。

OAuthのフロー(4)
UserはClientがProviderにあるUserのリソースにClientがアクセスすることを承認する。

OAuthのフロー(5)
Providerは一時的なAccess Tokenを発行する。

OAuthのフロー(6)
Userは一時的なAccess TokenをClientに入力する。

OAuthのフロー(7)
ClientはProviderに有効なAccess Tokenを発行するように依頼する。

OAuthのフロー(8)
Providerはリソースアクセス用のAccess TokenとAccess TokenをリフレッシュするためのRefresh TokenをClientに渡す。

OAuthのフロー(9)
期限時間内はもらったAccess Tokenで持って、Providerと通信をして、Userリソースにアクセスする。
また、Userに対してClientの機能を提供する。

OAuthのフロー(10)
期限時間が過ぎたら、Refresh Tokenを使って、Providerに対して、新しいAccess Tokenを発行するように依頼する。

資料はこんな感じです。


最後の(9)と(10)はまだ検証していないので、ツッコミお待ちしております。