Todo アプリケーションの概要

ログイン画面、一覧画面、詳細画面、追加・編集画面、確認画面を持つシンプルなアプリケーションです。 このチュートリアルは Cubby Showcase の「Todoサンプルアプリケーション」に、ほぼ同じ内容を収録しています。

アプリケーションの雛形作成

雛形作成の詳細はMaven2 によるプロジェクトの雛形作成をご覧ください。
Maven2 のインストール後、以下のようにプロジェクトの雛形を作成します。 Eclipseのワークスペースディレクトリにターミナル(コマンドプロンプト)で移動して、以下のコマンドを入力してください。

mvn archetype:generate -DarchetypeCatalog=http://cubby.seasar.org 

コマンド入力後、いくつかの質問に回答していきます。

Choose archetype:
1: remote -> cubby-s2-archetype (Cubby 2.0.x S2Container Integration)
2: remote -> cubby-guice-archetype (Cubby 2.0.x Guice Integration)
3: remote -> cubby-spring-archetype (Cubby 2.0.x Spring Integration)
4: remote -> cubby-archetype (Cubby 1.1.x)
Choose a number:  (1/2/3/4): 

1を入力して、cubby-s2-archetype (Cubby 2.0.x S2Container Integration) を選んでください。

以下、groupId,artifactId,version,packageを入力します。

Define value for groupId: : org.seasar.cubby.todo
Define value for artifactId: : cubby-todo
Define value for version: : 1.0-SNAPSHOT
Define value for package: : org.seasar.cubby.todo

Confirm properties configuration:
groupId: org.seasar.cubby.todo
artifactId: cubby-todo
version: 1.0-SNAPSHOT
package: org.seasar.cubby.todo
 Y: :

いままで入力した内容の確認を求められます。問題がなければ Y を入力してください。 プロジェクトの雛形として「cubby-todo」ディレクトリが作成されます。

Eclipseへの取り込み

Eclipse を使った開発に従って、「cubby-todo」プロジェクトのEclipseへの取り込みとWTPの設定を行います。 設定が終わったらWTPからTomcatを起動します。
ブラウザで http://localhost:8080/cubby-todo/ にアクセスして、トップページが表示されることを確認してください。

ログイン画面の作成

ログイン画面のActionクラス(LoginAction.java)とユーザクラス(User.java)、ログイン用のJSP(login.jsp)を作成します。 まずはバリデーションなしで作成します。 アクションにログイン画面を表示する「indexメソッド」とログイン処理を行う「process」メソッドを定義します。 「processメソッド」ではログインに成功した場合一覧画面の「/todo/」にリダイレクトします。 リダイレクトは「new Redirct("/todo/")」となります。

ログインに失敗した場合、エラーメッセージを追加してログイン画面にフォワードします。 アクションで発生したエラーやバリデーションエラーはアクションの「errors」フィールドに追加します。 ここではアクションで発生したエラーを「errors.add("ユーザIDかパスワードが違います。");」 で追加しています。

エラーメッセージをHTMLに出力する部品はCubbyでは用意されていません。 これはエラー画面はアプリケーションにより、表示形式が異なると思われるためです。 サンプルでは「common/errors.jsp」をエラーメッセージの表示用部品としてインポートして各画面で使用しています。 こちらを参考に各アプリケーションでのエラー表示部品を作成してください。

3つのファイルを作成したら、「http://localhost:8080/cubby-todo/todo/login/」で確認してください。 (TomcatではloginというURLは特別に扱われるので、URLの最後に必ずスラッシュを付けて確認してください)

ログイン画面

ログイン画面

LoginAction.java

package org.seasar.cubby.todo.action;

import java.util.Map;

import org.seasar.cubby.action.Action;
import org.seasar.cubby.action.ActionResult;
import org.seasar.cubby.action.Forward;
import org.seasar.cubby.action.Path;
import org.seasar.cubby.action.Redirect;
import org.seasar.cubby.action.RequestParameter;
import org.seasar.cubby.todo.entity.User;

@Path("todo/login")
public class LoginAction extends Action {

  // フィールドにはアクセサメソッドが必要。
  // サンプルではPropertyInterTypeを使用しているのでアクセサメソッドは自動生成されます。

  public Map<String, Object> sessionScope;
  public @RequestParameter String userId;
  public @RequestParameter String password;

  // ----------------------------------------------[Action Method]

  /**
   * ログイン画面表示処理(/todo/login/)
   */
  public ActionResult index() {
    return new Forward("/todo/login.jsp");
  }

