Spark Web Framework の使い方 : ページ（画面）アプリを作成する

[Spark Frameworkの使い方：Getting Start編](https://qiita.com/yoshikawaa/items/e6c0dff41eeb26dad1ce) はこちら
[Spark Web Framework の使い方 : RESTアプリを作成する](https://qiita.com/yoshikawaa/items/35e2a266594fa897654e) はこちら

今回はSpark Frameworkを使ったページ（画面）アプリの作成について解説します。

## 今回使用するライブラリ

* spark-core 2.7.1
* spark-template-thymeleaf 2.7.1
* spark-debug-tools 0.5

spark-template-thymeleafを依存関係に追加します。

```xml
<dependency>
    <groupId>com.sparkjava</groupId>
    <artifactId>spark-template-thymeleaf</artifactId>
    <version>2.7.1</version>
</dependency>
```

## Spark Frameworkにおけるページアプリ

RESTアプリを作成するでやったように、Spark FrameworkではHTTP Methodの`get`・`post`・`put`・`delete`に対応するマッピングメソッドが用意されています。このうち、ページアプリでは`get`・`post`を利用します。

```java
get("/", (req, res) -> { ... });
post("/", (req, res) -> { ... });
```

また、各マッピングメソッドには、レスポンスの内容によって異なる`Route`インターフェイスが用意されています。このうち、ページアプリでは`TemplateViewRoute`インターフェイスを利用します。


```java
get(String, TemplateViewRoute, TemplateEngine);
```

`TemplateViewRoute`インターフェイスのシグネチャを見てみましょう。

```java
@FunctionalInterface
public interface TemplateViewRoute {

    ModelAndView handle(Request request, Response response) throws Exception;

}
```

`TemplateViewRoute`インターフェイスは`Route`と同様に`@FunctionalInterface`で定義されており、リクエストとレスポンスを引数にとり、`ModelAndView`オブジェクトを返却するよう実装します。
`ModelAndView`は、テンプレートとなるView名と、テンプレートで使用する変数（Model）を格納するオブジェクトです。

### `TemplateEngine`

マッピングメソッドの第3引数となる`TemplateEngine`は、実際のテンプレートエンジンを呼び出すためのラッパークラスで、Sparkの付属ライブラリとして以下が提供されています。

* spark-template-closure
* spark-template-freemarker
* spark-template-handlebars
* spark-template-jade
* spark-template-jetbrick
* spark-template-jinjava
* spark-template-jtwig
* spark-template-mustache
* spark-template-mvel
* spark-template-pebble
* spark-template-rocker
* spark-template-thymeleaf
* spark-template-velocity
* spark-template-water

今回は、テンプレートエンジンとしてThymeleaf（spark-template-thymeleaf）を利用することにします。

## ページアプリを実装する

### 前提となる実装

今回は[RESTアプリを実装する](https://qiita.com/yoshikawaa/items/35e2a266594fa897654e#rest%E3%82%A2%E3%83%97%E3%83%AA%E3%82%92%E5%AE%9F%E8%A3%85%E3%81%99%E3%82%8B)で作成したTODOリストを管理するRESTアプリを、ページアプリに変換します。

大きな違いとして、前述の`TemplateViewRote`を利用するのに加え、RESTアプリとページアプリではURL（パス）の体系が異なります。

* RESTアプリ

```text
findAll : get    /todos
findOne : get    /todos/:id
create  : post   /todos
finish  : put    /todos/:id
remove  : delete /todos/:id
```

* ページアプリ

```text
findAll : get    /todos
findOne : -
create  : post   /todos/create
finish  : post   /todos/finish
remove  : post   /todos/remove
```

### 実装

それではページアプリを実装していきます。

まずはWebページのテンプレートファイルを作成します。
`spark-template-thymeleaf`の`ThymeleafTemplateEngine`のデフォルトでは、テンプレートファイルは`src/main/resources/templates/*.html`というファイル体系になります。

* todos.html

```html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="/css/styles.css"><!-- CSSファイルを読み込む -->
<title>Todo</title>
</head>
<body>
    <h1>Todo List</h1>
    <!-- TODOを新規登録するHTMLフォーム -->
    <form method="post" action="/todos/create">
        <input name="title">
        <button>create</button>
    </form>
    <hr>
    <!-- 登録されたTODOを表示するテーブル -->
    <table th:if="${not #lists.isEmpty(todos)}">
        <thead>
            <tr>
                <th>title</th>
                <th>created</th>
                <th></th>
                <th></th>
            </tr>
        </thead>
        <tbody>
            <tr th:each="todo : ${todos}" th:object="${todo}">
                <td th:text="*{title}"></td>
                <td th:text="*{#temporals.formatISO(created)}"></td> <!-- (1) -->
                <td th:switch="*{finished}">
                    <!-- 登録されたTODOを完了するHTMLフォーム -->
                    <span th:case="true">finished</span>
                    <form th:case="*" method="post" action="/todos/finish">
                        <button name="id" th:value="*{id}">finish</button>
                    </form>
                </td>
                <td>
                    <!-- 登録されたTODOを削除するHTMLフォーム -->
                    <form method="post" action="/todos/remove">
                        <button name="id" th:value="*{id}">remove</button>
                    </form>
                </td>
            </tr>
        </tbody>
    </table>
</body>
</html>
```

| no. | description |
|-----|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| (1) | `#temporals#formatISO`メソッドでJava 8 Date and Time APIの日付オブジェクトをフォーマットしています。`spark-template-thymeleaf`ではデフォルトで`thymeleaf-extras-java8time`をサポートしており、`#temporals`を利用することができます。 |

> ここでは`action`属性の指定方法としてリンクURL式（`@{}`）を利用していませんが、`spark-template-thymeleaf`ではリンクURL式をサポートしていないためです。Thymeleafの機能をすべてサポートしているわけではないので注意する必要がありそうです。

次にコントローラクラスを作成します。
前回と同様、ハンドラの処理が複雑化することを考慮して、ハンドラをApplication.javaから分離してコントローラクラスを作成します。

* TodoController.java

```java
public class TodoController {

    private static final TodoRepository repository = new TodoRepository();

    // (1)
    public static final TemplateViewRoute findAll = (req, res) -> {
        Map<String, Object> model = new HashMap<>(); // (2)
        model.put("todos", repository.findAll()); // (3)
        return new ModelAndView(model, "todos"); // (4)
    };

    // (5)
    public static final Route create = (req, res) -> {
        repository.create(req.queryParams("title"); // (6)
        res.redirect("/todos"); // (7)
        return null; // (8)
    };

    public static final Route finish = (req, res) -> {
        repository.finish(req.queryParams("id"));
        res.redirect("/todos");
        return null;
    };

    public static final Route remove = (req, res) -> {
        repository.remove(req.queryParams("id"));
        res.redirect("/todos");
        return null;
    };

}
```

| no. | description |
|-----|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| (1) | 画面を返却するハンドラは`TemplateViewRoute`インターフェイスを実装します。`@FunctionalInterface`なので分かりづらいかもしれませんが、匿名クラスのオブジェクトを生成してクラスフィールドに格納しています。 |
| (2) | 戻り値の`ModelAndView`オブジェクトに指定する`Map<String, Object>`オブジェクト（Model）を生成します。 |
| (3) | Modelに変数を追加します。ここでは"todos"という名前で、リポジトリから取得したTODOリストをセットしています。 |
| (4) | 戻り値として、前述のModelとView名（テンプレートファイルの名前）をセットした`ModelAndView`オブジェクトを返却します。テンプレートファイルの解決とHTMLの生成は前述のとおり`TemplateEngine`が実施します。 |
| (5) | create以降のパスでは、処理を行った後findAllのパスにリダイレクトします。リダイレクトするパスでは画面を返却しないため、`TemplateViewRoute`ではなく`Route`インターフェイスを実装します。 |
| (6) | `Request#queryParams`メソッドでHTMLフォームで入力された`title`を取得できます。 |
| (7) | `Response#redirect`メソッドでリダイレクトすることができます。<br>なお、`Response`を取れない状況なら`Spark.redirect`メソッドを利用してリダイレクトすることも可能です。 |
| (8) | リダイレクトしてもメソッドは終了しないので、`null`を返却します。ちなみに`TemplateViewRoute`を実装した場合は、無駄に`ModelAndView`オブジェクトを生成して返却する必要があります。 |

> リダイレクト後のロジックをスキップする手段として、リクエストの処理を中断する`Spark.halt`メソッドがありますが、`Spark.after`メソッドで適用したフィルタもスキップしてしまうので、利用には注意が必要そうです。

これで必要なピースは揃ったので、あとはリクエストマッピングの設定を行うだけです。

* Application.java

```java
public class Application {

    private static final TemplateEngine templateEngine = new ThymeleafTemplateEngine();

    public static void main(String[] args) {
        // configure application.
        staticFiles.location("/public"); // (1)
        staticFiles.expireTime(600L);
        enableDebugScreen();

        // configure routes.
        path("/todos", () -> { // (2)
            // (3)
            get("", TodoController.findAll, templateEngine);
            // (4)
            post("/create", TodoController.create);
            post("/finish", TodoController.finish);
            post("/remove", TodoController.remove);
        });
    }
}
```

| no. | description |
|-----|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| (1) | 静的リソースをホストするため、`staticFiles`メソッドが用意されています。ここでは、`src/main/resources/public`ディレクトリのCSSファイルを公開し、ブラウザキャッシュを10分間保持させています。 |
| (2) | `path`メソッドは、パスの階層化・グルーピングに使用します。例えば、2番目の`post`メソッドにはパス`/create`を指定しているので、`/todos/create`というパスへのリクエストがマッピングされることになります。 |
| (3) | 画面を返却するメソッドとパスに対するリクエストを処理するハンドラに先ほど作成したコントローラクラスを指定し、テンプレートを処理する`TemplateEngine`に`ThymeleafTemplateEngine`を指定しています。 |
| (4) | リダイレクトするパスでは画面を返却する必要がないため、`TemplateEngine`は指定しません。 |

### 実行

アプリを起動して、ブラウザから`http://localhost:4567/todos`にアクセスすると、TODOリストの画面が返却され、ページアプリとして機能していることが分かると思います。

## まとめ

`TemplateViewRoute`とテンプレートエンジンを利用して、ページアプリをシンプルに実装できることが分かりました。
REST APIに比べると、HTMLフォームのパラメータからモデルに変換する機構がないため、ここをどう実装するかがポイントになりそうですね。

次回以降、ロギングや入力チェックなど、アプリを作成する上でポイントとなる機能の実装について解説していきます。