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


11.1.1. はじめに

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

  • Macchinetta Server Framework (1.x)による基本的なアプリケーションの開発方法

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

  • Macchinetta Server Framework (1.x)のアプリケーションのレイヤ化に従った開発方法


11.1.1.2. 対象読者

  • SpringのDIやAOPに関する基礎的な知識がある

  • Servlet/JSPを使用してWebアプリケーションを開発したことがある

  • SQLに関する知識がある


11.1.1.3. 検証環境

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

種別

名前

OS

Windows 11

JVM

Java 17

IDE

Spring Tool Suite 5.0.0.RELEASE (以降「STS」と呼ぶ。設定方法はSTSの設定手順を参照されたい。)

Build Tool

Apache Maven 3.9.9 (以降「Maven」と呼ぶ)

Application Server

Apache Tomcat 10.1.50

Web Browser

Google Chrome 145


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

11.1.2.1. アプリケーションの概要

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

../_images/image001.png

11.1.2.2. アプリケーションの業務要件

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

ルールID

説明

B01

未完了のTODOは5件までしか登録できない

B02

完了済みのTODOは完了できない

Note

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


11.1.2.3. アプリケーションの処理仕様

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

../_images/image002.png

項番

プロセス名

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へリダイレクト


11.1.2.3.1. Show all TODO

  • TODOを全件表示する

  • 未完了のTODOに対しては「Finish」と「Delete」用のボタンが付く

  • 完了のTODOは打ち消し線で装飾する

  • TODOの件名のみ表示する


11.1.2.3.2. Create TODO

  • フォームから送信されたTODOを保存する

  • TODOの件名は1文字以上30文字以下であること

  • アプリケーションの業務要件のB01を満たさない場合はエラーコードE001でビジネス例外をスローする

  • 処理が成功した場合は、遷移先の画面で「Created successfully!」を表示する


11.1.2.3.3. Finish TODO

  • フォームから送信されたtodoIdに対応するTODOを完了済みにする

  • 該当するTODOが存在しない場合はエラーコードE404でリソース未検出例外をスローする

  • アプリケーションの業務要件のB02を満たさない場合はエラーコードE002でビジネス例外をスローする

  • 処理が成功した場合は、遷移先の画面で「Finished successfully!」を表示する


11.1.2.3.4. Delete TODO

  • フォームから送信されたtodoIdに対応するTODOを削除する

  • 該当するTODOが存在しない場合はエラーコードE404でリソース未検出例外をスローする

  • 処理が成功した場合は、遷移先の画面で「Deleted successfully!」を表示する


11.1.2.4. エラーメッセージ一覧

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

エラーコード

メッセージ

置換パラメータ

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


11.1.3. 環境構築

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

  • データベースを使用せずjava.util.Mapを使ったインメモリ実装のRepositoryImpl

  • MyBatis3を使用してデータベースにアクセスするRepositoryImpl

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

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


11.1.3.1. プロジェクトの作成

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

mvn archetype:generate -B^
 -DarchetypeGroupId=com.github.macchinetta.blank^
 -DarchetypeArtifactId=macchinetta-web-blank-jsp-archetype^
 -DarchetypeVersion=1.11.1.RELEASE^
 -DgroupId=com.example.todo^
 -DartifactId=todo^
 -Dversion=1.0.0-SNAPSHOT

プロジェクトの作成についての詳細は、「ブランクプロジェクトの作成」を参照されたい。


11.1.3.2. プロジェクトのインポート

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

プロジェクトのインポートについては、「ブランクプロジェクトのSTSへのインポート」を参照されたい。


11.1.3.3. プロジェクトの構成

本チュートリアルで作成するプロジェクトの構成については、「O/R Mapperに依存しないプロジェクトの場合」を参照されたい。

Note

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

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

マルチプロジェクトの作成方法は、「ブランクプロジェクトの作成」を参照されたい。


11.1.3.4. 設定ファイルの確認

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

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

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

Note

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


11.1.3.5. プロジェクトの動作確認

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

11.1.4. Todoアプリケーションの作成

Todoアプリケーションを作成する。作成する順は、以下の通りである。
  • ドメイン層(+ インフラストラクチャ層)

    • Domain Object作成

    • Repository作成

    • RepositoryImpl作成

    • Service作成

  • アプリケーション層

    • Controller作成

    • Form作成

    • View作成


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