  /**
   * ログイン処理(/todo/login/proess)
   */
  public ActionResult process() {
    User user = login(userId, password);
    if (user != null) {
      sessionScope.put("user", user);
      return new Redirect("/todo/");
    } else {
      errors.add("ユーザIDかパスワードが違います。");
      return new Forward("/todo/login.jsp");
    }
  }

  /**
   * 認証処理
   */
  private User login(String userId, String password) {
    User user;
    if ("test".equals(userId) && "test".equals(password)) {
      user = new User();
      user.setId(1);
      user.setName("Cubby");
    } else {
      user = null;
    }
    return user;
  }

}

LoginAction.java

package org.seasar.cubby.todo.entity;

import java.io.Serializable;

public class User implements Serializable {
        private Integer id;
        private String name;

        public Integer getId() {
                return id;
        }
        public void setId(Integer id) {
                this.id = id;
        }
        public String getName() {
                return name;
        }
        public void setName(String name) {
                this.name = name;
        }
}

src/main/webapp/todo/login.jsp

...
<body>
[<a href="${contextPath}/">戻る</a>]
<h1>Todoログイン</h1>
<c:import url="/common/errors.jsp"/>
<t:form action="${contextPath}/todo/login/process" method="post" value="${action}">
<table border="1">
  <tr>
    <th>ユーザID</th>
    <td><t:input type="text" name="userId" maxlength="20" /></td>
  </tr>
  <tr>
    <th>パスワード</th>
    <td><t:input type="password" name="password" maxlength="20" /></td>
  </tr>
  <tr>
    <th></th>
    <td><input type="submit" value="ログイン"/></td>
  </tr>
</table>
test/testでログインできます。
</t:form>
</body>
...

src/main/webapp/common/errors.jsp

