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

残機能の開発

<記事編集>ページのControllerとJSPの実装

<記事編集>ページの画面イメージをおさらいします。


記事編集イメージ

<記事編集>ページは<記事詳細>画面から、パスワードを入力することで記事の削除、修正が行えるフォームを表示します。パスワードが不一致の場合はこのページへは遷移せず、<記事詳細>ページでエラーメッセージを表示します。

  • Antタスク gen-controller の実行gen-controller に /bbs/edit と入力して以下のクラスとJSPファイルを生成します。
    • simplebbs.controller.bbs.EditController
    • simplebbs.controller.bbs.EditControllerTest
    • war/bbs/edit.jsp
  • JSPの実装gen-controller で生成された war/bbs/edit.jsp を以下のように修正しました。

    edit.jsp

    <%@page pageEncoding="UTF-8" isELIgnored="false" session="false"%>
    <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
    <%@taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>
    <%@taglib prefix="f" uri="http://www.slim3.org/functions"%>
    
    <html>
    <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>SimpleBBS</title>
    <link href="/css/bbs.css" rel="stylesheet" type="text/css" />
    </head>
    <body>
      <div id="page">
      <h1>SimpleBBS</h1>
      <hr>
      <div class="err">${errors.message}</div>
      <form method="post" action="deleteEntry" style="display: inline" onsubmit="return confirm('この記事を削除します。よろしいですか?')">
        <input type="hidden" ${f:hidden("key")}/>
        <input type="hidden" ${f:hidden("password")}/>
        <input type="submit" value=" この記事を削除する " class="button"/>
      </form>
      <hr>
      <form method="post" action="updateEntry">
        <input type="hidden" ${f:hidden("key")}/>
        <input type="hidden" ${f:hidden("password")}/>
        <table>
          <thead><tr><td colspan="2">記事の修正</td></tr></thead>
          <tbody>
            <tr><td class="label">タイトル</td><td class="elem"><input type="text" ${f:text("subject")} class="normal ${f:errorClass("subject","err")}"/><span class="err">${errors.subject}</span></td></tr>
            <tr><td>お名前</td><td><input type="text" ${f:text("username")} class="normal ${f:errorClass("username","err")}"/><span class="err">${errors.username}</span></td></tr>
            <tr><td>本文</td><td><textarea name="text" class="largetext ${f:errorClass("text","err")}">${f:h(text)}</textarea><div class="err">${errors.text}</div></td></tr>
            <tr><td colspan=2><input type="submit" value=" 更新する " class="button"/><input type="reset" value=" リセット " class="button" ></td></tr>
          </tbody>
        </table>
      </form>
      <p><a href="javascript:history.back()">戻る</a></p>
      </div>
    </body>
    </html>
    

    記事削除と記事修正の2つのフォームが定義されたシンプルなページです。この中での新しい記述は以下になります。

    • onsubmitイベント処理について記事削除ボタンが押下された際にJavaScriptで確認ダイアログを表示するようにしています。この記述はSlim3やappengineとは直接関係ありませんので詳細は割愛します。
    • f:hidden() についてhidden属性の入力フォームに ${f:hidden(“key”)} という記述があります。これは前回説明した f:text() と全く同じ動作をします。詳細は前回の記事を参照下さい。
    • javascript:history.back() についてこのページでは「戻る」のリンクにJavaScriptの戻る機能を利用しています。遷移元の<記事詳細>ページにURL指定で戻る場合 /bbs/read?key=(記事の主キーの文字列) と指定する必要がありますが history.back() なら簡単です。この記述もSlim3やappengineとは直接関係ありません。

    JSPの修正が終わったら保存して、ブラウザで確認してみましょう。<記事詳細>ページから「編集」ボタンをクリックします。


    記事編集ページイメージ

    上記のようなページが表示されればJSPの修正は完了です。続けて、このフォームに記事情報を表示するためにEditControllerを修正します。

  • テストケースの作成<記事編集>ページは<記事詳細>ページで入力された編集用パスワードの入力が正しかった時のみ表示可能とします。また<記事詳細>のテスト同様、記事が削除されていた場合のことも考慮し3つのテストケースを作成しました。

    EditControllerTest.java

    package simplebbs.controller.bbs;
    
    import java.util.Date;
    
    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 EditControllerTest extends ControllerTestCase {
    
        private BbsService service = new BbsService();
        private String keyString = null;
        // 正しいパスワード
        private static final String PASSWORD = "password";
    
        @Override
        // 各テストの前に実行される処理
        public void setUp() throws Exception {
            super.setUp();
            // 各テストの実行前に記事を1件登録しておく
            insertEntry("テスト用の記事", "テストユーザ", new Date(), "テスト用の記事の本文です。", 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);
        }
    
        @Test
        public void testValidParameter() throws Exception {
            // 記事詳細ページから正しいパスワードが入力されてきた動作をエミュレート
            tester.param("key", keyString);
            tester.param("password", PASSWORD);
            tester.request.setMethod("POST");
            tester.start("/bbs/edit");
            EditController controller = tester.getController();
            // 記事編集ページの表示
            // ========== assertion start ========== //
            assertThat(controller, is(notNullValue()));
            // requestScopeに記事情報がセットされていること
            assertThat(tester.requestScope("key"), is(notNullValue()));
            assertThat(tester.requestScope("password"), is(notNullValue()));
            assertThat(tester.requestScope("subject"), is(notNullValue()));
            assertThat(tester.requestScope("username"), is(notNullValue()));
            assertThat(tester.requestScope("text"), is(notNullValue()));
            // エラーメッセージは空であること
            assertThat(tester.getErrors().isEmpty(), is(true));
            // 記事編集のjspへフォワードしていること
            assertThat(tester.getDestinationPath(), is("/bbs/edit.jsp"));
            assertThat(tester.isRedirect(), is(false));
            // ========== assertion end ========== //
        }
    
        @Test
        public void testInvalidParameter() throws Exception {
            // 記事詳細ページから不正なパスワードが入力されてきた動作をエミュレート
            tester.param("key", keyString);
            tester.param("password", "hogehoge");
            tester.request.setMethod("POST");
            tester.start("/bbs/edit");
            EditController controller = tester.getController();
            // 記事詳細ページへ戻る
            // ========== assertion start ========== //
            assertThat(controller, is(notNullValue()));
            // requestScopeに記事の主キーが指定されていること
            assertThat(tester.requestScope("key"), is(notNullValue()));
            // パスワードのエラーメッセージがセットされていること
            assertThat(tester.getErrors().get("password"), is(notNullValue()));
            // 記事詳細ページへフォワードしていること
            assertThat(tester.getDestinationPath(), is("/bbs/read"));
            assertThat(tester.isRedirect(), is(false));
            // ========== assertion end ========== //
        }
    
        @Test
        public void testAfterDeleted() throws Exception {
            // 該当の記事を予め削除
            service.delete(Datastore.stringToKey(keyString));
            // 記事詳細ページから正しいパスワードが入力されてきた動作をエミュレート
            tester.param("key", keyString);
            tester.param("password", PASSWORD);
            tester.request.setMethod("POST");
            tester.start("/bbs/edit");
            EditController controller = tester.getController();
            // トップページへ戻る
            // ========== assertion start ========== //
            assertThat(controller, is(notNullValue()));
            // Errorsのキー"message"にエラーメッセージがセットされていること
            assertThat(tester.getErrors().get("message"), is(notNullValue()));
            // トップページへフォワードしていること
            assertThat(tester.getDestinationPath(), is("/bbs/"));
            assertThat(tester.isRedirect(), is(false));
            // ========== assertion end ========== //
        }
    
    }
    

    3つのテストメソッドの検査内容はコメントの通りで、特に目新しい記述はありません。テストケースの記述が終わったら保存して実行し、3つとも失敗することを確認して下さい。

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

    EditController.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 EditController extends Controller {
    
        @Override
        public Navigation run() throws Exception {
            if (!isPost() || !validate()) {
                // POSTではないリクエスト、またはバリデーションエラーの場合は記事詳細へ戻る
                return forward("read");
            }
            BbsService service = new BbsService();
            Head head = service.get(asKey("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(head, request);
            requestScope("text", body.getText());
    
            return forward("edit.jsp");
        }
    
        private boolean validate() {
            Validators v = new Validators(request);
            v.add("key", v.required());
            v.add("password", v.required());
            return v.validate();
        }
    
    }
    

    上記のソース以外にパスワードが不一致だった場合のエラーメッセージとして以下の定義を追加しています。

    application_ja.properties

    error.password.invalid=パスワードが違います。

    EditControllerの実装も特に目新しいものはありません。処理についてはコメントを参考下さい。

  • テストケースの実行EditControllerの実装が完了したら保存して、テストを実行してみます。
    EditControllerTest実行イメージ
    グリーンになればEditControllerの実装も完了です。ブラウザからも動作を確認してみましょう。先にブラウザで開いていた http://localhost:8888/bbs/edit をリロードしてみます。
    パスワードなしで編集ボタン押下イメージ
    先ほどはパスワードの入力なしに「修正」ボタンを押下して /bbs/edit ページを表示していましたが、EditControllerを実装したことでパスワードのバリデーションが有効になったことが確認できます。次に適当に不正なパスワードを入力し「編集」ボタンを押下してみます。
    違うパスワードを入力して編集ボタン押下イメージ

    最後に正しいパスワードを入力してみます。


    正常に記事編集に遷移したイメージ

    正常に<記事編集>ページが表示されることを確認できました。また、先ほどは表示されていなかった記事の内容も正しく表示されています。以上で<記事編集>の実装は完了です。

    • simplebbs.controller.bbs.EditController.java
    • simplebbs.controller.bbs.EditControllerTest.java
    • war/bbs/edit.jsp

    ここまでで全画面分のJSPの作成は終わりました。残りは記事更新、記事削除、コメント投稿のControllerの作成になります。

    次は<記事更新>処理を実装します。