【入門編】Slim3で始める!GAE/JでWebアプリケーション開発 (第3回)

残機能の開発

記事更新を行うControllerの実装

先ほど作成した<記事編集>で修正された記事を更新するControllerを実装します。


記事更新イメージ

  • Antタスク gen-controller-without-view の実行gen-controller-without-view に /bbs/updateEntry と入力して以下のクラスを生成します。
    • simplebbs.controller.bbs.UpdateEntryController
    • simplebbs.controller.bbs.UpdateEntryControllerTest
  • テストケースの作成UpdateEntryController は既存の記事データへ変更内容を上書き更新しますが、その際の入力値のバリデーションは記事登録の PostEntryController と同じです。PostEntryControllerTest のソースを一部流用しつつ以下のように修正しました。

    UpdateEntryControllerTest.java

    package simplebbs.controller.bbs;
    
    import java.util.Date;
    import java.util.List;
    
    import org.slim3.datastore.Datastore;
    import org.slim3.tester.ControllerTestCase;
    import org.junit.Test;
    
    import simplebbs.model.bbs.Body;
    import simplebbs.model.bbs.Head;
    import simplebbs.service.bbs.BbsService;
    import static org.junit.Assert.*;
    import static org.hamcrest.CoreMatchers.*;
    
    public class UpdateEntryControllerTest extends ControllerTestCase {
    
        private BbsService service = new BbsService();
        private String keyString = null;
        private static final String PASSWORD = "password";
    
        // 初期登録の値
        private static final String BEFORE_SUBJECT = "タイトルです";
        private static final String BEFORE_USERNAME = "投稿者名です";
        private static final String BEFORE_TEXT = "本文です";
    
        // 更新後のバリデーションOKな入力値
        private static final String AFTER_SUBJECT = "更新したタイトルです";
        private static final String AFTER_USERNAME = "更新した投稿者名です";
        private static final String AFTER_TEXT = "更新した本文です";
    
        @Override
        // 各テストの前に実行される処理
        public void setUp() throws Exception {
            super.setUp();
            // テスト前に記事を1件登録しておく
            insertEntry(BEFORE_SUBJECT, BEFORE_USERNAME, new Date(), BEFORE_TEXT, PASSWORD);
            // 登録した1件の記事を取得する
            Head head = service.getAll().get(0);
            // この記事のKeyを文字列に変換して保持
            keyString = Datastore.keyToString(head.getKey());
        }
    
        // 記事を新規登録する
        private void insertEntry(
                String subject,
                String username,
                Date postDate,
                String text,
                String password) throws Exception {
            // 記事の作成
            Head head = new Head();
            head.setSubject(subject);
            head.setUsername(username);
            head.setPostDate(postDate);
            head.setPassword(password);
            // 本文の作成
            Body body = new Body();
            body.setText(text);
            // データストアへ登録
            service.insert(head, body);
        }
    
        // 記事編集ページで修正した記事をPOSTした動作をエミュレート
        private UpdateEntryController updateEntry(String subject, String username, String text) throws Exception {
            tester.param("key", keyString);     // hidden 
            tester.param("password", PASSWORD); // hidden
            tester.param("subject", subject);
            tester.param("username", username);
            tester.param("text", text);
            tester.request.setMethod("POST");
            tester.start("/bbs/updateEntry");
            return tester.getController();
        }
        // バリデーションOKな入力値であること
        private void assertValidParameters(String subject, String username, String text) {
            // 1件の記事が登録されていること
            List<Head> lstHead = service.getAll();
            assertThat(lstHead.size(), is(1));
            // 1件の記事の内容が更新した内容であること
            Head head = lstHead.get(0);
            assertThat(head.getSubject(), is(subject));
            assertThat(head.getUsername(), is(username));
            assertThat(head.getBodyRef().getModel().getText(), is(text));
            // エラーメッセージが1つもセットされていないこと
            assertThat(tester.getErrors().isEmpty(), is(true));
            // 記事詳細ページにリダイレクトしていること
            assertThat(tester.getDestinationPath(), is("/bbs/read?key="+keyString));
            assertThat(tester.isRedirect(), is(true));
        }
        // バリデーションNGな入力値であること
        private void assertInvalidParameters() {
            List<Head> lstHead = service.getAll();
            assertThat(lstHead.size(), is(1));
            // 1件の記事の内容が更新前の内容であること
            Head head = lstHead.get(0);
            assertThat(head.getSubject(), is(BEFORE_SUBJECT));
            assertThat(head.getUsername(), is(BEFORE_USERNAME));
            assertThat(head.getBodyRef().getModel().getText(), is(BEFORE_TEXT));
            // エラーメッセージが何かしらセットされていること
            assertThat(tester.getErrors().isEmpty(), is(false));
            // 記事編集JSPを表示していること
            assertThat(tester.getDestinationPath(), is("/bbs/edit.jsp"));
            assertThat(tester.isRedirect(), is(false));
        }
    
        @Test
        public void testValidParameters() throws Exception {
            // -----<< すべての入力値OKのテスト >>----- //
            updateEntry(AFTER_SUBJECT, AFTER_USERNAME, AFTER_TEXT);
            // ========== assertion start ========== //
            assertValidParameters(AFTER_SUBJECT, AFTER_USERNAME, AFTER_TEXT);
            // ========== assertion end ========== //
        }
    
        @Test
        public void testSubjectEmpty() throws Exception {
            // -----<< タイトル未入力で入力値NGのテスト >>----- //
            updateEntry("", AFTER_USERNAME, AFTER_TEXT);
            // ========== assertion start ========== //
            assertInvalidParameters(); // 入力値NG
            // subject のエラーメッセージがセットされていること
            assertThat(tester.getErrors().get("subject"), is(notNullValue()));
            // ========== assertion end ========== //
        }
    
        ~ 省略 ~
    
        @Test
        public void testAfterDeleted() throws Exception {
            // 該当の記事を削除しておく
            service.delete(Datastore.stringToKey(keyString));
            // -----<< 有効な入力値で更新 >>----- //
            updateEntry(AFTER_SUBJECT, AFTER_USERNAME, AFTER_TEXT);
            // ========== assertion start ========== //
            // 記事が削除されてるので0件であること
            List<Head> lstHead = service.getAll();
            assertThat(lstHead.size(), is(0));
            // Errorsのキー"message"にエラーメッセージがセットされていること
            assertThat(tester.getErrors().get("message"), is(notNullValue()));
            // トップページにforwardしていること
            assertThat(tester.getDestinationPath(), is("/bbs/"));
            assertThat(tester.isRedirect(), is(false));
            // ========== assertion end ========== //
        }
    }
    

    テスト数が多いため途中で省略しています。全テストのソースはコチラです。

    既存データに対する更新なので setUp() メソッドで事前データを作成し、各テストメソッドではそのデータに対し更新を行い検査しています。

    全テストケースを書き終わったら保存して実行して全て失敗することを確認して下さい。


    UpdateEntryControllerTest失敗イメージ

  • 必要な処理の実装UpdateEntryController は以下のように修正しました。

    UpdateEntryController.java

    package simplebbs.controller.bbs;
    
    import org.slim3.controller.Controller;
    import org.slim3.controller.Navigation;
    import org.slim3.controller.validator.Validators;
    import org.slim3.util.ApplicationMessage;
    import org.slim3.util.BeanUtil;
    
    import simplebbs.model.bbs.Body;
    import simplebbs.model.bbs.Head;
    import simplebbs.service.bbs.BbsService;
    
    public class UpdateEntryController extends Controller {
    
        @Override
        public Navigation run() throws Exception {
            if (!isPost()) {
                // POSTではないリクエストはトップページへリダイレクト
                return redirect(basePath);
            }
            // 入力値のバリデーション
            if (!validate()) {
                // バリデーションエラーの場合は記事詳細ページを表示
                return forward("edit.jsp");
            }
            BbsService service = new BbsService();
            Head head = null;
            try {
                // 指定されたkeyから記事を取得
                head = service.get(asKey("key"));
            }
            catch (Exception e) {
                // keyが不正な場合
            }
            // 記事が取得できなかった場合
            if (head == null) {
                // 指定されたキーに該当する記事がない場合はトップへ戻る
                errors.put("message", ApplicationMessage.get("error.entry.notfound"));
                return forward(basePath);
            }
            if (!asString("password").equals(head.getPassword())) {
                // パスワードが不一致の場合は記事詳細へ戻る
                errors.put("password", ApplicationMessage.get("error.password.invalid"));
                return forward("read");
            }
            // リクエストの値をHead、Bodyのプロパティにセットする
            Body body = head.getBodyRef().getModel();
    
            BeanUtil.copy(request, head);
            body.setText(asString("text"));
            // 上書き更新
            service.update(head, body);
            // 記事詳細にリダイレクト(GETパラメータで記事の主キーを指定)
            return redirect(basePath + "read?key="+asString("key"));
        }
    
        private boolean validate() {
            Validators v = new Validators(request);
            v.add("subject", v.required(),v.maxlength(50));
            v.add("username", v.required(),v.maxlength(50));
            v.add("text", v.required());
            return v.validate();
        }
    }
    

    UpdateEntryController の主な処理は以下3点です。

    • 入力値(subject、username、text)のバリデーション
    • 記事情報の上書き更新
    • 記事詳細ページにリダイレクトして終了

    補足ですが、最後の記事詳細ページへのリダイレクトはGETパラメータで戻るべき記事の主キーを渡しています。

    // 記事詳細にリダイレクト(GETパラメータで記事の主キーを指定)
    return redirect(basePath + "read?key="+asString("key"));
    

    リダイレクトの場合はリクエストスコープの値を遷移先( /bbs/read )に引き継げないので、このようにGETパラメータで渡してあげる必要があります。

  • テストケースの実行UpdateEntryControllerの実装が完了したら保存して、テストを実行してみます。
    UpdateEntryControllerTest成功イメージ
    グリーンになれば UpdateEntryController の実装は完了です。ブラウザからも動作確認して意図した画面遷移になっているか確認してみましょう。先にブラウザで開いていた http://localhost:8888/bbs/edit にてタイトルを空にして更新ボタンを押下してみます。
    タイトルブランクで記事を更新イメージ
    タイトルが空なので記事編集ページが表示され、エラーメッセージが表示されています。次に、それぞれのテキストフィールドに有効な新しい値を入力して更新してみましょう。
    有効な値で記事を更新イメージ


    有効な値で記事を更新後イメージ
    記事詳細ページに遷移し、正常に更新されていることが確認できました。 以上で<記事更新>の実装は完了です。

    作成したソースは以下になります。

    • simplebbs.controller.bbs.UpdateEntryController.java
    • simplebbs.controller.bbs.UpdateEntryControllerTest.java

    次は<記事削除>の処理を実装します。