チュートリアル(Todoアプリケーション JSP編)
********************************************************************************

.. only:: html

.. contents:: 目次
  :depth: 3
  :local:

|

はじめに
================================================================================

このチュートリアルで学ぶこと
--------------------------------------------------------------------------------

* \ |framework_name|\による基本的なアプリケーションの開発方法
* MavenおよびSTS(Eclipse)プロジェクトの構築方法
* \ |framework_name|\の\ :doc:`../Overview/ApplicationLayering`\ に従った開発方法

|

対象読者
--------------------------------------------------------------------------------

* SpringのDIやAOPに関する基礎的な知識がある
* Servlet/JSPを使用してWebアプリケーションを開発したことがある
* SQLに関する知識がある

|

検証環境
--------------------------------------------------------------------------------

このチュートリアルは以下の環境で動作確認している。他の環境で実施する際は本書をベースに適宜読み替えて設定していくこと。

.. tabularcolumns:: |p{0.30\linewidth}|p{0.70\linewidth}|
.. list-table::
  :header-rows: 1
  :widths: 30 70

  * - 種別
    - 名前
  * - OS
    - Windows 11
  * - JVM
    - \ :url_redhat_openjdk:`Java <>`\  17
  * - IDE
    - \ :url_spring_io:`Spring Tool Suite </tools>`\  |sts_version| (以降「STS」と呼ぶ。設定方法は\ :doc:`../Appendix/SpringToolSuite`\ を参照されたい。)
  * - Build Tool
    - \ :url_maven:`Apache Maven </download.cgi>`\  |maven_version| (以降「Maven」と呼ぶ)
  * - Application Server
    - \ :url_tomcat:`Apache Tomcat </index.html>`\  |tomcat_version|
  * - Web Browser
    - \ :url_chrome:`Google Chrome </>`\  |chrome_version|

|

作成するアプリケーションの説明
================================================================================

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

TODOを管理するアプリケーションを作成する。TODOの一覧表示、TODOの登録、TODOの完了、TODOの削除を行える。

.. figure:: ./images_TutorialTodo/image001.png
  :width: 50%

|

.. _app-requirement:

アプリケーションの業務要件
--------------------------------------------------------------------------------
アプリケーションの業務要件は、以下の通りとする。

.. tabularcolumns:: |p{0.20\linewidth}|p{0.80\linewidth}|
.. list-table::
  :header-rows: 1
  :widths: 20 80

  * - ルールID
    - 説明
  * - B01
    - 未完了のTODOは5件までしか登録できない
  * - B02
    - 完了済みのTODOは完了できない

.. note::

   本要件は学習のためのもので、現実的なTODO管理アプリケーションとしては適切ではない。

|

アプリケーションの処理仕様
--------------------------------------------------------------------------------
アプリケーションの処理仕様と画面遷移は、以下の通りとする。

.. figure:: ./images_TutorialTodo/image002.png
  :width: 60%

.. tabularcolumns:: |p{0.10\linewidth}|p{0.20\linewidth}|p{0.10\linewidth}|p{0.15\linewidth}|p{0.45\linewidth}|
.. list-table::
  :header-rows: 1
  :widths: 10 20 10 15 45

  * - 項番
    - プロセス名
    - HTTPメソッド
    - URL
    - 備考
  * - 1
    - Show all TODO
    - \-
    - /todo/list
    -
  * - 2
    - Create TODO
    - POST
    - /todo/create
    - 作成処理終了後、Show all TODOへリダイレクト
  * - 3
    - Finish TODO
    - POST
    - /todo/finish
    - 完了処理終了後、Show all TODOへリダイレクト
  * - 4
    - Delete TODO
    - POST
    - /todo/delete
    - 削除処理終了後、Show all TODOへリダイレクト

|

Show all TODO
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

* TODOを全件表示する
* 未完了のTODOに対しては「Finish」と「Delete」用のボタンが付く
* 完了のTODOは打ち消し線で装飾する
* TODOの件名のみ表示する

|

Create TODO
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

* フォームから送信されたTODOを保存する
* TODOの件名は1文字以上30文字以下であること
* \ :ref:`app-requirement`\ のB01を満たさない場合はエラーコードE001でビジネス例外をスローする
* 処理が成功した場合は、遷移先の画面で「Created successfully!」を表示する

|

Finish TODO
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

* フォームから送信された\ ``todoId``\ に対応するTODOを完了済みにする
* 該当するTODOが存在しない場合はエラーコードE404でリソース未検出例外をスローする
* \ :ref:`app-requirement`\ のB02を満たさない場合はエラーコードE002でビジネス例外をスローする
* 処理が成功した場合は、遷移先の画面で「Finished successfully!」を表示する

|

Delete TODO
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

* フォームから送信された\ ``todoId``\ に対応するTODOを削除する
* 該当するTODOが存在しない場合はエラーコードE404でリソース未検出例外をスローする
* 処理が成功した場合は、遷移先の画面で「Deleted successfully!」を表示する

|

エラーメッセージ一覧
--------------------------------------------------------------------------------

エラーメッセージとして、以下の3つを定義する。

.. tabularcolumns:: |p{0.15\linewidth}|p{0.50\linewidth}|p{0.35\linewidth}|
.. list-table::
  :header-rows: 1
  :widths: 15 50 35

  * - エラーコード
    - メッセージ
    - 置換パラメータ
  * - E001
    - [E001] The count of un-finished Todo must not be over {0}.
    - {0}… max unfinished count
  * - E002
    - [E002] The requested Todo is already finished. (id={0})
    - {0}… todoId
  * - E404
    - [E404] The requested Todo is not found. (id={0})
    - {0}… todoId

|

環境構築
================================================================================

本チュートリアルでは、インフラストラクチャ層のRepositoryImplの実装として、

* データベースを使用せず\ ``java.util.Map``\ を使ったインメモリ実装のRepositoryImpl
* MyBatis3を使用してデータベースにアクセスするRepositoryImpl
* Spring Data JPAを使用してデータベースにアクセスするRepositoryImpl

の3種類を用意している。用途に応じていずれかを選択する。

チュートリアルの進行上、まずはインメモリ実装を試し、その後MyBatis3またはSpring Data JPAを選ぶのが円滑である。

|

プロジェクトの作成
--------------------------------------------------------------------------------

本チュートリアルを順序通り読み進める場合は、以下の\ ``mvn archetype:generate``\ コマンドを実行し、O/R Mapperに依存しないブランクプロジェクトを作成すること。

.. tabs::

  .. group-tab:: Java Config

    .. code-block:: console

      mvn archetype:generate -B^
       -DarchetypeGroupId=org.terasoluna.gfw.blank^
       -DarchetypeArtifactId=terasoluna-gfw-web-blank-jsp-archetype^
       -DarchetypeVersion=5.10.1.RELEASE^
       -DgroupId=com.example.todo^
       -DartifactId=todo^
       -Dversion=1.0.0-SNAPSHOT

  .. group-tab:: XML Config

    .. code-block:: console

      mvn archetype:generate -B^
       -DarchetypeGroupId=org.terasoluna.gfw.blank^
       -DarchetypeArtifactId=terasoluna-gfw-web-blank-xmlconfig-jsp-archetype^
       -DarchetypeVersion=5.10.1.RELEASE^
       -DgroupId=com.example.todo^
       -DartifactId=todo^
       -Dversion=1.0.0-SNAPSHOT

プロジェクトの作成についての詳細は、「\ :ref:`BlankProjectCreateBlankProject`\ 」を参照されたい。

|

プロジェクトのインポート
--------------------------------------------------------------------------------

作成したブランクプロジェクトをSTSへインポートする。

プロジェクトのインポートについては、「\ :ref:`BlankProjectBlankProjectIntoSts`\ 」を参照されたい。

|

プロジェクトの構成
--------------------------------------------------------------------------------

本チュートリアルで作成するプロジェクトの構成については、「\ :ref:`BlankProjectProjectNotRelyingOnOrMapper`\ 」を参照されたい。

.. note::

  \ :ref:`前節の「プロジェクト構成」 <application-layering_project-structure>`\ ではマルチプロジェクトにすることを推奨していたが、本チュートリアルでは、学習容易性を重視しているためシングルプロジェクト構成にしている。

  \ **ただし、実プロジェクトで適用する場合は、マルチプロジェクト構成を強く推奨する。**\

  マルチプロジェクトの作成方法は、「\ :ref:`BlankProjectCreateBlankProject`\ 」を参照されたい。

|

設定ファイルの確認
--------------------------------------------------------------------------------
チュートリアルを進める上で必要となる設定の多くは、作成したブランクプロジェクトに既に設定済みの状態である。

チュートリアルを実施するだけであれば、これらの設定の理解は必須ではないが、アプリケーションを動かすためにどのような設定が必要なのかを理解しておくことを推奨する。

アプリケーションを動かすために必要な設定(設定ファイル)の解説については、「\ :ref:`BlankProjectCheckConfigurationFile`\ 」を参照されたい。

.. note::

  まず、手を動かしてTodoアプリケーションを作成したい場合は、設定ファイルの確認は読み飛ばしてもよいが、Todoアプリケーションを作成した後に一読して頂きたい。

|

プロジェクトの動作確認
--------------------------------------------------------------------------------
Todoアプリケーションの開発を始める前に、プロジェクトの動作確認を行う。
プロジェクトの動作確認の手順については、「\ :ref:`BlankProjectVerifyOperationOfBlankProject`\ 」を参照されたい。

Todoアプリケーションの作成
================================================================================
| Todoアプリケーションを作成する。作成する順は、以下の通りである。

* ドメイン層(+ インフラストラクチャ層)

  * Domain Object作成
  * Repository作成
  * RepositoryImpl作成
  * Service作成

* アプリケーション層

  * Controller作成
  * Form作成
  * View作成

|

RepositoryImplの作成は、選択したインフラストラクチャ層の種類に応じて実装方法が異なる。

| ここでは、データベースを使用せず\ ``java.util.Map``\ を使ったインメモリ実装のRepositoryImplを作成する方法について説明を行う。
| データベースを使用する場合は、「\ :ref:`tutorial-todo_infra`\ 」に記載されている内容で読み替えて、Todoアプリケーションを作成して頂きたい。

|

ドメイン層の作成
--------------------------------------------------------------------------------

Domain Objectの作成
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Domainオブジェクトを作成する。

Package Explorer上で右クリック -> New -> Class を選択し、「New Java Class」ダイアログを表示し、

  .. tabularcolumns:: |p{0.10\linewidth}|p{0.30\linewidth}|p{0.50\linewidth}|
  .. list-table::
    :header-rows: 1
    :widths: 10 30 50

    * - 項番
      - 項目
      - 入力値
    * - 1
      - Package
      - \ ``com.example.todo.domain.model``\
    * - 2
      - Name
      - \ ``Todo``\
    * - 3
      - Interfaces
      - \ ``java.io.Serializable``\

を入力して「Finish」する。

.. figure:: ./images_TutorialTodo/image057.png
  :width: 70%

作成したクラスは以下のディレクトリに格納される。

.. tabs::
  .. group-tab:: Java Config

     .. figure:: ./images_TutorialTodo/image058_JavaConfig.png

  .. group-tab:: XML Config

     .. figure:: ./images_TutorialTodo/image058_XMLConfig.png


|

作成したクラスに以下のプロパティを追加する。

* ID → todoId
* タイトル → todoTitle
* 完了フラグ → finished
* 作成日 → createdAt

.. code-block:: java

  package com.example.todo.domain.model;

  import java.io.Serializable;
  import java.time.LocalDate;

  public class Todo implements Serializable {

      private static final long serialVersionUID = 1L;

      private String todoId;

      private String todoTitle;

      private boolean finished;

      private LocalDate createdAt;

      public String getTodoId() {
          return todoId;
      }

      public void setTodoId(String todoId) {
          this.todoId = todoId;
      }

      public String getTodoTitle() {
          return todoTitle;
      }

      public void setTodoTitle(String todoTitle) {
          this.todoTitle = todoTitle;
      }

      public boolean isFinished() {
          return finished;
      }

      public void setFinished(boolean finished) {
          this.finished = finished;
      }

      public LocalDate getCreatedAt() {
          return createdAt;
      }

      public void setCreatedAt(LocalDate createdAt) {
          this.createdAt = createdAt;
      }
  }