ここでは、データベースを使用せずjava.util.Mapを使ったインメモリ実装のRepositoryImplを作成する方法について説明を行う。
データベースを使用する場合は、「データベースアクセスを伴うインフラストラクチャ層の作成」に記載されている内容で読み替えて、Todoアプリケーションを作成して頂きたい。

11.1.4.1. ドメイン層の作成

11.1.4.1.1. Domain Objectの作成

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

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

項番

項目

入力値

1

Package

com.example.todo.domain.model

2

Name

Todo

3

Interfaces

java.io.Serializable

を入力して「Finish」する。

../_images/image057.png

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

../_images/image058_JavaConfig.png

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

  • ID → todoId

  • タイトル → todoTitle

  • 完了フラグ → finished

  • 作成日 → createdAt

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…」を選択する。

../_images/image059.png

serialVersionUID以外を選択して「Generate」

../_images/image060.png

11.1.4.1.2. Repositoryの作成

TodoRepositoryインタフェースを作成する。
データベースを使用する場合は、「データベースアクセスを伴うインフラストラクチャ層の作成」に記載されている内容で読み替えて、Repositoryを作成する。

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

項番

項目

入力値

1

Package

com.example.todo.domain.repository.todo

2

Name

TodoRepository

を入力して「Finish」する。

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

../_images/image061_JavaConfig.png

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

  • TODOの1件取得 → findById

  • TODOの全件取得 → findAll

  • TODOの1件作成 → create

  • TODOの1件更新 → update

  • TODOの1件削除 → delete

  • 完了済みTODO件数の取得 → countByFinished

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

import java.util.Collection;

import com.example.todo.domain.model.Todo;

public interface TodoRepository {
    Todo findById(String todoId);

    Collection<Todo> findAll();

    void create(Todo todo);

    boolean update(Todo todo);

    void delete(Todo todo);

    long countByFinished(boolean finished);
}

Note

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

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


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

ここでは、説明を単純化するため、java.util.Mapを使ったインメモリ実装のRepositoryImplを作成する。
データベースを使用する場合は、「データベースアクセスを伴うインフラストラクチャ層の作成」に記載されている内容で読み替えて、RepositoryImplを作成する。

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

項番

項目

入力値

1

Package

com.example.todo.domain.repository.todo

2

Name

TodoRepositoryImpl

3

Interfaces

com.example.todo.domain.repository.todo.TodoRepository

を入力して「Finish」する。

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

../_images/image062_JavaConfig.png

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

Note

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

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 // (1)
public class TodoRepositoryImpl implements TodoRepository {
    private static final Map<String, Todo> TODO_MAP = new ConcurrentHashMap<>();

    @Override
    public Todo findById(String todoId) {
        return 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;
    }
}

項番

説明

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

Note

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

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

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


11.1.4.1.4. Serviceの作成

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

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

項番

項目

入力値

1

Package

com.example.todo.domain.service.todo

2

Name

TodoService

を入力して「Finish」する。

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

../_images/image063_JavaConfig.png

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

  • Todoの全件取得 → findAll

  • Todoの新規作成 → create

  • Todoの完了 → finish

  • Todoの削除 → delete

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」ダイアログを表示し、

項番

項目

入力値

1

Package

com.example.todo.domain.service.todo

2

Name

TodoServiceImpl

3

Interfaces

com.example.todo.domain.service.todo.TodoService

を入力して「Finish」する。

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

../_images/image064_JavaConfig.png
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);

        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);
        return todo;
    }

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

    // (8)
    private Todo findOne(String todoId) {
        Todo todo = todoRepository.findById(todoId);
        if (todo == null) {
            ResultMessages messages = ResultMessages.error();
            messages.add(ResultMessage
                    .fromText("[E404] The requested Todo is not found. (id=" + todoId + ")"));
            // (9)
            throw new ResourceNotFoundException(messages);
        }
        return todo;
    }
}

項番

説明

(1)
Serviceとしてcomponent-scanの対象とするため、クラスレベルに@Serviceアノテーションをつける。
(2)
クラスレベルに、@Transactionalアノテーションをつけることで、公開メソッドをすべてトランザクション管理する。
アノテーションを付与することで、メソッド開始時にトランザクションを開始、メソッド正常終了時にトランザクションのコミットが行われる。
また、途中で非検査例外が発生した場合は、トランザクションがロールバックされる。