<c:if test="${fn:length(allErrors) > 0}">
  <div id="errors" class="errors">
    <ul>
    <c:forEach var="error" varStatus="s" items="${allErrors}">
      <li>${fn:replace(error, "
", "<br/>")}</li>
    </c:forEach>
    </ul>
  </div>
</c:if>

バリデーションの追加

ログイン画面にバリデーションを追加します。 userId、passwordとも必須にしてみましょう。 バリデーションを定義する場合、以下4カ所を追記します。

  • バリデーションルールをアクションのフィールドに定義します。
  • バリデーションをかけたいアクションメソッドに@Validationアノテーションを追加します。
  • JSP中でバリデーションエラーを表示する記述を追記します。
  • messages.propertiesにフィールド名を追記します。

    バリデーションの詳細はバリデーションのリファレンスを参照下さい。

    バリデーションエラーの表示

    バリデーションエラーの表示

    LoginAction.java

    ...
    import org.seasar.cubby.action.Validation;
    import org.seasar.cubby.validator.DefaultValidationRules;
    import org.seasar.cubby.validator.ValidationRules;
    import org.seasar.cubby.validator.validators.RequiredValidator;
    ...
    
    @Path("todo/login")
    public class LoginAction extends Action {
    
      // ----------------------------------------------[Validation]
    
      public ValidationRules loginValidation = new DefaultValidationRules("login.") {
            @Override
            public void initialize() {
              add("userId", new RequiredValidator());
              add("password", new RequiredValidator());
            }
      };
      ...
      @Validation(rules="loginValidation", errorPage = "/todo/login.jsp")
      public ActionResult process() {
        ...
      }
      ...
    }

    src/main/resources/messages.properties

    login.userId=ユーザ名
    login.password=パスワード

HttpServletRequestやHttpServletResponseの利用

アクションクラスでHttpServletRequestやHttpServletResponseを使用する場合、 プロパティを用意することで、Seasar2が自動的にインジェクションしてくれます。
ログイン画面ではセッションを取得するためにHttpServletRequestを使用しています。

フォワードとリダイレクトの使い分け

フォワードは表示処理のみで使用します。 何らかの処理(ここではログイン処理)を行った後に別の画面に遷移する場合、次画面の表示用URLにリダイレクトするようにします。 リダイレクト後のURLは次画面の表示用URLになっているので、ブラウザのリロードボタンを押しても 処理が2回実行されることはありません。

リダイレクトを使用した場合、処理用のリクエストと表示用のリクエストが別になってしまうため 情報(例えば「TODO1を更新しました」というメッセージ)などの情報を引き継げない問題があります。 これは「フラッシュメッセージ」という揮発性のメッセージをセッションに保持することで解消します。 フラッシュメッセージについては後述します。

一覧画面の作成

一覧画面では検索条件に一致するTODOを表示しています。 なお、サンプルではインジェクションされたDaoを直接呼び出していますが、 実際の大きなアプリケーションではアクションはサービス(あるいはLogic)を使用して、Daoを直接呼び出すのは 避けたほうが良いでしょう。

一覧画面

一覧画面

TodoListAction.java

@Path("todo")
public class TodoListAction extends Action {

  // ----------------------------------------------[Validation]

  public ValidationRules validation = new DefaultValidationRules() {
    @Override
    public void initialize() {
      add("limitDate", new DateFormatValidator());
      }
  };

  // ----------------------------------------------[DI Filed]

  public FormatPattern formatPattern;
  public TodoDao todoDao;
  public TodoTypeDao todoTypeDao;

  // ----------------------------------------------[Attribute]

  public TodoConditionDto todoConditionDto;
  public List<Todo> todoList;

  // ----------------------------------------------[Action Method]

  /**
   * 一覧表示処理(/todo/)
   */
  @Form("todoConditionDto")
  @Validation(rules="validation", errorPage="list.jsp")
  public ActionResult index() {
    this.todoList = todoDao.selectByCondition(todoConditionDto);
    return new Forward("list.jsp");
  }

  // ----------------------------------------------[Helper Method]

  /**
   * 種別一覧の取得
   */
  public List<TodoType> getTodoTypes() {
    List<TodoType> todoTypes = todoTypeDao.seletAll();
    return todoTypes;
  }

  /**
   * 検索条件の文字列取得
   */
  public String getQueryString() {
    StringBuilder sb = new StringBuilder();
    if (todoConditionDto.hasKeyword()) {
      sb.append("キーワード:").append(todoConditionDto.getKeyword()).append(" ");
    }
    if (todoConditionDto.hasTypeId()) {
      sb.append("種別:").append(
      todoTypeDao.selectById(todoConditionDto.getTypeId()).getName()).append(" ");
    }
    if (todoConditionDto.hasLimitDate()) {
      DateFormat dateFormat = formatPattern.getDateFormat();
      sb.append("期限日<=").append(
      dateFormat.format(todoConditionDto.getLimitDate()));
    }
    return sb.toString();
  }
  
}

list.jsp

...
<c:import url="/common/notice.jsp"/>
<h2>Todoの一覧</h2>
<c:import url="/common/errors.jsp"/>
<div class="menu">
[<a href="${contextPath}/todo/create">新規作成</a>]
</div>
<t:form action="${contextPath}/todo/" method="post" value="${todoConditionDto}">
<table>
  <tr>
        <th>キーワード</th>
    <td>
        <t:input type="text" name="keyword" size="10" maxsize="10"/>
    </td>
  </tr>
  <tr>
    <th>種別</th>
    <td>
        <t:select id="typeId" name="typeId"
                items="${action.todoTypes}" labelProperty="name" valueProperty="id"/>
  </tr>
  <tr>
    <th>期限日</th>
    <td>
      <t:input type="text" id="limitDate" name="limitDate" size="10" maxsize="10"/>
    </td>
  </tr>
  <tr>
    <th></th>
    <td><input type="submit" value="検索"/></td>
  </tr>
</table>
検索条件:${f:out(action.queryString)}
</t:form>
<table>
  <tr>
    <th>内容</th>
    <th>種別</th>
    <th>期限日</th>
    <th>アクション</th>
  </tr>
        <c:forEach var="todo" items="${action.todoList}" varStatus="status">
                <tr class="${f:odd(status.index, 'odd,even')}">
                        <td><a href="${contextPath}/todo/${f:out(todo.id)}">${f:out(todo.text)}</a></td>
                        <td>${f:out(todo.todoType.name)}</td>
                        <td>${f:dateFormat(todo.limitDate, 'yyyy-MM-dd')}</td>
                        <td>[<a href="javascript:doDelete('${f:out(todo.text)}',${f:out(todo.id)});">削除</a>]</td>
            </tr>
        </c:forEach>
</table>
</div><!-- End of id="content" -->
<c:import url="fotter.jsp" />
</body>
</html>

コンボ・チェックボックス・ラジオボタンの一覧データ取得

検索条件の「種別」コンボボックスに表示するデータはTodoListActionの「getTodoTypes」メソッドから取得しています。 labelPropertyに表示用のラベルのプロパティ名、valuePropertyにvalue値のプロパティ名を指定します。 このようにコンボ・チェックボックス・ラジオボタンの一覧データ取得にはアクションのメソッドを呼び出して使用することもできます。

ただしこの場合画面中で2回「getTodoTypes」を呼び出す箇所があると処理が2回実行されてしまいます。 2回以上使用されるデータに関しては、アクションメソッドの中で取得してフィールドに保持するのが良いでしょう。

list.jsp

        <t:select id="typeId" name="typeId"
                items="${action.todoTypes}" labelProperty="name" valueProperty="id"/>

行毎の色分け

一覧表示では「$f:odd(status.index, 'odd,even')」のようにodd関数をつかって1行ごとにCSSのクラスを切り替えています。

list.jsp

        <c:forEach var="todo" items="${action.todoList}" varStatus="status">
                <tr class="${f:odd(status.index, 'odd,even')}">
                  ...
            </tr>
        </c:forEach>

詳細画面の作成

次に詳細画面を作成します。

詳細画面

詳細画面

TodoAction.java

public class TodoAction extends Action {
  ...
  // ----------------------------------------------[DI Filed]

  public TodoDao todoDao;
  public TodoDxo todoDxo;
  public TodoTypeDao todoTypeDao;

  // ----------------------------------------------[Attribute]

  public @RequestParameter Integer id;
  public @RequestParameter String text;
  public @RequestParameter String memo;
  public @RequestParameter Integer typeId;
  public @RequestParameter String limitDate;
  public TodoType todoType;

  // ----------------------------------------------[Action Method]

  /**
   * 詳細画面表示(/todo/{id})
   */
  @Path("{id,[0-9]+}")
  public ActionResult show() {
    Todo todo = todoDao.selectById(this.id);
    todoDxo.convert(todo, this);
    return new Forward("show.jsp");
  }
  ...
}

show.jsp

...
<h2>Todo詳細</h2>
<div class="menu">
[<a href="${contextPath}/todo/">一覧に戻る</a>]
[<a href="${contextPath}/todo/edit?id=${id}">編集</a>]
[<a href="javascript:doDelete('${f:out(text)}',${f:out(id)})">削除</a>]
</div>
<table border="1">
  <tr>
    <th>内容</th>
    <td>${f:out(text)}</td>
  </tr>
  <tr>
    <th>種別</th>
    <td>${f:out(todoType.name)}</td>
  </tr>
  <tr>
    <th>期限日</th>
    <td>${f:out(limitDate)}</td>
  </tr>
  <tr>
  <th>メモ</th>
  <td>${f:out(memo)}</td>
  </tr>
</table>
</div><!-- End of id="content" -->
<c:import url="fotter.jsp" />
</body>
...

@Pathを使用したURLマッピングのカスタマイズ

@Pathアノテーションを使用することでURLのカスタマイズができます。 ここでは「@Path("id,[0-9]+")」という定義で、「todo/1001」のようなURLが呼び出された場合、 URLのリクエストパラメータに「id=1001」を追加してアクションのidフィールドにセットします。 @Pathを使用したURLマッピングのカスタマイズは@PathのAPIドキュメントを参照ください。

追加・編集画面の作成

追加・編集画面は同じJSPを使用して作成します。

追加・編集画面

追加・編集画面

TodoAction.java

  ...
  /**
   * 追加画面表示(todo/create)
   */
  public ActionResult create() {
    return new Forward("edit.jsp");
  }

  /**
   * 編集画面表示(todo/edit)
   */
  public ActionResult edit() {
    Todo todo = todoDao.selectById(this.id);
    todoDxo.convert(todo, this);
    return new Forward("edit.jsp");
  }
  ...

edit.jsp

...
<h2>Todo編集</h2>
<c:import url="/common/errors.jsp"/>
[<a href="${contextPath}/todo/">一覧に戻る</a>]
<t:form action="confirm" method="post" value="${action}">
<t:input type="hidden" name="id" />
<table border="1">
  <tr>
    <th>タイトル</th>
    <td>
        <t:input type="text" maxlength="50" name="text"  /></td>
  </tr>
  <tr>
    <th>種別</th>
    <td>
                <t:select id="typeId" name="typeId"
                        items="${todoTypes}" labelProperty="name" valueProperty="id" emptyOptionLabel="選択してください。" />
    </td>
  </tr>
  <tr>
    <th>期限日</th>
    <td>
        <t:input type="text" maxlength="12" name="limitDate"/>(YYYY-MM-DD)</td>
  </tr>
  <tr>
    <th>メモ</th>
  <td>
    <t:textarea name="memo" value="${f:out(memo)}"/>
  </td>
  </tr>
  <tr>
    <th></th>
    <td><input type="submit" value="次へ"/></td>
  </tr>
</table>
</t:form>
<div id="content">
</div><!-- End of id="content" -->
<c:import url="fotter.jsp" />
</body>
...

確認画面の作成

確認画面は表示処理で追加・編集画面からのパラメータにバリデーションをかけています。 確認画面は入力された値の表示のみなので、入力データはhiddenパラメータとして保持します。 確認画面からポストされたデータのポスト先はアクションのsaveメソッドです。 saveメソッドでは入力データをDBに保存後、一覧画面にリダイレクトしています。

確認画面

確認画面

TodoAction.java

  ...
  public ValidationRules validation = new DefaultValidationRules() {
    @Override
    public void initialize() {
      add("text", new RequiredValidator(), new MaxLengthValidator(10));
      add("memo", new RequiredValidator(), new MaxLengthValidator(100));
      add("typeId", "type", new RequiredValidator());
      add("limitDate", new DateFormatValidator());
        }
  };
  
  /**
   * 確認画面表示
   */
  @Validation(rules = "validation", errorPage = "edit.jsp")
  public ActionResult confirm() {
    TodoType todoType = todoTypeDao.selectById(this.typeId);
    this.todoType = todoType;
    return new Forward("confirm.jsp");
  }

  /**
   * 確認画面から編集画面への表示
   */
  public ActionResult confirm_back() {
    return new Forward("edit.jsp");
  }

  /**
   * 保存処理
   */
  @Validation(rules = "validation", errorPage = "confirm.jsp")
  public ActionResult save() {
    if (this.id == null) {
      Todo todo = todoDxo.convert(this);
      todoDao.insert(todo);
      flash.put("notice", todo.getText() + "を追加しました。");
    } else {
      Todo todo = todoDxo.convert(this);
      todoDao.update(todo);
      flash.put("notice", todo.getText() + "を更新しました。");
    }
    return new Redirect("/todo/");
  }
  ...

confirm.jsp

...
<h2>Todo編集確認</h2>
以下の内容で登録しますがよろしいですか?
<div class="menu">
[<a href="javascript:doBack()">戻る</a>]
</div>
<t:form action="${contextPath}/todo/save" method="post" value="${action}">
<t:input type="hidden" name="id"/>
<t:input type="hidden" name="text"/>
<t:input type="hidden" name="typeId"/>
<t:input type="hidden" name="limitDate" value="${limitDate}"/>
<t:input type="hidden" name="memo"/>
<table border="1">
  <tr>
    <th>内容</th>
    <td>${f:out(text)}</td>
  </tr>
  <tr>
    <th>種別</th>
    <td>${f:out(todoType.name)}</td>
  </tr>
  <tr>
    <th>期限日</th>
    <td>${f:out(limitDate)}</td>
  </tr>
  <tr>
    <th>メモ</th>
    <td>${f:out(memo)}</td>
  </tr>
  <tr>
    <th></th>
    <td><input type="submit" value="登録"/></td>
  </tr>
</table>
</t:form>
</div><!-- End of id="content" -->
<c:import url="fotter.jsp" />
</body>
...

フラッシュメッセージの利用

リダイレクト前の処理から、リダイレクト後の画面表示処理に値を引き継ぎたいときは、 揮発性のフラッシュメッセージを使用します。 Actionのフィールドflash を使用して、 追加・更新のメッセージを受け渡しています。 フラッシュメッセージは次画面のフォワード処理後にクリアされます。 フラッシュメッセージを使用する場合、フラッシュメッセージ表示用のJSP部品を用意しておくと便利です(common/notice.jsp)。 サンプルではscript.aculo.usEffectsを利用して フラッシュメッセージをエフェクト付きで表示しています。

フラッシュメッセージの表示

フラッシュメッセージの表示

TodoAction.java

  public ActionResult save() {
    ...
      flash.put("notice", todo.getText() + "を追加しました。");
    ...
  }

list.jsp

<c:import url="/common/notice.jsp"/>

common/notice.jsp

<c:if test="${f:containsKey(flash, 'notice')}">
<div id="notice" class="notice">${f:out(flash['notice'])}</div>
...
</c:if>