.. tip::

  Getter/SetterメソッドはSTSの機能を使って自動生成することができる。
  
  フィールドを定義した後、エディタ上で右クリックし、「Source」->「Generate Getters and Setters…」を選択する。

  .. figure:: ./images_TutorialTodo/image059.png
    :width: 90%

  serialVersionUID以外を選択して「Generate」

  .. figure:: ./images_TutorialTodo/image060.png
    :width: 60%

|

.. _TutorialTodoCreateRepository:

Repositoryの作成
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

| \ ``TodoRepository``\ インタフェースを作成する。
| データベースを使用する場合は、「\ :ref:`tutorial-todo_infra`\ 」に記載されている内容で読み替えて、Repositoryを作成する。

Package Explorer上で右クリック -> New -> Interface を選択し、「New Java Interface」ダイアログを表示し、

  .. tabularcolumns:: |p{0.10\linewidth}|p{0.30\linewidth}|p{0.50\linewidth}|
  .. list-table::
    :header-rows: 1
    :widths: 10 30 50

    * - 項番
      - 項目
      - 入力値
    * - 1
      - Package
      - \ ``com.example.todo.domain.repository.todo``\
    * - 2
      - Name
      - \ ``TodoRepository``\

を入力して「Finish」する。

作成したインタフェースは以下のディレクトリに格納される。

.. tabs::
  .. group-tab:: Java Config

     .. figure:: ./images_TutorialTodo/image061_JavaConfig.png

  .. group-tab:: XML Config

     .. figure:: ./images_TutorialTodo/image061_XMLConfig.png


作成したインタフェースに、今回のアプリケーションで必要となる以下のCRUD操作を行うメソッドを定義する。

* TODOの1件取得 → findById
* TODOの全件取得 → findAll
* TODOの1件作成 → create
* TODOの1件更新 → update
* TODOの1件削除 → delete
* 完了済みTODO件数の取得 → countByFinished

.. code-block:: java

  package com.example.todo.domain.repository.todo;

  import java.util.Collection;
  import java.util.Optional;
  import com.example.todo.domain.model.Todo;

  public interface TodoRepository {
      Optional<Todo> findById(String todoId);

      Collection<Todo> findAll();

      void create(Todo todo);

      boolean update(Todo todo);

      void delete(Todo todo);

      long countByFinished(boolean finished);
  }

.. note::

  \ ``java.util.Optional``\ 型はJava 8から導入された\ ``null``\ を安全に扱うための仕組みであり、コーディングミスによる\ ``NullPointerException``\ 等の防止に寄与する。

  Repositoryの1件取得（\ ``findById``\ ）の戻り値を\ ``Optional``\ 型にすべきか否かは、以下のようにO/R Mapperにより異なるため、採用するO/R Mapperの仕様を確認されたい。

  * O/R Mapperに依存しない場合とMyBatis3を利用する場合は、\ **Optional型にしても良い。**\
  * Spring Data JPAを利用する場合は、Repositoryのメソッドシグネチャが決まっており\ **必ずOptional型となる。**\

  本チュートリアルではO/R Mapperによらず戻り値に\ ``Optional``\ 型に統一しているが、これはRepositoryを上記3種類のO/R Mapperで実装し、それらを呼び出すServiceの実装を統一するためである。

.. note::

  ここでは、\ ``TodoRepository``\ の汎用性を上げるため、「完了済み件数を取得する」メソッド(\ ``long countFinished()``\ )ではなく、「完了状態がxxである件数を取得する」メソッド(\ ``long countByFinished(boolean)``\ )として定義している。

  \ ``long countByFinished(boolean)``\ の引数として\ ``true``\ を渡すと「完了済みの件数」、\ ``false``\ を渡すと「未完了の件数」が取得できる仕様としている。

|

RepositoryImplの作成(インフラストラクチャ層)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

| ここでは、説明を単純化するため、\ ``java.util.Map``\ を使ったインメモリ実装のRepositoryImplを作成する。
| データベースを使用する場合は、「\ :ref:`tutorial-todo_infra`\ 」に記載されている内容で読み替えて、RepositoryImplを作成する。

Package Explorer上で右クリック -> New -> Class を選択し、「New Java Class」ダイアログを表示し、

  .. tabularcolumns:: |p{0.10\linewidth}|p{0.30\linewidth}|p{0.50\linewidth}|
  .. list-table::
    :header-rows: 1
    :widths: 10 30 50

    * - 項番
      - 項目
      - 入力値
    * - 1
      - Package
      - \ ``com.example.todo.domain.repository.todo``\
    * - 2
      - Name
      - \ ``TodoRepositoryImpl``\
    * - 3
      - Interfaces
      - \ ``com.example.todo.domain.repository.todo.TodoRepository``\

を入力して「Finish」する。

作成したクラスは以下のディレクトリに格納される。

.. tabs::
  .. group-tab:: Java Config

     .. figure:: ./images_TutorialTodo/image062_JavaConfig.png

  .. group-tab:: XML Config

     .. figure:: ./images_TutorialTodo/image062_XMLConfig.png


作成したクラスにCRUD操作を実装する。

.. note::

  RepositoryImplには、業務ロジックは含めず、Domainオブジェクトの保存先への出し入れ(CRUD操作)に終始することが実装ポイントである。

.. code-block:: java

  package com.example.todo.domain.repository.todo;

  import java.util.Collection;
  import java.util.Map;
  import java.util.Optional;
  import java.util.concurrent.ConcurrentHashMap;
  import org.springframework.stereotype.Repository;
  import com.example.todo.domain.model.Todo;

  @Repository // (1)
  public class TodoRepositoryImpl implements TodoRepository {
      private static final Map<String, Todo> TODO_MAP = new ConcurrentHashMap<>();

      @Override
      public Optional<Todo> findById(String todoId) {
          return Optional.ofNullable(TODO_MAP.get(todoId));
      }

      @Override
      public Collection<Todo> findAll() {
          return TODO_MAP.values();
      }

      @Override
      public void create(Todo todo) {
          TODO_MAP.put(todo.getTodoId(), todo);
      }

      @Override
      public boolean update(Todo todo) {
          TODO_MAP.put(todo.getTodoId(), todo);
          return true;
      }

      @Override
      public void delete(Todo todo) {
          TODO_MAP.remove(todo.getTodoId());
      }

      @Override
      public long countByFinished(boolean finished) {
          long count = 0;
          for (Todo todo : TODO_MAP.values()) {
              if (finished == todo.isFinished()) {
                  count++;
              }
          }
          return count;
      }
  }

.. tabularcolumns:: |p{0.10\linewidth}|p{0.80\linewidth}|
.. list-table::
  :header-rows: 1
  :widths: 10 80

  * - 項番
    - 説明
  * - | (1)
    - | Repositoryとしてcomponent-scan対象とするため、クラスレベルに\ ``@Repository``\ アノテーションをつける。

.. note::

  本チュートリアルでは、インフラストラクチャ層に属するクラス(RepositoryImpl)をドメイン層のパッケージ(\ ``com.example.todo.domain``\ )に格納しているが、完全に層別にパッケージを分けるのであれば、インフラストラクチャ層のクラスは、\ ``com.example.todo.infra``\ 以下に作成した方が良い。

  ただし、通常のプロジェクトでは、インフラストラクチャ層が変更されることを前提としていない(そのような前提で進めるプロジェクトは、少ない)。

  そこで、作業効率向上のために、ドメイン層のRepositoryインタフェースと同じ階層に、RepositoryImplを作成しても良い。

|

Serviceの作成
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

まず、\ ``TodoService``\ インタフェースを作成する。

Package Explorer上で右クリック -> New -> Interface を選択し、「New Java Interface」ダイアログを表示し、

  .. tabularcolumns:: |p{0.10\linewidth}|p{0.30\linewidth}|p{0.50\linewidth}|
  .. list-table::
    :header-rows: 1
    :widths: 10 30 50

    * - 項番
      - 項目
      - 入力値
    * - 1
      - Package
      - \ ``com.example.todo.domain.service.todo``\
    * - 2
      - Name
      - \ ``TodoService``\

を入力して「Finish」する。

作成したインタフェースは以下のディレクトリに格納される。

.. tabs::
  .. group-tab:: Java Config

     .. figure:: ./images_TutorialTodo/image063_JavaConfig.png

  .. group-tab:: XML Config

     .. figure:: ./images_TutorialTodo/image063_XMLConfig.png


作成したインタフェースに以下の業務処理を行うメソッドを定義する。

* Todoの全件取得 → findAll
* Todoの新規作成 → create
* Todoの完了 → finish
* Todoの削除 → delete

.. code-block:: java

  package com.example.todo.domain.service.todo;

  import java.util.Collection;
  import com.example.todo.domain.model.Todo;

  public interface TodoService {
      Collection<Todo> findAll();

      Todo create(Todo todo);

      Todo finish(String todoId);

      void delete(String todoId);
  }

|

次に、\ ``TodoService``\ インタフェースに定義したメソッドを実装する\ ``TodoServiceImpl``\ クラスを作成する。

Package Explorer上で右クリック -> New -> Class を選択し、「New Java Class」ダイアログを表示し、

  .. tabularcolumns:: |p{0.10\linewidth}|p{0.30\linewidth}|p{0.50\linewidth}|
  .. list-table::
    :header-rows: 1
    :widths: 10 30 50

    * - 項番
      - 項目
      - 入力値
    * - 1
      - Package
      - \ ``com.example.todo.domain.service.todo``\
    * - 2
      - Name
      - \ ``TodoServiceImpl``\
    * - 3
      - Interfaces
      - \ ``com.example.todo.domain.service.todo.TodoService``\

を入力して「Finish」する。

作成したクラスは以下のディレクトリに格納される。

.. tabs::
  .. group-tab:: Java Config

     .. figure:: ./images_TutorialTodo/image064_JavaConfig.png

  .. group-tab:: XML Config

     .. figure:: ./images_TutorialTodo/image064_XMLConfig.png


.. code-block:: java

  package com.example.todo.domain.service.todo;

  import java.time.LocalDate;
  import java.util.Collection;
  import java.util.UUID;
  import org.springframework.stereotype.Service;
  import org.springframework.transaction.annotation.Transactional;
  import org.terasoluna.gfw.common.exception.BusinessException;
  import org.terasoluna.gfw.common.exception.ResourceNotFoundException;
  import org.terasoluna.gfw.common.message.ResultMessage;
  import org.terasoluna.gfw.common.message.ResultMessages;
  import com.example.todo.domain.model.Todo;
  import com.example.todo.domain.repository.todo.TodoRepository;
  import jakarta.inject.Inject;

  @Service // (1)
  @Transactional // (2)
  public class TodoServiceImpl implements TodoService {

      private static final long MAX_UNFINISHED_COUNT = 5;

      @Inject // (3)
      TodoRepository todoRepository;

      @Override
      @Transactional(readOnly = true) // (4)
      public Collection<Todo> findAll() {
          return todoRepository.findAll();
      }

      @Override
      public Todo create(Todo todo) {
          long unfinishedCount = todoRepository.countByFinished(false);
          if (unfinishedCount >= MAX_UNFINISHED_COUNT) {
              // (5)
              ResultMessages messages = ResultMessages.error();
              messages.add(
                      ResultMessage.fromText("[E001] The count of un-finished Todo must not be over "
                              + MAX_UNFINISHED_COUNT + "."));
              // (6)
              throw new BusinessException(messages);
          }

          // (7)
          String todoId = UUID.randomUUID().toString();
          LocalDate createdAt = LocalDate.now();

          todo.setTodoId(todoId);
          todo.setCreatedAt(createdAt);
          todo.setFinished(false);

          todoRepository.create(todo);
          /* REMOVE THIS LINE IF YOU USE JPA
              todoRepository.save(todo); // (8)
             REMOVE THIS LINE IF YOU USE JPA */

          return todo;
      }

      @Override
      public Todo finish(String todoId) {
          Todo todo = findOne(todoId);
          if (todo.isFinished()) {
              ResultMessages messages = ResultMessages.error();
              messages.add(ResultMessage.fromText(
                      "[E002] The requested Todo is already finished. (id=" + todoId + ")"));
              throw new BusinessException(messages);
          }
          todo.setFinished(true);
          todoRepository.update(todo);
          /* REMOVE THIS LINE IF YOU USE JPA
              todoRepository.save(todo); // (9)
             REMOVE THIS LINE IF YOU USE JPA */
          return todo;
      }

      @Override
      public void delete(String todoId) {
          Todo todo = findOne(todoId);
          todoRepository.delete(todo);
      }

      // (10)
      private Todo findOne(String todoId) {
          return todoRepository.findById(todoId).orElseThrow(() -> {
              // (11)
              ResultMessages messages = ResultMessages.error();
              messages.add(ResultMessage
                      .fromText("[E404] The requested Todo is not found. (id=" + todoId + ")"));
              return new ResourceNotFoundException(messages);
          });
      }
  }

.. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}|
.. list-table::
  :header-rows: 1
  :widths: 10 90
  :class: longtable

  * - 項番
    - 説明
  * - | (1)
    - | Serviceとしてcomponent-scanの対象とするため、クラスレベルに\ ``@Service``\ アノテーションをつける。
  * - | (2)
    - | クラスレベルに、\ ``@Transactional``\ アノテーションをつけることで、公開メソッドをすべてトランザクション管理する。
      | アノテーションを付与することで、メソッド開始時にトランザクションを開始、メソッド正常終了時にトランザクションのコミットが行われる。
      | また、途中で非検査例外が発生した場合は、トランザクションがロールバックされる。
      |
      | データベースを使用しない場合は、\ ``@Transactional``\ アノテーションは不要である。
  * - | (3)
    - | \ ``@Inject``\ アノテーションで、\ ``TodoRepository``\ の実装をインジェクションする。
  * - | (4)
    - | 参照のみ行う処理に関しては、\ ``readOnly=true``\ をつける。
      | O/R Mapperによっては、この設定により、参照時のトランザクション制御の最適化が行われる(JPAを使用する場合、効果はない)。
      |
      | データベースを使用しない場合は、\ ``@Transactional``\ アノテーションは不要である。
  * - | (5)
    - | 結果メッセージを格納するクラスとして、共通ライブラリで用意されている\ ``org.terasoluna.gfw.common.message.ResultMessage``\ を用いる。
      | 今回は、エラーメッセージを例外に追加する際に、\ ``ResultMessages.error()``\ でメッセージ種別を指定して、\ ``ResultMessage``\ を追加している。
  * - | (6)
    - | 業務エラーが発生した場合、共通ライブラリで用意されている\ ``org.terasoluna.gfw.common.exception.BusinessException``\ をスローする。
  * - | (7)
    - | 一意性のある値を生成するために、UUIDを使用している。データベースのシーケンスを用いてもよい。
  * - | (8)
    - | Spring Data JPAを使用してデータベースにアクセスする場合は、\ ``create``\ メソッドではなく、\ ``save``\ メソッドを呼び出す。
  * - | (9)
    - | Spring Data JPAを使用してデータベースにアクセスする場合は、\ ``update``\ メソッドではなく、\ ``save``\ メソッドを呼び出す。
  * - | (10)
    - | 1件取得は、\ ``finish``\ メソッドでも\ ``delete``\ メソッドでも使用するため、メソッドとして用意しておく(interfaceに公開しても良い)。
  * - | (11)
    - | 取得したデータを返す。対象のデータが存在しない場合は共通ライブラリで用意されている\ ``org.terasoluna.gfw.common.exception.ResourceNotFoundException``\ をスローする。

.. note::

  本節では、説明を単純化するため、エラーメッセージをハードコードしているが、メンテナンスの観点で本来は好ましくない。通常、メッセージは、プロパティファイルに外部化することが推奨される。

  プロパティファイルに外部化する方法は、\ :doc:`../ArchitectureInDetail/GeneralFuncDetail/PropertyManagement`\ を参照されたい。

|

アプリケーション層の作成
--------------------------------------------------------------------------------

ドメイン層の実装が完了したので、次はドメイン層を利用して、アプリケーション層の作成に取り掛かる。

|

Controllerの作成
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

まずは、Todo管理業務にかかわる画面遷移を、制御するControllerを作成する。

Package Explorer上で右クリック -> New -> Class を選択し、「New Java Class」ダイアログを表示し、

  .. tabularcolumns:: |p{0.10\linewidth}|p{0.30\linewidth}|p{0.50\linewidth}|
  .. list-table::
    :header-rows: 1
    :widths: 10 30 50

    * - 項番
      - 項目
      - 入力値
    * - 1
      - Package
      - \ ``com.example.todo.app.todo``\
    * - 2
      - Name
      - \ ``TodoController``\

を入力して「Finish」する。

.. note::

  \ **上位パッケージがドメイン層と異なるので注意すること。**\

作成したクラスは以下のディレクトリに格納される。

.. tabs::
  .. group-tab:: Java Config

     .. figure:: ./images_TutorialTodo/image065_JavaConfig.png

  .. group-tab:: XML Config

     .. figure:: ./images_TutorialTodo/image065_XMLConfig.png


.. code-block:: java

  package com.example.todo.app.todo;

  import org.springframework.stereotype.Controller;
  import org.springframework.web.bind.annotation.RequestMapping;

  @Controller // (1)
  @RequestMapping("todo") // (2)
  public class TodoController {

  }

.. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}|
.. list-table::
  :header-rows: 1
  :widths: 10 90

  * - 項番
    - 説明
  * - | (1)
    - | Controllerとしてcomponent-scanの対象とするため、クラスレベルに、\ ``@Controller``\ アノテーションをつける。
  * - | (2)
    - | \ ``TodoController``\ が扱う画面遷移のパスを、すべて\ ``<contextPath>/todo``\ 配下にするため、クラスレベルに\ ``@RequestMapping(“todo”)``\ を設定する。

|

Show all TODOの実装
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
本チュートリアルで作成する画面では、

* 新規作成フォームの表示
* TODOの全件表示

を行う。

はじめに、TODOの全件表示を行うための処理を実装する。

|

Formの作成
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""

Formクラス(JavaBean)を作成する。

Package Explorer上で右クリック -> New -> Class を選択し、「New Java Class」ダイアログを表示し、

  .. tabularcolumns:: |p{0.10\linewidth}|p{0.30\linewidth}|p{0.50\linewidth}|
  .. list-table::
    :header-rows: 1
    :widths: 10 30 50

    * - 項番
      - 項目
      - 入力値
    * - 1
      - Package
      - \ ``com.example.todo.app.todo``\
    * - 2
      - Name
      - \ ``TodoForm``\
    * - 3
      - Interfaces
      - \ ``java.io.Serializable``\

を入力して「Finish」する。

作成したクラスは以下のディレクトリに格納される。

.. tabs::
  .. group-tab:: Java Config

     .. figure:: ./images_TutorialTodo/image066_JavaConfig.png

  .. group-tab:: XML Config

     .. figure:: ./images_TutorialTodo/image066_XMLConfig.png

作成したクラスに以下のプロパティを追加する。

* タイトル → todoTitle

.. code-block:: java

  package com.example.todo.app.todo;

  import java.io.Serializable;

  public class TodoForm implements Serializable {
      private static final long serialVersionUID = 1L;

      private String todoTitle;

      public String getTodoTitle() {
          return todoTitle;
      }

      public void setTodoTitle(String todoTitle) {
          this.todoTitle = todoTitle;
      }

  }

|

Controllerの実装
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""

一覧画面表示処理を\ ``TodoController``\ に追加する。

.. code-block:: java

  package com.example.todo.app.todo;

  import java.util.Collection;
  import org.springframework.stereotype.Controller;
  import org.springframework.ui.Model;
  import org.springframework.web.bind.annotation.GetMapping;
  import org.springframework.web.bind.annotation.ModelAttribute;
  import org.springframework.web.bind.annotation.RequestMapping;
  import com.example.todo.domain.model.Todo;
  import com.example.todo.domain.service.todo.TodoService;
  import jakarta.inject.Inject;

  @Controller
  @RequestMapping("todo")
  public class TodoController {

      @Inject // (1)
      TodoService todoService;

      @ModelAttribute // (2)
      public TodoForm setUpForm() {
          TodoForm form = new TodoForm();
          return form;
      }

      @GetMapping("list") // (3)
      public String list(Model model) {
          Collection<Todo> todos = todoService.findAll();
          model.addAttribute("todos", todos); // (4)
          return "todo/list"; // (5)
      }
  }

.. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}|
.. list-table::
  :header-rows: 1
  :widths: 10 90

  * - 項番
    - 説明
  * - | (1)
    - | \ ``TodoService``\ を、DIコンテナによってインジェクションさせるために、\ ``@Inject``\ アノテーションをつける。
      |
      | DIコンテナの管理する\ ``TodoService``\ 型のインスタンス(\ ``TodoServiceImpl``\ のインスタンス)がインジェクションされる。
  * - | (2)
    - | Formを初期化する。
      |
      | \ ``@ModelAttribute``\ アノテーションをつけることで、このメソッドの返り値のformオブジェクトが、\ ``todoForm``\ という名前で\ ``Model``\ に追加される。
      | これは、\ ``TodoController``\ の各処理で、\ ``model.addAttribute("todoForm", form)``\ を実装するのと同義である。
  * - | (3)
    - | \ ``/todo/list``\ というパスに\ ``GET``\ メソッドを使用してリクエストされた際に、一覧画面表示処理用のメソッド(\ ``list``\ メソッド)が実行されるように\ ``@GetMapping``\ アノテーションを設定する。
      |
      | クラスレベルに\ ``@RequestMapping(“todo”)``\ が設定されているため、ここでは\ ``@GetMapping("list")``\ のみで良い。
  * - | (4)
    - | \ ``Model``\ にTodoのリストを追加して、Viewに渡す。
  * - | (5)
    - | View名として\ ``todo/list``\ を返すと、spring-mvc.xmlまたはSpringMvcConfig.javaに定義した\ ``ViewResolver``\ によって、\ :file:`WEB-INF/views/todo/list.jsp`\ がレンダリングされることになる。

.. note::

  \ ``@GetMapping``\ や\ ``@PostMapping``\ は、対応するHTTPメソッドにマッピングする。

  詳細は、\ :ref:`controller_mapping-label`\ を参照されたい。

|

JSPの作成
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
JSPを作成し、Controllerから渡されたModelを表示する。

Package Explorer上で右クリック -> New -> File を選択し、「Create New File」ダイアログを表示し、

  .. tabularcolumns:: |p{0.10\linewidth}|p{0.30\linewidth}|p{0.50\linewidth}|
  .. list-table::
    :header-rows: 1
    :widths: 10 30 50

    * - 項番
      - 項目
      - 入力値
    * - 1
      - Enter or select the parent folder
      - \ ``todo/src/main/webapp/WEB-INF/views/todo``\
    * - 2
      - File name
      - \ ``list.jsp``\

を入力して「Finish」する。

作成したファイルは以下のディレクトリに格納される。

.. figure:: ./images_TutorialTodo/create-list-jsp.png

まず、TODOの全件表示を行うために必要なJSPの実装を行う。

.. code-block:: jsp

  <!DOCTYPE html>
  <html>
      <head>
          <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
          <title>Todo List</title>
          <style type="text/css">
              .strike {
                  text-decoration: line-through;
              }
          </style>
      </head>
      <body>
          <h1>Todo List</h1>
          <hr />
          <div id="todoList">
              <ul>
                  <!-- (1) -->
                  <c:forEach items="${todos}" var="todo">
                      <li>
                          <c:choose>
                              <c:when test="${todo.finished}"><!-- (2) -->
                                  <span class="strike">
                                      <!-- (3) -->
                                      ${f:h(todo.todoTitle)}
                                  </span>
                              </c:when>
                              <c:otherwise> ${f:h(todo.todoTitle)} </c:otherwise>
                          </c:choose>
                      </li>
                  </c:forEach>
              </ul>
          </div>
      </body>
  </html>

.. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}|
.. list-table::
  :header-rows: 1
  :widths: 10 90

  * - 項番
    - 説明
  * - | (1)
    - | \ ``<c:forEach>``\ タグを用いて、Todoのリストを全て表示する。
  * - | (2)
    - | 完了かどうか(\ ``finished``\ )で、打ち消し線(\ ``text-decoration: line-through;``\ )を装飾するかどうかを判断する。
  * - | (3)
    - | \ **文字列値を出力する際は、XSS対策のため、必ずf:h()関数を使用してHTMLエスケープを行うこと。**\
      | XSS対策についての詳細は、\ :doc:`../Security/XSS`\ を参照されたい。

|

| STSで「todo」プロジェクトを右クリックし、「Run As」→「Run on Server」でWebアプリケーションを起動する。
| ブラウザで \ :url_localhost:`/todo/todo/list`\ にアクセスすると、以下のような画面が表示される。

.. figure:: ./images_TutorialTodo/image067.png
  :width: 25%

|

.. note::

  上記で表示されている画面には、TODOが1件も登録されていないため、TODOの一覧は出力されない。

  以下のように、ドメイン層の作成で作成したTodoRepositoryImplを一時的に修正し初期データを登録することで、TODOの一覧が出力されることを確認できる。

  なお、次節「\ :ref:`CreateTodoImplementation`\ 」で実際にTODOを登録できるようになるため、一覧の出力が確認できたら削除して構わない。

  * \ ``TodoRepositoryImpl.java``\

    .. code-block:: java

      package com.example.todo.domain.repository.todo;

      import java.util.Collection;
      import java.util.Map;
      import java.util.concurrent.ConcurrentHashMap;
      import org.springframework.stereotype.Repository;
      import com.example.todo.domain.model.Todo;

      @Repository
      public class TodoRepositoryImpl implements TodoRepository {
          private static final Map<String, Todo> TODO_MAP = new ConcurrentHashMap<String, Todo>();

          static {
              Todo todo1 = new Todo();
              todo1.setTodoId("1");
              todo1.setTodoTitle("Send a e-mail");
              Todo todo2 = new Todo();
              todo2.setTodoId("2");
              todo2.setTodoTitle("Have a lunch");
              Todo todo3 = new Todo();
              todo3.setTodoId("3");
              todo3.setTodoTitle("Read a book");
              todo3.setFinished(true);
              TODO_MAP.put(todo1.getTodoId(), todo1);
              TODO_MAP.put(todo2.getTodoId(), todo2);
              TODO_MAP.put(todo3.getTodoId(), todo3);
          }

          // omitted

  以下のように画面に出力される。

  .. figure:: ./images_TutorialTodo/show-all-todo-note.png
    :width: 30%

|

.. _CreateTodoImplementation:

Create TODOの実装
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

次に、一覧表示画面から「Create TODO」ボタンを押した後の、新規作成処理を実装する。

はじめに、TODOの全件表示を行うための処理を実装する。

|

マッパーインタフェースの作成
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""

Beanマッピングのマッパーインタフェースを作成する。

Package Explorer上で右クリック -> New -> Interface を選択し、「New Java Interface」ダイアログを表示し、

  .. tabularcolumns:: |p{0.10\linewidth}|p{0.30\linewidth}|p{0.50\linewidth}|
  .. list-table::
    :header-rows: 1
    :widths: 10 30 50

    * - 項番
      - 項目
      - 入力値
    * - 1
      - Package
      - \ ``com.example.todo.app.todo``\
    * - 2
      - Name
      - \ ``TodoMapper``\

を入力して「Finish」する。

作成したクラスは以下のディレクトリに格納される。

.. tabs::
  .. group-tab:: Java Config

     .. figure:: ./images_TutorialTodo/create-bean-mapper_JavaConfig.png

  .. group-tab:: XML Config

     .. figure:: ./images_TutorialTodo/create-bean-mapper_XMLConfig.png


作成したクラスに以下の\ ``@Mapper``\ アノテーションを付与したBeanマッピングメソッドを追加する。

* Todo map(TodoForm form)

  * \ ``@Mapping``\ アノテーションによるマッピング除外項目定義

    * createdAt
    * finished

.. code-block:: java

  package com.example.todo.app.todo;

  import org.mapstruct.Mapper;
  import org.mapstruct.Mapping;
  import com.example.todo.domain.model.Todo;

  @Mapper
  public interface TodoMapper {

      @Mapping(target = "createdAt", ignore = true)
      @Mapping(target = "finished", ignore = true)
      Todo map(TodoForm form);

  }

.. note::

  マッパーインタフェース追加後、以下のようなビルドエラーが発生する場合がある。

    .. code-block:: console

      Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.example.todo.app.todo.TodoMapper'

  この場合は、プロジェクト名を右クリックし、「Run As」->「Maven build」をクリックする。
  Goalsに「compile」を指定し「Run」をクリックする。

  .. figure:: ./images_TutorialTodo/mvnBuild.png
    :width: 40%

  ビルドが成功した後、プロジェクト名を右クリックし、「Run As」->「Maven install」をクリックする。

|

Controllerの修正
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""

新規作成処理を\ ``TodoController``\ に追加する。

.. code-block:: java

  package com.example.todo.app.todo;

  import java.util.Collection;
  import org.springframework.stereotype.Controller;
  import org.springframework.ui.Model;
  import org.springframework.validation.BindingResult;
  import org.springframework.web.bind.annotation.GetMapping;
  import org.springframework.web.bind.annotation.ModelAttribute;
  import org.springframework.web.bind.annotation.PostMapping;
  import org.springframework.web.bind.annotation.RequestMapping;
  import org.springframework.web.servlet.mvc.support.RedirectAttributes;
  import org.terasoluna.gfw.common.exception.BusinessException;
  import org.terasoluna.gfw.common.message.ResultMessage;
  import org.terasoluna.gfw.common.message.ResultMessages;
  import com.example.todo.domain.model.Todo;
  import com.example.todo.domain.service.todo.TodoService;
  import jakarta.inject.Inject;
  import jakarta.validation.Valid;

  @Controller
  @RequestMapping("todo")
  public class TodoController {

      @Inject
      TodoService todoService;

      // (1)
      @Inject
      TodoMapper beanMapper;

      @ModelAttribute
      public TodoForm setUpForm() {
          TodoForm form = new TodoForm();
          return form;
      }

      @GetMapping("list")
      public String list(Model model) {
          Collection<Todo> todos = todoService.findAll();
          model.addAttribute("todos", todos);
          return "todo/list";
      }

      @PostMapping("create") // (2)
      public String create(@Valid TodoForm todoForm, BindingResult bindingResult, // (3)
              Model model, RedirectAttributes attributes) { // (4)

          // (5)
          if (bindingResult.hasErrors()) {
              return list(model);
          }

          // (6)
          Todo todo = beanMapper.map(todoForm);

          try {
              todoService.create(todo);
          } catch (BusinessException e) {
              // (7)
              model.addAttribute(e.getResultMessages());
              return list(model);
          }

          // (8)
          attributes.addFlashAttribute(
                  ResultMessages.success().add(ResultMessage.fromText("Created successfully!")));
          return "redirect:/todo/list";
      }

  }

.. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}|
.. list-table::
  :header-rows: 1
  :widths: 10 90
  :class: longtable

  * - 項番
    - 説明
  * - | (1)
    - | FormオブジェクトをDomainObjectに変換するために、``TodoMapper``\ インタフェースをインジェクションする。
  * - | (2)
    - | \ ``/todo/create``\ というパスに\ ``POST``\ メソッドを使用してリクエストされた際に、新規作成処理用のメソッド(\ ``create``\ メソッド)が実行されるように\ ``@PostMapping``\ アノテーションを設定する。
  * - | (3)
    - | フォームの入力チェックを行うため、Formの引数に\ ``@Valid``\ アノテーションをつける。入力チェック結果は、その直後の引数\ ``BindingResult``\ に格納される。
  * - | (4)
    - | 正常に作成が完了した後にリダイレクトし、一覧画面を表示する。
      | リダイレクト先への情報を格納するために、引数に\ ``RedirectAttributes``\ を加える。
  * - | (5)
    - | 入力エラーがあった場合、一覧画面に戻る。
      | Todo全件取得を再度行う必要があるので、\ ``list``\ メソッドを再実行する。
  * - | (6)
    - | \ ``Mapstruct``\ を用いて、\ ``TodoForm``\ オブジェクトから\ ``Todo``\ オブジェクトを作成する。
  * - | (7)
    - | 業務処理を実行して、\ ``BusinessException``\ が発生した場合、結果メッセージを\ ``Model``\ に追加して、一覧画面に戻る。
  * - | (8)
    - | 正常に作成が完了したので、結果メッセージをflashスコープに追加して、一覧画面でリダイレクトする。
      | リダイレクトすることにより、ブラウザを再読み込みして、再び新規登録処理が\ ``POST``\ されることがなくなる。（詳しくは、「\ :ref:`DoubleSubmitProtectionAboutPRG`\ 」を参照されたい）
      | なお、今回は成功メッセージであるため、\ ``ResultMessages.success()``\ を使用している。

|

Formの修正
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""

入力チェックのルールを定義するため、Formオブジェクトにアノテーションを追加する。

.. code-block:: java

  package com.example.todo.app.todo;

  import java.io.Serializable;
  import jakarta.validation.constraints.NotNull;
  import jakarta.validation.constraints.Size;

  public class TodoForm implements Serializable {
      private static final long serialVersionUID = 1L;

      @NotNull // (1)
      @Size(min = 1, max = 30) // (2)
      private String todoTitle;

      public String getTodoTitle() {
          return todoTitle;
      }

      public void setTodoTitle(String todoTitle) {
          this.todoTitle = todoTitle;
      }
  }

.. tabularcolumns:: |p{0.10\linewidth}|p{0.80\linewidth}|
.. list-table::
  :header-rows: 1
  :widths: 10 80

  * - 項番
    - 説明
  * - | (1)
    - | \ ``@NotNull``\ アノテーションを使用して必須チェックを有効化する。
  * - | (2)
    - | \ ``@Size``\ アノテーションを使用して文字数チェックを有効化する。

|

JSPの修正
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""

以下を表示するために必要なJSPの実装を追加する。

* TODOの入力フォーム
* 「Create Todo」ボタン
* 入力チェックエラーを表示するエリア
* 結果メッセージを表示するエリア

.. code-block:: jsp

  <!DOCTYPE html>
  <html>
      <head>
          <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
          <title>Todo List</title>
          <style type="text/css">
              .strike {
                  text-decoration: line-through;
              }
          </style>
      </head>
      <body>
          <h1>Todo List</h1>
          <div id="todoForm">
              <!-- (1) -->
              <t:messagesPanel />

              <!-- (2) -->
              <form:form action="${pageContext.request.contextPath}/todo/create" method="post" modelAttribute="todoForm">
                  <form:input path="todoTitle" /><!-- (3) -->
                  <form:errors path="todoTitle" /><!-- (4) -->
                  <form:button>Create Todo</form:button>
              </form:form>
          </div>
          <hr />
          <div id="todoList">
              <ul>
                  <c:forEach items="${todos}" var="todo">
                      <li>
                          <c:choose>
                              <c:when test="${todo.finished}">
                                  <span class="strike"> ${f:h(todo.todoTitle)} </span>
                              </c:when>
                              <c:otherwise> ${f:h(todo.todoTitle)} </c:otherwise>
                          </c:choose>
                      </li>
                  </c:forEach>
              </ul>
          </div>
      </body>
  </html>