データベースを使用しない場合は、@Transactionalアノテーションは不要である。
(3)
@Injectアノテーションで、TodoRepositoryの実装をインジェクションする。
(4)
参照のみ行う処理に関しては、readOnly=trueをつける。
O/R Mapperによっては、この設定により、参照時のトランザクション制御の最適化が行われる。

データベースを使用しない場合は、@Transactionalアノテーションは不要である。
(5)
結果メッセージを格納するクラスとして、共通ライブラリで用意されているorg.terasoluna.gfw.common.message.ResultMessageを用いる。
今回は、エラーメッセージを例外に追加する際に、ResultMessages.error()でメッセージ種別を指定して、ResultMessageを追加している。
(6)
業務エラーが発生した場合、共通ライブラリで用意されているorg.terasoluna.gfw.common.exception.BusinessExceptionをスローする。
(7)
一意性のある値を生成するために、UUIDを使用している。データベースのシーケンスを用いてもよい。
(8)
1件取得は、finishメソッドでもdeleteメソッドでも使用するため、メソッドとして用意しておく(interfaceに公開しても良い)。
(9)
取得したデータを返す。対象のデータが存在しない場合は共通ライブラリで用意されているorg.terasoluna.gfw.common.exception.ResourceNotFoundExceptionをスローする。

Note

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

プロパティファイルに外部化する方法は、プロパティ管理を参照されたい。


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

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


11.1.4.2.1. Controllerの作成

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

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

項番

項目

入力値

1

Package

com.example.todo.app.todo

2

Name

TodoController

を入力して「Finish」する。

Note

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

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

../_images/image065_JavaConfig.png
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 {

}

項番

説明

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

11.1.4.2.2. Show all TODOの実装

本チュートリアルで作成する画面では、

  • 新規作成フォームの表示

  • TODOの全件表示

を行う。

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


11.1.4.2.2.1. Formの作成

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

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

項番

項目

入力値

1

Package

com.example.todo.app.todo

2

Name

TodoForm

3

Interfaces

java.io.Serializable

を入力して「Finish」する。

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

../_images/image066_JavaConfig.png

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

  • タイトル → todoTitle

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;
    }

}

11.1.4.2.2.2. Controllerの実装

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

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)
    }
}

項番

説明

(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によって、WEB-INF/views/todo/list.jspがレンダリングされることになる。

Note

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

詳細は、リクエストとハンドラメソッドのマッピング方法を参照されたい。


11.1.4.2.2.3. JSPの作成

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

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

項番

項目

入力値

1

Enter or select the parent folder

todo/src/main/webapp/WEB-INF/views/todo

2

File name

list.jsp

を入力して「Finish」する。

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

../_images/create-list-jsp.png

まず、TODOの全件表示を行うために必要な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>

項番

説明

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

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

Note

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

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

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

  • TodoRepositoryImpl.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
    

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

../_images/show-all-todo-note.png

11.1.4.2.3. Create TODOの実装

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

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


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

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

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

項番

項目

入力値

1

Package

com.example.todo.app.todo

2

Name

TodoMapper

を入力して「Finish」する。

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

../_images/create-bean-mapper_JavaConfig.png

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

  • Todo map(TodoForm form)

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

      • createdAt

      • finished

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

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

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」をクリックする。

../_images/mvnBuild1.png

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


11.1.4.2.3.2. Controllerの修正

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

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";
    }

}

項番

説明

(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されることがなくなる。(詳しくは、「PRG(Post-Redirect-Get)パターンについて」を参照されたい)
なお、今回は成功メッセージであるため、ResultMessages.success()を使用している。

11.1.4.2.3.3. Formの修正

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

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;
    }
}

項番

説明

(1)
@NotNullアノテーションを使用して必須チェックを有効化する。
(2)
@Sizeアノテーションを使用して文字数チェックを有効化する。

11.1.4.2.3.4. JSPの修正

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

  • TODOの入力フォーム

  • 「Create Todo」ボタン

  • 入力チェックエラーを表示するエリア

  • 結果メッセージを表示するエリア

<!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>

項番

説明

