Thymeleaf3で改行コードを改行タグに変換する独自ダイアレクトを作成

# 要旨
Thymeleafで改行コードを含む文字列を出力してもHTMLソース内で改行されるだけで表示上は改行されない。
改行コードを改行タグに変換して文字列を出力する機能を持つ独自ダイアレクトを作成し`th:text`の代替ダイアレクトとして使用する。
なお、Thymeleaf2と3では実装が異なるが本稿ではThymeleaf3を扱う。

#### 環境
* Java SE 8 (jdk-8u181)
* Maven 3.5.4
* Spring Boot 2.0.5
* Thymeleaf 3.0.9

#### 概要
* Processorクラス(操作を実際に記述するクラス)を実装する
* Dialectクラス(Processorをまとめてダイアレクトとして定義するクラス)を実装する
* Beanに設定する
* テンプレートで使用する

# 詳細手順
## Processorクラス(操作を実際に記述するクラス)を実装する
#### クラス定義
Thymeleafが提供しているProcessor系の抽象クラスからやりたいことに近いものを選んでそれを継承するクラスを作成する。
今回はStandard Dialectである`th:text`に類似したものを作成するので`AbstractStandardExpressionAttributeTagProcessor`を継承する。

```Java
public class BreakLineProcessor extends AbstractStandardExpressionAttributeTagProcessor {
  ...
}
```

#### コンストラクタ
必ずコンストラクタを作成し、当該Processorがどのように使われるかを定義する。

```Java
  private static final String ATTR_NAME = "brtext";

  public BreakLineProcessor(String dialectPrefix, int precedence) {
    super(TemplateMode.HTML, dialectPrefix, ATTR_NAME, precedence, true);
  }
```
なお、継承元クラスである`AbstractStandardExpressionAttributeTagProcessor`のコンストラクタの各引数は以下のとおりである。

||変数名|クラス|役割|
|---|---|---|---|
|1|templateMode|TemplateMode(enum)|テンプレートの種類(HTMLなど)|
|2|dialectPrefix|String|ダイアレクトのプレフィックス(コロンの左側)|
|3|attrName|String|ダイアレクトの属性名(コロンの右側)|
|4|precedence|int|Processor処理実行の優先順位(値が小さいほうが優先)|
|5|removeAttribute|boolean|Processor処理後に当該ダイアレクト属性を除去するか(trueなら除去する)|

今回はプレフィックスと優先順位のみ外から受け取り、残りは固定値とした。

#### 処理実行メソッド
抽象メソッドである`doProcess`をオーバーライドして処理を記述する。
`AbstractStandardExpressionAttributeTagProcessor`の`doProcess`の場合はEL式を解釈した結果が引数の`expressionResult`に格納されるので、これを改行コードごとに分割し、改行タグをはさみながら出力すればよい。
全体の処理の概要は以下の通り。

1. 引数の`context`から`IModelFactory`インスタンスを取得する。
1. 取得した`IModelFactory`インスタンスから`IModel`インスタンスを作成する。
1. 作成した`IModel`インスタンスにHTMLに反映する要素(文字列やタグ)を順番に追加する。
  なお、追加するインスタンスも`IModelFactory`インスタンスから作成する。
1. 引数の`structureHandler`に`IModel`インスタンスを適用する。

```Java
  @Override
	protected void doProcess(ITemplateContext context, IProcessableElementTag tag, AttributeName attributeName,
			String attributeValue, Object expressionResult, IElementTagStructureHandler structureHandler) {
		// nullの場合は何も出力せず処理を終了
		if (expressionResult == null) return;

		IModelFactory factory = context.getModelFactory();
		IModel model = factory.createModel();

		ITemplateEvent brTag = factory.createOpenElementTag("br");

    // 改行コードで分割
		String[] lines = expressionResult.toString().split("\r\n|\r|\n", -1);
		for (String line : lines) {
      // <br>+1行分の文字列をmodelに追加
			model.add(brTag);
			model.add(factory.createText(line));
		}
		// 先頭の<br>タグを除去
		model.remove(0);

		structureHandler.setBody(model, false);
```

## Dialectクラス(Processorをまとめてダイアレクトとして定義するクラス)を実装する
#### クラス定義
Thymeleafが提供しているDialect系の抽象クラスやインターフェースから作成したダイアレクト処理に合致するもの選んでそれを継承(実装)するクラスを作成する。
今回はProcessorにより本処理を実装したので抽象クラス`AbstractProcessorDialect`を継承する。

```Java
public class KurukuruzDialect extends AbstractProcessorDialect {
  ...
}
```

#### コンストラクタ
必ずコンストラクタを作成し、当該Dialectがどのように使われるかを定義する。

```Java
  private static final String NAME = "kurukuruz original dialect";
  private static final String PREFIX = "krz";

  public KurukuruzDialect() {
		  super(NAME, PREFIX, StandardDialect.PROCESSOR_PRECEDENCE);
	}
```

なお、継承元クラスである`AbstractProcessorDialect`のコンストラクタの各引数は以下のとおりである。

||変数名|クラス|役割|
|---|---|---|---|
|1|name|String|ダイアレクトの名前(?), 特に使うことはない|
|2|prefix|String|ダイアレクトのプレフィックス, 後述のProcessor登録メソッドの引数になる|
|3|processorPrecedence|int|Processor処理実行の優先順位に使用する数値|

#### Processor登録
`AbstractProcessorDialect`の抽象メソッドである`getProcessors`内でダイアレクトに登録する各ProcessorのインスタンスをSetとしてまとめ、返却する。

```Java
  @Override
  public Set<IProcessor> getProcessors(String dialectPrefix) {
    Set<IProcessor> processors = new HashSet<>();

    processors.add(new BreakLineProcessor(dialectPrefix, getDialectProcessorPrecedence()));

    return processors;
  }
```

## Beanに設定する
`@Bean`アノテーションが有効な適当なクラスで以下のようにBean定義メソッドを作成し、Dialectインスタンスを登録する。

```Java
 @Bean
 KurukuruzDialect kurukuruzDialect() {
   return new KurukuruzDialect();
 }
```

# 実行結果
* HTMLテンプレート

```HTML
<body>
<div krz:brtext="${text1}"></div>
<div krz:brtext="${text2}"></div>
<div krz:brtext="${text3}"></div>
<div krz:brtext="${text4}"></div>
<div krz:brtext="${text5}"></div>
</body>
```

* 表示対象のModel

```Java
model.addAttribute("text1", "改行無し");
model.addAttribute("text2", "CRLF改行\r\nあり\r\n\r\n↑2回連続改行");
model.addAttribute("text3", "CR改行\rあり");
model.addAttribute("text4", "LF改行\nあり");
model.addAttribute("text5", null);
```

↓↓↓↓↓

* 出力結果(html)

```HTML
<body>
<div>改行無し</div>
<div>CRLF改行<br>あり<br><br>↑2回連続改行</div>
<div>CR改行<br>あり</div>
<div>LF改行<br>あり</div>
<div></div>
</body>
```

* 出力結果(画面表示)

![画面表示.png](https://qiita-image-store.s3.amazonaws.com/0/266076/102d0253-f989-608f-9417-7046d1289cd2.png)

# 参考サイト
[Thymeleafで改行コードをhtmlの改行タグに変換して出力する - Qiita](https://qiita.com/ushidatmhr/items/64102d81bfab12dcdf86)
[Thymeleaf3で独自のDialectを作る - Qiita](https://qiita.com/yoshikawaa/items/aba090f291f69185e6a5)
[thymeleaf3でDialectを実装する - abcdefg.....](http://pppurple.hatenablog.com/entry/2017/03/22/225114)