.. tabularcolumns:: |p{0.10\linewidth}|p{0.80\linewidth}|
.. list-table::
  :header-rows: 1
  :widths: 10 80

  * - 項番
    - 説明
  * - | (1)
    - | \ ``<t:messagesPanel>``\ タグで、結果メッセージを表示する。
  * - | (2)
    - | 新規作成処理用のformを表示する。
      | formを表示するために、\ ``<form:form>``\ タグを使用する。
      | \ ``modelAttribute``\ 属性には、Controllerで\ ``Model``\ に追加したFormの名前を指定する。
      | \ ``action``\ 属性には新規作成処理を実行するためのURL(\ ``<contextPath>/todo/create``\ )を指定する。
      | 新規作成処理は更新系の処理なので、\ ``method``\ 属性には\ ``POST``\ メソッドを指定する。
      |
      | \ ``action``\ 属性に指定する<contextPath>は、\ ``${pageContext.request.contextPath}``\ で取得することができる。
  * - | (3)
    - | \ ``<form:input>``\ タグでフォームのプロパティをバインドする。
      | \ ``modelAttribute``\ 属性に指定したFormのプロパティ名と、\ ``path``\ 属性の値が一致している必要がある。
  * - | (4)
    - | \ ``<form:errors>``\ タグで、入力エラーがあった場合に表示する。\ ``path``\ 属性の値は、\ ``<form:input>``\ タグと合わせる。

|

フォームに適切な値を入力してsubmitすると、以下のように、成功メッセージが表示される。

.. figure:: ./images_TutorialTodo/image068.png
  :width: 40%

.. figure:: ./images_TutorialTodo/image069.png
  :width: 40%

未完了のTODOが5件登録済みの場合は、業務エラーとなり、エラーメッセージが表示される。

.. figure:: ./images_TutorialTodo/image070.png
  :width: 60%

入力フォームを、空文字にしてsubmitすると、以下のように、エラーメッセージが表示される。

.. figure:: ./images_TutorialTodo/image071.png
  :width: 65%

|

メッセージ表示のカスタマイズ
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""

\ ``<t:messagesPanel>``\ を使用した場合、以下のようなHTMLが出力される。

.. code-block:: html

  <div class="alert alert-success"><ul><li>Created successfully!</li></ul></div>

スタイルシート(\ ``list.jsp``\ の\ ``<style>``\ タグ内)に、以下の修正を加えて、結果メッセージの見た目をカスタマイズする。

.. code-block:: css

  .alert {
      border: 1px solid;
  }

  .alert-error {
      background-color: #c60f13;
      border-color: #970b0e;
      color: white;
  }

  .alert-success {
      background-color: #5da423;
      border-color: #457a1a;
      color: white;
  }

|

メッセージは、以下のように装飾される。

.. figure:: ./images_TutorialTodo/image072.png
  :width: 40%

.. figure:: ./images_TutorialTodo/image073.png
  :width: 60%

|

また、\ ``<form:errors>``\ タグの\ ``cssClass``\ 属性で、入力エラーメッセージのclassを指定できる。

JSPを次のように修正し、

.. code-block:: jsp

  <form:errors path="todoTitle" cssClass="text-error" />

スタイルシートに、以下を追加する。

.. code-block:: css

  .text-error {
      color: #c60f13;
  }

入力エラー時のメッセージは、以下のように装飾される。

.. figure:: ./images_TutorialTodo/image074.png
  :width: 65%

|

Finish TODOの実装
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

一覧画面に「Finish」ボタンを追加し、TODOを完了させるための処理を追加する。

|

Formの修正
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""

完了処理用のFormについても、\ ``TodoForm``\ を使用する。

| \ ``TodoForm``\ に\ ``todoId``\ プロパティを追加する必要があるが、単純に追加してしまうと、新規作成処理でも\ ``todoId``\ プロパティのチェックが実行されてしまう。
| 一つのFormクラスを使用して複数のformから送信されるリクエストパラメータをバインドする場合は、\ ``groups``\ 属性を使用して、入力チェックルールをグループ化する。

Formクラスに以下のプロパティを追加する。

* ID → todoId

.. code-block:: java

  package com.example.todo.app.todo;

  import java.io.Serializable;
  import jakarta.validation.constraints.NotEmpty;
  import jakarta.validation.constraints.NotNull;
  import jakarta.validation.constraints.Size;

  public class TodoForm implements Serializable {
      // (1)
      public static interface TodoCreate {
      };

      public static interface TodoFinish {
      };

      private static final long serialVersionUID = 1L;

      // (2)
      @NotEmpty(groups = {TodoFinish.class})
      private String todoId;

      // (3)
      @NotNull(groups = {TodoCreate.class})
      @Size(min = 1, max = 30, groups = {TodoCreate.class})
      private String todoTitle;

      public String getTodoId() {
          return todoId;
      }

      public void setTodoId(String todoId) {
          this.todoId = todoId;
      }

      public String getTodoTitle() {
          return todoTitle;
      }

      public void setTodoTitle(String todoTitle) {
          this.todoTitle = todoTitle;
      }

  }

.. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}|
.. list-table::
  :header-rows: 1
  :widths: 10 90

  * - 項番
    - 説明
  * - | (1)
    - | 入力チェックルールをグループ化するためのインタフェースを作成する。
      | 入力チェックルールのグループ化については、\ :doc:`../ArchitectureInDetail/WebApplicationDetail/Validation`\ を参照されたい。
      |
      | ここでは、新規作成処理用のインタフェースとして\ ``TodoCreate``\ を、完了処理用のインタフェースとして\ ``TodoFinish``\ を作成している。
  * - | (2)
    - | \ ``todoId``\ は完了処理で使用するプロパティである。
      | そのため、\ ``@NotEmpty``\ アノテーションの\ ``groups``\ 属性には、完了処理用の入力チェックルールである事を示す\ ``TodoFinish``\ インタフェースを指定する。
  * - | (3)
    - | \ ``todoTitle``\ は新規作成処理で使用するプロパティである。
      | そのため、\ ``@NotNull``\ アノテーションと\ ``@Size``\ アノテーションの\ ``groups``\ 属性には、新規作成処理用の入力チェックルールである事を示す\ ``TodoCreate``\ インタフェースを指定する。

|

Controllerの修正
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""

完了処理を\ ``TodoController``\ に追加する。

グループ化した入力チェックルールを適用するためには、\ **@Valid アノテーションの代わりに、@Validated アノテーションを使用すること**\ に注意する。

.. code-block:: java

  package com.example.todo.app.todo;

  import java.util.Collection;
  import org.springframework.stereotype.Controller;
  import org.springframework.ui.Model;
  import org.springframework.validation.BindingResult;
  import org.springframework.validation.annotation.Validated;
  import org.springframework.web.bind.annotation.GetMapping;
  import org.springframework.web.bind.annotation.ModelAttribute;
  import org.springframework.web.bind.annotation.PostMapping;
  import org.springframework.web.bind.annotation.RequestMapping;
  import org.springframework.web.servlet.mvc.support.RedirectAttributes;
  import org.terasoluna.gfw.common.exception.BusinessException;
  import org.terasoluna.gfw.common.message.ResultMessage;
  import org.terasoluna.gfw.common.message.ResultMessages;
  import com.example.todo.app.todo.TodoForm.TodoCreate;
  import com.example.todo.app.todo.TodoForm.TodoFinish;
  import com.example.todo.domain.model.Todo;
  import com.example.todo.domain.service.todo.TodoService;
  import jakarta.inject.Inject;
  import jakarta.validation.groups.Default;

  @Controller
  @RequestMapping("todo")
  public class TodoController {

      @Inject
      TodoService todoService;

      @Inject
      TodoMapper beanMapper;

      @ModelAttribute
      public TodoForm setUpForm() {
          TodoForm form = new TodoForm();
          return form;
      }

      @GetMapping("list")
      public String list(Model model) {
          Collection<Todo> todos = todoService.findAll();
          model.addAttribute("todos", todos);
          return "todo/list";
      }

      @PostMapping("create")
      public String create(@Validated({Default.class, // (1)
              TodoCreate.class}) TodoForm todoForm, BindingResult bindingResult, Model model,
              RedirectAttributes attributes) {

          if (bindingResult.hasErrors()) {
              return list(model);
          }

          Todo todo = beanMapper.map(todoForm);

          try {
              todoService.create(todo);
          } catch (BusinessException e) {
              model.addAttribute(e.getResultMessages());
              return list(model);
          }

          attributes.addFlashAttribute(
                  ResultMessages.success().add(ResultMessage.fromText("Created successfully!")));
          return "redirect:/todo/list";
      }

      @PostMapping("finish") // (2)
      public String finish(@Validated({Default.class, // (3)
              TodoFinish.class}) TodoForm form, BindingResult bindingResult, Model model,
              RedirectAttributes attributes) {
          // (4)
          if (bindingResult.hasErrors()) {
              return list(model);
          }

          try {
              todoService.finish(form.getTodoId());
          } catch (BusinessException e) {
              // (5)
              model.addAttribute(e.getResultMessages());
              return list(model);
          }

          // (6)
          attributes.addFlashAttribute(
                  ResultMessages.success().add(ResultMessage.fromText("Finished successfully!")));
          return "redirect:/todo/list";
      }
  }

.. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}|
.. list-table::
  :header-rows: 1
  :widths: 10 90

  * - 項番
    - 説明
  * - | (1)
    - | グループ化した入力チェックルールを適用するために、\ ``@Valid``\ アノテーションを\ ``@Validated``\ アノテーションに変更する。
      | \ ``value``\ 属性には、適用する入力チェックルールのグループ(グループインタフェース)を指定する。
      | \ ``Default.class``\ は、グループ化されていない入力チェックルールを適用するために用意されているグループインタフェースである。
  * - | (2)
    - | \ ``/todo/finish``\ というパスに\ ``POST``\ メソッドを使用してリクエストされた際に、完了処理用のメソッド(\ ``finish``\ メソッド)が実行されるように\ ``@PostMapping``\ アノテーションを設定する。
  * - | (3)
    - | 適用する入力チェックのグループとして、完了処理用のグループインタフェース(\ ``TodoFinish``\ インタフェース)を指定する。
  * - | (4)
    - | 入力エラーがあった場合、一覧画面に戻る。
  * - | (5)
    - | 業務処理を実行して、\ ``BusinessException``\ が発生した場合は、結果メッセージを\ ``Model``\ に追加して、一覧画面に戻る。
  * - | (6)
    - | 正常に作成が完了した場合は、結果メッセージをflashスコープに追加して、一覧画面でリダイレクトする。

.. note::

  新規作成処理用と完了処理用を別々のFormクラスとして作成しても良い。別々のFormクラスにした場合、入力チェックルールをグループ化する必要がないため、入力チェックルールの定義はシンプルになる。

  ただし、処理毎にFormクラスを作成した場合、

  * クラス数が増える
  * プロパティが重複するため入力チェックルールを一元管理できない

  ため、仕様変更が発生した場合に修正コストが高くなる可能性があるという点に注意してほしい。

  また、\ ``@ModelAttribute``\ メソッドを使用して複数のFormを初期化した場合、毎回すべてのFormが初期化されるため、不要なインスタンスが生成されることになる。

|

JSPの修正
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""

完了処理用のformを追加する。

.. code-block:: jsp

  <!DOCTYPE html>
  <html>
      <head>
          <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
          <title>Todo List</title>
      </head>
      <style type="text/css">
          .strike {
              text-decoration: line-through;
          }

          .inline {
              display: inline-block;
          }

          .alert {
              border: 1px solid;
          }

          .alert-error {
              background-color: #c60f13;
              border-color: #970b0e;
              color: white;
          }

          .alert-success {
              background-color: #5da423;
              border-color: #457a1a;
              color: white;
          }

          .text-error {
              color: #c60f13;
          }
      </style>
      <body>
          <h1>Todo List</h1>

          <div id="todoForm">
              <t:messagesPanel />

              <form:form action="${pageContext.request.contextPath}/todo/create" method="post" modelAttribute="todoForm">
                  <form:input path="todoTitle" />
                  <form:errors path="todoTitle" cssClass="text-error" />
                  <form:button>Create Todo</form:button>
              </form:form>
          </div>
          <hr />
          <div id="todoList">
              <ul>
                  <c:forEach items="${todos}" var="todo">
                      <li>
                          <c:choose>
                              <c:when test="${todo.finished}">
                                  <span class="strike">${f:h(todo.todoTitle)}</span>
                              </c:when>
                              <c:otherwise>
                                  ${f:h(todo.todoTitle)}
                                  <!-- (1) -->
                                  <form:form action="${pageContext.request.contextPath}/todo/finish" method="post" modelAttribute="todoForm" cssClass="inline">
                                      <!-- (2) -->
                                      <form:hidden path="todoId" value="${f:h(todo.todoId)}" />
                                      <form:button>Finish</form:button>
                                  </form:form>
                              </c:otherwise>
                          </c:choose>
                      </li>
                  </c:forEach>
              </ul>
          </div>
      </body>
  </html>

.. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}|
.. list-table::
  :header-rows: 1
  :widths: 10 90

  * - 項番
    - 説明
  * - | (1)
    - | TODOが未完了の場合は、TODOを完了させるためのリクエストを送信するformを表示する。
      | \ ``action``\ 属性には完了処理を実行するためのURL(\ ``<contextPath>/todo/finish``\ )を指定する。
      | 完了処理は更新系の処理なので、\ ``method``\ 属性には\ ``POST``\ メソッドを指定する。
      | なお、「Finish」ボタンをインラインブロック要素（\ ``display: inline-block;``\ ）としてTODOの横に表示させている。
  * - | (2)
    - | \ ``<form:hidden>``\ タグを使用して、リクエストパラメータとして\ ``todoId``\ を送信する。
      | \ ``value``\ 属性に値を設定する場合も、\ **必ずf:h()関数でHTMLエスケープすること。**\

|

Todoを新規作成した後に、「Finish」ボタン押下すると、以下のように打ち消し線が入り、完了したことがわかる。

.. figure:: ./images_TutorialTodo/image075.png
  :width: 40%

.. figure:: ./images_TutorialTodo/image076.png
  :width: 40%

|

Delete TODOの実装
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

一覧表示画面に「Delete」ボタンを追加して、TODOを削除するための処理を追加する。

|

Formの修正
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""

削除処理用のFormについても、\ ``TodoForm``\ を使用する。

.. code-block:: java

  package com.example.todo.app.todo;

  import java.io.Serializable;
  import jakarta.validation.constraints.NotEmpty;
  import jakarta.validation.constraints.NotNull;
  import jakarta.validation.constraints.Size;

  public class TodoForm implements Serializable {
      public static interface TodoCreate {
      };

      public static interface TodoFinish {
      };

      // (1)
      public static interface TodoDelete {
      }

      private static final long serialVersionUID = 1L;

      // (2)
      @NotEmpty(groups = {TodoFinish.class, TodoDelete.class})
      private String todoId;

      @NotNull(groups = {TodoCreate.class})
      @Size(min = 1, max = 30, groups = {TodoCreate.class})
      private String todoTitle;

      public String getTodoId() {
          return todoId;
      }

      public void setTodoId(String todoId) {
          this.todoId = todoId;
      }

      public String getTodoTitle() {
          return todoTitle;
      }

      public void setTodoTitle(String todoTitle) {
          this.todoTitle = todoTitle;
      }

  }

.. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}|
.. list-table::
  :header-rows: 1
  :widths: 10 90

  * - 項番
    - 説明
  * - | (1)
    - | 削除処理用の入力チェックルールをグループ化するためのインタフェースとして\ ``TodoDelete``\ を作成する。
  * - | (2)
    - | 削除処理では\ ``todoId``\ プロパティを使用する。
      | そのため、\ ``todoId``\ の\ ``@NotEmpty``\ アノテーションの\ ``groups``\ 属性には、削除処理用の入力チェックルールである事を示す\ ``TodoDelete``\ インタフェースを指定する。

|

Controllerの修正
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""

削除処理を\ ``TodoController``\ に追加する。完了処理とほぼ同じである。

.. code-block:: java

  package com.example.todo.app.todo;

  import java.util.Collection;
  import org.springframework.stereotype.Controller;
  import org.springframework.ui.Model;
  import org.springframework.validation.BindingResult;
  import org.springframework.validation.annotation.Validated;
  import org.springframework.web.bind.annotation.GetMapping;
  import org.springframework.web.bind.annotation.ModelAttribute;
  import org.springframework.web.bind.annotation.PostMapping;
  import org.springframework.web.bind.annotation.RequestMapping;
  import org.springframework.web.servlet.mvc.support.RedirectAttributes;
  import org.terasoluna.gfw.common.exception.BusinessException;
  import org.terasoluna.gfw.common.message.ResultMessage;
  import org.terasoluna.gfw.common.message.ResultMessages;
  import com.example.todo.app.todo.TodoForm.TodoCreate;
  import com.example.todo.app.todo.TodoForm.TodoDelete;
  import com.example.todo.app.todo.TodoForm.TodoFinish;
  import com.example.todo.domain.model.Todo;
  import com.example.todo.domain.service.todo.TodoService;
  import jakarta.inject.Inject;
  import jakarta.validation.groups.Default;

  @Controller
  @RequestMapping("todo")
  public class TodoController {

      @Inject
      TodoService todoService;

      @Inject
      TodoMapper beanMapper;

      @ModelAttribute
      public TodoForm setUpForm() {
          TodoForm form = new TodoForm();
          return form;
      }

      @GetMapping("list")
      public String list(Model model) {
          Collection<Todo> todos = todoService.findAll();
          model.addAttribute("todos", todos);
          return "todo/list";
      }

      @PostMapping("create")
      public String create(@Validated({Default.class, TodoCreate.class}) TodoForm todoForm,
              BindingResult bindingResult, Model model, RedirectAttributes attributes) {

          if (bindingResult.hasErrors()) {
              return list(model);
          }

          Todo todo = beanMapper.map(todoForm);

          try {
              todoService.create(todo);
          } catch (BusinessException e) {
              model.addAttribute(e.getResultMessages());
              return list(model);
          }

          attributes.addFlashAttribute(
                  ResultMessages.success().add(ResultMessage.fromText("Created successfully!")));
          return "redirect:/todo/list";
      }

      @PostMapping("finish")
      public String finish(@Validated({Default.class, TodoFinish.class}) TodoForm form,
              BindingResult bindingResult, Model model, RedirectAttributes attributes) {
          if (bindingResult.hasErrors()) {
              return list(model);
          }

          try {
              todoService.finish(form.getTodoId());
          } catch (BusinessException e) {
              model.addAttribute(e.getResultMessages());
              return list(model);
          }

          attributes.addFlashAttribute(
                  ResultMessages.success().add(ResultMessage.fromText("Finished successfully!")));
          return "redirect:/todo/list";
      }

      @PostMapping("delete") // (1)
      public String delete(@Validated({Default.class, TodoDelete.class}) TodoForm form,
              BindingResult bindingResult, Model model, RedirectAttributes attributes) {

          if (bindingResult.hasErrors()) {
              return list(model);
          }

          try {
              todoService.delete(form.getTodoId());
          } catch (BusinessException e) {
              model.addAttribute(e.getResultMessages());
              return list(model);
          }

          attributes.addFlashAttribute(
                  ResultMessages.success().add(ResultMessage.fromText("Deleted successfully!")));
          return "redirect:/todo/list";
      }

  }

.. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}|
.. list-table::
  :header-rows: 1
  :widths: 10 90


  * - 項番
    - 説明
  * - | (1)
    - \ ``/todo/delete``\ というパスに\ ``POST``\ メソッドを使用してリクエストされた際に、削除処理用のメソッド(\ ``delete``\ メソッド)が実行されるように\ ``@PostMapping``\ アノテーションを設定する。

|

JSPの修正
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
削除処理用のformを追加する。

.. code-block:: jsp

  <!DOCTYPE html>
  <html>
      <head>
          <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
          <title>Todo List</title>
      </head>
      <style type="text/css">
          .strike {
              text-decoration: line-through;
          }

          .inline {
              display: inline-block;
          }

          .alert {
              border: 1px solid;
          }

          .alert-error {
              background-color: #c60f13;
              border-color: #970b0e;
              color: white;
          }

          .alert-success {
              background-color: #5da423;
              border-color: #457a1a;
              color: white;
          }

          .text-error {
              color: #c60f13;
          }
      </style>
      <body>
          <h1>Todo List</h1>

          <div id="todoForm">
              <t:messagesPanel />

              <form:form action="${pageContext.request.contextPath}/todo/create" method="post" modelAttribute="todoForm">
                  <form:input path="todoTitle" />
                  <form:errors path="todoTitle" cssClass="text-error" />
                  <form:button>Create Todo</form:button>
              </form:form>
          </div>
          <hr />
          <div id="todoList">
              <ul>
                  <c:forEach items="${todos}" var="todo">
                      <li>
                          <c:choose>
                              <c:when test="${todo.finished}">
                                  <span class="strike">${f:h(todo.todoTitle)}</span>
                              </c:when>
                              <c:otherwise>
                                  ${f:h(todo.todoTitle)}
                                  <form:form action="${pageContext.request.contextPath}/todo/finish" method="post" modelAttribute="todoForm" cssClass="inline">
                                      <form:hidden path="todoId" value="${f:h(todo.todoId)}" />
                                      <form:button>Finish</form:button>
                                  </form:form>
                              </c:otherwise>
                          </c:choose>
                          <!-- (1) -->
                          <form:form action="${pageContext.request.contextPath}/todo/delete" method="post" modelAttribute="todoForm" cssClass="inline">
                              <!-- (2) -->
                              <form:hidden path="todoId" value="${f:h(todo.todoId)}" />
                              <form:button>Delete</form:button>
                          </form:form>
                      </li>
                  </c:forEach>
              </ul>
          </div>
      </body>
  </html>

.. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}|
.. list-table::
  :header-rows: 1
  :widths: 10 90

  * - 項番
    - 説明
  * - | (1)
    - | 削除処理用のformを表示する。
      | \ ``action``\ 属性には削除処理を実行するためのURL(\ ``<contextPath>/todo/delete``\ )を指定する。
      | 削除処理は更新系の処理なので、\ ``method``\ 属性には\ ``POST``\ メソッドを指定する。
  * - | (2)
    - | \ ``<form:hidden>``\ タグを使用して、リクエストパラメータとして\ ``todoId``\ を送信する。
      | \ ``value``\ 属性に値を設定する場合も、\ **必ずf:h()関数でHTMLエスケープすること。**\

|

未完了状態のTODOの「Delete」ボタンを押下すると、以下のようにTODOが削除される。

.. figure:: ./images_TutorialTodo/image077.png
  :width: 40%

.. figure:: ./images_TutorialTodo/image078.png
  :width: 40%

|

CSSファイルの使用
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

これまでスタイルシートをJSPファイルの中で直接定義していたが、
実際のアプリケーションを開発する場合は、CSSファイルに定義するのが一般的である。

ここでは、スタイルシートをCSSファイルに定義する方法について説明する。

ブランクプロジェクトから提供しているCSSファイル(\ ``src/main/webapp/resources/app/css/styles.css``\ )にスタイルシートの定義を追加する。

.. code-block:: css

  /* ... */

  .strike {
      text-decoration: line-through;
  }

  .inline {
      display: inline-block;
  }

  .alert {
      border: 1px solid;
      margin-bottom: 5px;
  }

  .alert-error {
      background-color: #c60f13;
      border-color: #970b0e;
      color: white;
  }

  .alert-success {
      background-color: #5da423;
      border-color: #457a1a;
      color: white;
  }

  .text-error {
      color: #c60f13;
  }

  .alert ul {
      margin: 15px 0px 15px 0px;
  }

  #todoList li {
      margin-top: 5px;
  }

|

JSPからCSSファイルを読み込む。