(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すると、以下のように、成功メッセージが表示される。

../_images/image068.png
../_images/image069.png

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

../_images/image070.png

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

../_images/image071.png

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

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

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

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

.alert {
    border: 1px solid;
}

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

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

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

../_images/image072.png
../_images/image073.png

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

JSPを次のように修正し、

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

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

.text-error {
    color: #c60f13;
}

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

../_images/image074.png

11.1.4.2.4. Finish TODOの実装

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


11.1.4.2.4.1. Formの修正

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

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

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

  • ID → todoId

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;
    }

}

項番

説明

(1)
入力チェックルールをグループ化するためのインタフェースを作成する。
入力チェックルールのグループ化については、入力チェックを参照されたい。

ここでは、新規作成処理用のインタフェースとしてTodoCreateを、完了処理用のインタフェースとしてTodoFinishを作成している。
(2)
todoIdは完了処理で使用するプロパティである。
そのため、@NotEmptyアノテーションのgroups属性には、完了処理用の入力チェックルールである事を示すTodoFinishインタフェースを指定する。
(3)
todoTitleは新規作成処理で使用するプロパティである。
そのため、@NotNullアノテーションと@Sizeアノテーションのgroups属性には、新規作成処理用の入力チェックルールである事を示すTodoCreateインタフェースを指定する。

11.1.4.2.4.2. Controllerの修正

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

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

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";
    }
}

項番

説明

(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が初期化されるため、不要なインスタンスが生成されることになる。


11.1.4.2.4.3. JSPの修正

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

<!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>

項番

説明

(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」ボタン押下すると、以下のように打ち消し線が入り、完了したことがわかる。

../_images/image075.png
../_images/image076.png

11.1.4.2.5. Delete TODOの実装

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


11.1.4.2.5.1. Formの修正

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

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;
    }

}

項番

説明

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

11.1.4.2.5.2. Controllerの修正

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

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";
    }

}

項番

説明

(1)

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


11.1.4.2.5.3. JSPの修正

削除処理用のformを追加する。

<!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>

項番

説明

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

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

../_images/image077.png
../_images/image078.png

11.1.4.2.6. CSSファイルの使用

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

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

ブランクプロジェクトから提供しているCSSファイル(src/main/webapp/resources/app/css/styles.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ファイルを読み込む。

<!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>

項番

説明

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

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

../_images/list-screen-css.png

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

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

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


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

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

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

ブランクプロジェクトの作成

次に、データベースアクセスを伴うインフラストラクチャ層の作成までで作成したsrcフォルダ以下のうち、TodoRepositoryImplクラス以外のファイルを新規作成したプロジェクトにコピーする

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


11.1.5.2. データベースのセットアップ

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

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


11.1.5.2.1. todo-infra.propertiesの修正

APサーバ起動時にH2 Database上にテーブルが作成されるようにするために、src/main/resources/META-INF/spring/todo-infra.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

項番

説明

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

Note

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

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

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

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


11.1.5.3.1. TodoRepositoryの作成

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

11.1.5.3.2. TodoRepositoryImplの作成

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

11.1.5.3.3. Mapperファイルの作成

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

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

項番

項目

入力値

1

Enter or select the parent folder

todo/src/main/resources/com/example/todo/domain/repository/todo

2

File name

TodoRepository.xml

を入力して「Finish」する。

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

../_images/create-mapper-for-mybatis3.png

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

<?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>

項番

説明

(1)
mapper要素のnamespace属性に、Repositoryインタフェースの完全修飾クラス名(FQCN)を指定する。
(2)
<resultMap>要素に、検索結果(ResultSet)とJavaBeanのマッピング定義を行う。
マッピングファイルの詳細はデータベースアクセス(MyBatis3編)を参照されたい。
(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ログやトランザクションログが出力される。

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

11.1.6. おわりに

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

  • Macchinetta Server Framework (1.x)による基本的なアプリケーションの開発方法

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

  • Macchinetta Server Framework (1.x)のアプリケーションのレイヤ化に従った開発方法

    • POJO(+ Spring)を使用したドメイン層の実装

    • POJO(+ Spring MVC)とJSPタグライブラリを使用したアプリケーション層の実装

    • MyBatis3を使用したインフラストラクチャ層の実装

    • O/R Mapperを使用しないインフラストラクチャ層の実装

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