.. code-block:: jsp

  <!DOCTYPE html>
  <html>
      <head>
          <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
          <title>Todo List</title>
          <!-- (1) -->
          <link rel="stylesheet" href="${pageContext.request.contextPath}/resources/app/css/styles.css" type="text/css" />
      </head>
      <body>
          <h1>Todo List</h1>

          <div id="todoForm">
              <t:messagesPanel />

              <form:form action="${pageContext.request.contextPath}/todo/create" method="post" modelAttribute="todoForm">
                  <form:input path="todoTitle" />
                  <form:errors path="todoTitle" cssClass="text-error" />
                  <form:button>Create Todo</form:button>
              </form:form>
          </div>
          <hr />
          <div id="todoList">
              <ul>
                  <c:forEach items="${todos}" var="todo">
                      <li>
                          <c:choose>
                              <c:when test="${todo.finished}">
                                  <span class="strike">${f:h(todo.todoTitle)}</span>
                              </c:when>
                              <c:otherwise>
                                  ${f:h(todo.todoTitle)}
                                  <form:form action="${pageContext.request.contextPath}/todo/finish" method="post" modelAttribute="todoForm" cssClass="inline">
                                      <form:hidden path="todoId" value="${f:h(todo.todoId)}" />
                                      <form:button>Finish</form:button>
                                  </form:form>
                              </c:otherwise>
                          </c:choose>
                          <form:form action="${pageContext.request.contextPath}/todo/delete" method="post" modelAttribute="todoForm" cssClass="inline">
                              <form:hidden path="todoId" value="${f:h(todo.todoId)}" />
                              <form:button>Delete</form:button>
                          </form:form>
                      </li>
                  </c:forEach>
              </ul>
          </div>
      </body>
  </html>

.. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}|
.. list-table::
  :header-rows: 1
  :widths: 10 90

  * - 項番
    - 説明
  * - | (1)
    - | JSPファイルからスタイルシートの定義を削除し、代わりにスタイルシートを定義したCSSファイルを読み込む。

|

CSSファイルを適用すると、以下のようなレイアウトになる。

.. figure:: ./images_TutorialTodo/list-screen-css.png
  :width: 40%

|

.. _tutorial-todo_infra:

データベースアクセスを伴うインフラストラクチャ層の作成
================================================================================

ここでは、Domainオブジェクトをデータベースに永続化するためのインフラストラクチャ層の実装方法について説明する。

本チュートリアルでは、以下の2つのO/R Mapperを使用したインフラストラクチャ層の実装方法について説明する。

* MyBatis3
* Spring Data JPA

|

.. _TutorialCreateORMapperBlankProject:

O/R Mapperに依存したブランクプロジェクトの作成
--------------------------------------------------------------------------------

ここでは、O/R Mapperに依存したブランクプロジェクトの作成を行う。

まず、使用するO/R Mapperに応じてプロジェクトを作成し直す。

\ :ref:`BlankProjectCreateBlankProject`\

次に、\ :ref:`tutorial-todo_infra`\ までで作成した\ :file:`src`\ フォルダ以下のうち、\ **TodoRepositoryImplクラス以外のファイルを新規作成したプロジェクトにコピーする**\ 。

\ **ただし、コピーするファイルは新規作成したファイル・変更を加えたファイルに限り、修正を加えていないファイルはコピーしないこと**\ 。

|

.. _Tutorial_Setup_Database:

データベースのセットアップ
--------------------------------------------------------------------------------

ここでは、データベースのセットアップを行う。

本チュートリアルでは、データベースのセットアップの手間を省くため、H2 Databaseを使用する。

|

todo-infra.propertiesの修正
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

APサーバ起動時にH2 Database上にテーブルが作成されるようにするために、\ :file:`src/main/resources/META-INF/spring/todo-infra.properties`\ の設定を変更する。

.. code-block:: properties

  database=H2
  # (1)
  database.url=jdbc:h2:mem:todo;DB_CLOSE_DELAY=-1;INIT=create table if not exists todo(todo_id varchar(36) primary key, todo_title varchar(30), finished boolean, created_at timestamp)
  database.username=sa
  database.password=
  database.driverClassName=org.h2.Driver
  # connection pool
  cp.maxActive=96
  cp.maxIdle=16
  cp.minIdle=0
  cp.maxWait=60000

.. tabularcolumns:: |p{0.10\linewidth}|p{0.80\linewidth}|
.. list-table::
  :header-rows: 1
  :widths: 10 80

  * - 項番
    - 説明
  * - | (1)
    - | 接続URLのINITパラメータに、テーブルを作成するDDL文を指定する。

.. note::

  INITパラメータに設定しているDDL文をフォーマットすると、以下の様なSQLとなる。

    .. code-block:: sql

      create table if not exists todo (
          todo_id varchar(36) primary key,
          todo_title varchar(30),
          finished boolean,
          created_at timestamp
      )

|

.. _using_MyBatis3:

MyBatis3を使用したインフラストラクチャ層の作成
--------------------------------------------------------------------------------

ここでは、MyBatis3を使用してインフラストラクチャ層のRepositoryImplを作成する方法について説明する。

Spring Data JPAを使用する場合は、本節を読み飛ばして、\ :ref:`using_SpringDataJPA`\ に進んでよい。

|

TodoRepositoryの作成
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

| \ ``TodoRepository``\ は、O/R Mapperを使用しない場合と同じ方法で作成する。
| 作成方法は、「\ :ref:`TutorialTodoCreateRepository`\ 」を参照されたい。
|

TodoRepositoryImplの作成
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

| MyBatis3を使用する場合、RepositoryImplはRepositoryインタフェース(Mapperインタフェース)から自動生成される。
| そのため、\ ``TodoRepositoryImpl``\ の作成は不要である。作成した場合は削除すること。
|

Mapperファイルの作成
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

\ ``TodoRepository``\ インタフェースのメソッドが呼び出された際に実行するSQLを定義するためのMapperファイルを作成する。

Package Explorer上で右クリック -> New -> File を選択し、「Create New File」ダイアログを表示し、

  .. tabularcolumns:: |p{0.10\linewidth}|p{0.30\linewidth}|p{0.50\linewidth}|
  .. list-table::
    :header-rows: 1
    :widths: 10 30 50

    * - 項番
      - 項目
      - 入力値
    * - 1
      - Enter or select the parent folder
      - \ ``todo/src/main/resources/com/example/todo/domain/repository/todo``\
    * - 2
      - File name
      - \ ``TodoRepository.xml``\

を入力して「Finish」する。

作成したファイルは以下のディレクトリに格納される。

.. figure:: ./images_TutorialTodo/create-mapper-for-mybatis3.png

\ ``TodoRepository``\ インタフェースに定義したメソッドが呼び出された際に実行するSQLを記述する。

.. code-block:: xml

  <?xml version="1.0" encoding="UTF-8"?>
  <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

  <!-- (1) -->
  <mapper namespace="com.example.todo.domain.repository.todo.TodoRepository">

      <!-- (2) -->
      <resultMap id="todoResultMap" type="Todo">
          <id property="todoId" column="todo_id" />
          <result property="todoTitle" column="todo_title" />
          <result property="finished" column="finished" />
          <result property="createdAt" column="created_at" />
      </resultMap>

      <!-- (3) -->
      <select id="findById" parameterType="String" resultMap="todoResultMap">
        <![CDATA[
            SELECT
                todo_id,
                todo_title,
                finished,
                created_at
            FROM
                todo
            WHERE
                todo_id = #{todoId}
        ]]>
      </select>

      <!-- (4) -->
      <select id="findAll" resultMap="todoResultMap">
        <![CDATA[
            SELECT
                todo_id,
                todo_title,
                finished,
                created_at
            FROM
                todo
        ]]>
      </select>

      <!-- (5) -->
      <insert id="create" parameterType="Todo">
        <![CDATA[
            INSERT INTO todo
            (
                todo_id,
                todo_title,
                finished,
                created_at
            )
            VALUES
            (
                #{todoId},
                #{todoTitle},
                #{finished},
                #{createdAt}
            )
        ]]>
      </insert>

      <!-- (6) -->
      <update id="update" parameterType="Todo">
        <![CDATA[
            UPDATE todo
            SET
                todo_title = #{todoTitle},
                finished = #{finished},
                created_at = #{createdAt}
            WHERE
                todo_id = #{todoId}
        ]]>
      </update>

      <!-- (7) -->
      <delete id="delete" parameterType="Todo">
        <![CDATA[
            DELETE FROM
                todo
            WHERE
                todo_id = #{todoId}
        ]]>
      </delete>

      <!-- (8) -->
      <select id="countByFinished" parameterType="Boolean"
          resultType="Long">
        <![CDATA[
            SELECT
                COUNT(*)
            FROM
                todo
            WHERE
                finished = #{finished}
        ]]>
      </select>

  </mapper>

.. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}|
.. list-table::
  :header-rows: 1
  :widths: 10 90

  * - 項番
    - 説明
  * - | (1)
    - | \ ``mapper``\ 要素の\ ``namespace``\ 属性に、Repositoryインタフェースの完全修飾クラス名(FQCN)を指定する。
  * - | (2)
    - | \ ``<resultMap>``\ 要素に、検索結果(\ ``ResultSet``\ )とJavaBeanのマッピング定義を行う。
      | マッピングファイルの詳細は\ :doc:`../ArchitectureInDetail/DataAccessDetail/DataAccessMyBatis3`\ を参照されたい。
  * - | (3)
    - | \ ``todoId``\ (PK)が一致するレコードを1件取得するSQLを実装する。
      | \ ``<select>``\ 要素の\ ``resultMap``\ 属性には、適用するマッピング定義のIDを指定する。
  * - | (4)
    - | 全レコードを取得するSQLを実装している。
      | \ ``<select>``\ 要素の\ ``resultMap``\ 属性に、適用するマッピング定義のIDを指定する。
      | アプリケーションの要件には記載がないが、最新のTODOが先頭に表示されるようにレコードを並び替えている。
  * - | (5)
    - | 引数に指定されたTodoオブジェクトを挿入するSQLを実装する。
      | \ ``<insert>``\ 要素の\ ``parameterType``\ 属性に、パラメータのクラス名(FQCN又はエイリアス名)を指定する。
  * - | (6)
    - | 引数に指定されたTodoオブジェクトを更新するSQLを実装する。
      | \ ``<update>``\ 要素の\ ``parameterType``\ 属性に、パラメータのクラス名(FQCN又はエイリアス名)を指定する。
  * - | (7)
    - | 引数に指定されたTodoオブジェクトを削除するSQLを実装する。
      | \ ``<delete>``\ 要素の\ ``parameterType``\ 属性に、パラメータのクラス名(FQCN又はエイリアス名)を指定する。
  * - | (8)
    - | 引数に指定された\ ``finished``\ に一致するTodoの件数を取得するSQLを実装する。

|

以上で、MyBatis3を使用したインフラストラクチャ層の作成が完了となる。

APサーバーを起動し、Todoの表示を行うと、以下のようなSQLログやトランザクションログが出力される。

.. code-block:: console

  date:2022-11-29 17:51:09	thread:http-nio-8080-exec-3	X-Track:9eef87761a264278a38d4c2e14a99959	level:TRACE	logger:o.t.gfw.web.logging.TraceLoggingInterceptor     	message:[START CONTROLLER] TodoController.list(Model)
  date:2022-11-29 17:51:09	thread:http-nio-8080-exec-3	X-Track:9eef87761a264278a38d4c2e14a99959	level:DEBUG	logger:o.s.jdbc.datasource.DataSourceTransactionManager	message:Creating new transaction with name [com.example.todo.domain.service.todo.TodoServiceImpl.findAll]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT,readOnly
  date:2022-11-29 17:51:09	thread:http-nio-8080-exec-3	X-Track:9eef87761a264278a38d4c2e14a99959	level:DEBUG	logger:o.s.jdbc.datasource.DataSourceTransactionManager	message:Acquired Connection [2013925503, URL=jdbc:h2:mem:todo-jsp-mybatis3, H2 JDBC Driver] for JDBC transaction
  date:2022-11-29 17:51:09	thread:http-nio-8080-exec-3	X-Track:9eef87761a264278a38d4c2e14a99959	level:DEBUG	logger:c.e.t.d.repository.todo.TodoRepository.findAll  	message:==>  Preparing: SELECT todo_id, todo_title, finished, created_at FROM todo
  date:2022-11-29 17:51:09	thread:http-nio-8080-exec-3	X-Track:9eef87761a264278a38d4c2e14a99959	level:DEBUG	logger:c.e.t.d.repository.todo.TodoRepository.findAll  	message:==> Parameters: 
  date:2022-11-29 17:51:09	thread:http-nio-8080-exec-3	X-Track:9eef87761a264278a38d4c2e14a99959	level:DEBUG	logger:c.e.t.d.repository.todo.TodoRepository.findAll  	message:<==      Total: 0
  date:2022-11-29 17:51:09	thread:http-nio-8080-exec-3	X-Track:9eef87761a264278a38d4c2e14a99959	level:DEBUG	logger:o.s.jdbc.datasource.DataSourceTransactionManager	message:Initiating transaction commit
  date:2022-11-29 17:51:09	thread:http-nio-8080-exec-3	X-Track:9eef87761a264278a38d4c2e14a99959	level:DEBUG	logger:o.s.jdbc.datasource.DataSourceTransactionManager	message:Committing JDBC transaction on Connection [2013925503, URL=jdbc:h2:mem:todo-jsp-mybatis3, H2 JDBC Driver]
  date:2022-11-29 17:51:09	thread:http-nio-8080-exec-3	X-Track:9eef87761a264278a38d4c2e14a99959	level:DEBUG	logger:o.s.jdbc.datasource.DataSourceTransactionManager	message:Releasing JDBC Connection [2013925503, URL=jdbc:h2:mem:todo-jsp-mybatis3, H2 JDBC Driver] after transaction
  date:2022-11-29 17:51:10	thread:http-nio-8080-exec-3	X-Track:9eef87761a264278a38d4c2e14a99959	level:TRACE	logger:o.t.gfw.web.logging.TraceLoggingInterceptor     	message:[END CONTROLLER  ] TodoController.list(Model)-> view=todo/list, model={todoForm=com.example.todo.app.todo.TodoForm@78becf2c, todos=[], org.springframework.validation.BindingResult.todoForm=org.springframework.validation.BeanPropertyBindingResult: 0 errors}
  date:2022-11-29 17:51:10	thread:http-nio-8080-exec-3	X-Track:9eef87761a264278a38d4c2e14a99959	level:TRACE	logger:o.t.gfw.web.logging.TraceLoggingInterceptor     	message:[HANDLING TIME   ] TodoController.list(Model)-> 165,863,500 ns

|

.. _using_SpringDataJPA:

Spring Data JPAを使用したインフラストラクチャ層の作成
--------------------------------------------------------------------------------

ここでは、\ :url_spring_io:`Spring Data JPA </projects/spring-data-jpa>`\ を使用してインフラストラクチャ層のRepositoryImplを作成する方法について説明する。

|

Entityの修正
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

TodoクラスとデータベースのTODOテーブルをマッピングするために、JPAのアノテーションを設定する。

.. code-block:: java

  package com.example.todo.domain.model;

  import java.io.Serializable;
  import java.time.LocalDate;
  import jakarta.persistence.Column;
  import jakarta.persistence.Entity;
  import jakarta.persistence.Id;
  import jakarta.persistence.Table;

  // (1)
  @Entity
  @Table(name = "todo")
  public class Todo implements Serializable {
      private static final long serialVersionUID = 1L;

      // (2)
      @Id
      @Column(name = "todo_id")
      private String todoId;

      @Column(name = "todo_title")
      private String todoTitle;

      @Column(name = "finished")
      private boolean finished;

      @Column(name = "created_at")
      private LocalDate createdAt;

      public String getTodoId() {
          return todoId;
      }

      public void setTodoId(String todoId) {
          this.todoId = todoId;
      }

      public String getTodoTitle() {
          return todoTitle;
      }

      public void setTodoTitle(String todoTitle) {
          this.todoTitle = todoTitle;
      }

      public boolean isFinished() {
          return finished;
      }

      public void setFinished(boolean finished) {
          this.finished = finished;
      }

      public LocalDate getCreatedAt() {
          return createdAt;
      }

      public void setCreatedAt(LocalDate createdAt) {
          this.createdAt = createdAt;
      }
  }

.. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}|
.. list-table::
  :header-rows: 1
  :widths: 10 90


  * - 項番
    - 説明
  * - | (1)
    - | JPAのエンティティであることを示す\ ``@Entity``\ アノテーションを付け、対応するテーブル名を\ ``@Table``\ アノテーションで設定する。
  * - | (2)
    - | 主キーとなるカラムに対応するフィールドに、\ ``@Id``\ アノテーションをつける。

|

TodoRepositoryの作成
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Spring Data JPAのRepository機能を使用して\ ``TodoRepository``\ の作成を行う。

Package Explorer上で右クリック -> New -> Interface を選択し、「New Java Interface」ダイアログを表示し、

  .. tabularcolumns:: |p{0.10\linewidth}|p{0.30\linewidth}|p{0.50\linewidth}|
  .. list-table::
    :header-rows: 1
    :widths: 10 30 50

    * - 項番
      - 項目
      - 入力値
    * - 1
      - Package
      - \ ``com.example.todo.domain.repository.todo``\
    * - 2
      - Name
      - \ ``TodoRepository``\
    * - 3
      - Extended interfaces
      - \ ``org.springframework.data.jpa.repository.JpaRepository<T, ID>``\

を入力して「Finish」する。

.. code-block:: java

  package com.example.todo.domain.repository.todo;

  import org.springframework.data.jpa.repository.JpaRepository;
  import org.springframework.data.jpa.repository.Query;
  import org.springframework.data.repository.query.Param;
  import com.example.todo.domain.model.Todo;

  // (1)
  public interface TodoRepository extends JpaRepository<Todo, String> {

      @Query("SELECT COUNT(t) FROM Todo t WHERE t.finished = :finished") // (2)
      long countByFinished(@Param("finished") boolean finished); // (3)

  }

.. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}|
.. list-table::
  :header-rows: 1
  :widths: 10 90

  * - 項番
    - 説明
  * - | (1)
    - | \ ``JpaRepository``\ のGenericsのパラメータを指定する。
      | 左から順に、Entityのクラス(\ ``Todo`` \)、主キーのクラス(\ ``String``\ )を指定する。
      | 基本的なCRUD操作(\ ``findById``\ ,\ ``findAll``\ ,\ ``save``\ ,\ ``deleteById``\ など)は、\ ``JpaRepository``\ インタフェースに定義済みであるため、\ ``TodoRepository``\ には\ ``countByFinished``\ メソッドのみ定義すればよい。
  * - | (2)
    - | \ ``countByFinished``\ メソッドを呼び出した際に実行するJPQLを、\ ``@Query``\ アノテーションで指定する。
  * - | (3)
    - | (2)で指定したJPQL内のバインド変数に対応するメソッド引数に、\ ``@Param``\ アノテーションを指定する。
      | ここでは、JPQL中の\ ``”:finished”``\ に値を埋め込むために、メソッド引数の\ ``finished``\ に\ ``@Param(“finished”)``\ を付けている。

|

TodoRepositoryImplの作成
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Spring Data JPAを使用する場合、RepositoryImplはRepositoryインタフェースから自動生成される。
そのため、\ ``TodoRepositoryImpl``\ の作成は不要である。作成した場合は削除すること。

.. note:: 

  当ページをはじめから順に実施した場合、\ ``TodoRepositoryImpl``\ の削除が必要になるが、削除したタイミングで\ ``TodoServiceImpl``\ がコンパイルエラーとなる。

  JPA用のメソッドに切り替える必要があるため、ソースコード内のコメントを参考に\ ``create``\ メソッドと\ ``update``\ メソッドを\ ``save``\ メソッドへ変更する必要がある。

|

以上で、Spring Data JPAを使用したインフラストラクチャ層の作成が完了となる。

APサーバーを起動し、Todoの表示を行うと、以下のようなSQLログやトランザクションログが出力される。

.. code-block:: console

  date:2022-11-29 17:42:58	thread:http-nio-8080-exec-4	X-Track:67bbe0a8a3e8416baf91d5dcd73273e8	level:TRACE	logger:o.t.gfw.web.logging.TraceLoggingInterceptor     	message:[START CONTROLLER] TodoController.list(Model)
  date:2022-11-29 17:42:58	thread:http-nio-8080-exec-4	X-Track:67bbe0a8a3e8416baf91d5dcd73273e8	level:DEBUG	logger:o.h.engine.transaction.internal.TransactionImpl 	message:On TransactionImpl creation, JpaCompliance#isJpaTransactionComplianceEnabled == false
  date:2022-11-29 17:42:58	thread:http-nio-8080-exec-4	X-Track:67bbe0a8a3e8416baf91d5dcd73273e8	level:DEBUG	logger:o.h.engine.transaction.internal.TransactionImpl 	message:begin
  date:2022-11-29 17:42:58	thread:http-nio-8080-exec-4	X-Track:67bbe0a8a3e8416baf91d5dcd73273e8	level:DEBUG	logger:org.hibernate.SQL                               	message:/* <criteria> */ select t1_0.todo_id,t1_0.created_at,t1_0.finished,t1_0.todo_title from todo t1_0
  date:2022-11-29 17:42:58	thread:http-nio-8080-exec-4	X-Track:67bbe0a8a3e8416baf91d5dcd73273e8	level:DEBUG	logger:o.h.engine.transaction.internal.TransactionImpl 	message:committing
  date:2022-11-29 17:42:58	thread:http-nio-8080-exec-4	X-Track:67bbe0a8a3e8416baf91d5dcd73273e8	level:TRACE	logger:o.t.gfw.web.logging.TraceLoggingInterceptor     	message:[END CONTROLLER  ] TodoController.list(Model)-> view=todo/list, model={todoForm=com.example.todo.app.todo.TodoForm@5fcdd91a, todos=[], org.springframework.validation.BindingResult.todoForm=org.springframework.validation.BeanPropertyBindingResult: 0 errors}
  date:2022-11-29 17:42:58	thread:http-nio-8080-exec-4	X-Track:67bbe0a8a3e8416baf91d5dcd73273e8	level:TRACE	logger:o.t.gfw.web.logging.TraceLoggingInterceptor     	message:[HANDLING TIME   ] TodoController.list(Model)-> 247,911,500 ns

|

おわりに
================================================================================
このチュートリアルでは、以下の内容を学習した。


* \ |framework_name|\による基本的なアプリケーションの開発方法

* MavenおよびSTS(Eclipse)プロジェクトの構築方法

* \ |framework_name|\のアプリケーションのレイヤ化に従った開発方法

  * POJO(+ Spring)を使用したドメイン層の実装
  * POJO(+ Spring MVC)とJSPタグライブラリを使用したアプリケーション層の実装
  * MyBatis3を使用したインフラストラクチャ層の実装
  * Spring Data JPAを使用したインフラストラクチャ層の実装
  * O/R Mapperを使用しないインフラストラクチャ層の実装

本チュートリアルで作成したTODO管理アプリケーションには、以下の改善点がある。
アプリケーションの修正を学習課題として、ガイドライン中の該当する説明を参照されたい。

* プロパティ(未完了TODOの上限数)を外部化する → \ :doc:`../ArchitectureInDetail/GeneralFuncDetail/PropertyManagement`\
* メッセージを外部化する → \ :doc:`../ArchitectureInDetail/WebApplicationDetail/MessageManagement`\
* ページネーション機能を追加する → \ :doc:`../ArchitectureInDetail/WebApplicationDetail/Pagination`\
* 例外ハンドリングを加える → \ :doc:`../ArchitectureInDetail/WebApplicationDetail/ExceptionHandling`\
* 二重送信を防止する(トランザクショントークンチェックを追加する) → \ :doc:`../ArchitectureInDetail/WebApplicationDetail/DoubleSubmitProtection`\
* システム日時の取得元を変更する → \ :doc:`../ArchitectureInDetail/GeneralFuncDetail/SystemDate`\

|

.. raw:: latex

  \newpage
