Thymeleafで独自Dialectを作成・設定して使ってみた

参画する案件の都合もあり、標準のスタンダードダイアレクトだけでは実現不可能な挙動が多数あったので独自で実装することになりました。
そもそもThymeleaf自体新人時代の研修からあまり触れる機会がなく、むしろJSPの方が馴染み深かったのでほぼイチから勉強しつつはじめました。

実際に試行錯誤しながら実装して意図した動作を再現できたりするうちに段々と楽しくなってきたので、備忘録として残しておこうと思います。

## そもそもDialectとは？
 
基本的に使用する代表的なものは以下のように分類されます。

| 名前 | 説明 |
|:-----------|:--------|
| IProcessorDialect | 要素や属性を提供するダイアレクト |
| IExpressionObjectDialect | タグ属性値内のEL式で使用できるオブジェクトを提供するダイアレクト |

IProcessorDialectはProcessorを登録してタグを実装するもので`th:text`のような記述方法で使用可能なもので、
IExpressionObjectDialectは`#strings`のようにEL式内で使用可能なユーティリティを追加できることができます。

## 独自Dialectの使用方法・作成

### IProcessorDialect

要素や属性のように記述できるDialectです。
IProcessorDialectにはAbstractProcessorDialectという抽象クラスが用意されているため、通常はそちらを継承して実装していきます。
実際の処理を実装するProcessorを別途作成し、AbstractProcessorDialectの実装クラス内で登録して動作するようにします。

例えば日付を`yyyy/mm/dd`でフォーマットするように作成すると以下のように使用することができます。

■テンプレート上の記述

```html
<span thex:formatdate="${date}"></span>
```

■実際に出力される要素

```html
<span>2020/01/01</span>
```

また、以前作成したDialectをscriptタグ内でインラインで記述したところ、Dialectが認識されずハマったことがありました。

```javascript
<script th:inline="javascript">
	var test =  [# thex:formatdate="${date}" /] ;
	console.log(test);
</script>
```

AbstractProcessorDialectの実装の中でインラインで記述が認識されるように設定しないと、テンプレート上で認識されないようです。
そのため、JavaScriptもしくはCSSなど使用するケースに合わせてテンプレートモードの設定が必要です。

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

  proccessors.add(new SampleProcesssor(TemplateMode.HTML, dialectPrefix));
  proccessors.add(new SampleProcesssor(TemplateMode.JAVASCRIPT, dialectPrefix));
  proccessors.add(new SampleProcesssor(TemplateMode.CSS, dialectPrefix));

  return proccessors;
}
```

### IExpressionObjectDialect

お試しで`IExpressionObjectDialect `を実装して`#sample`としてEL式内で使用できるユーティリティを作成していきます。
ユーティリティとして追加する名前については、標準提供されているものと重複しないように注意が必要です。

まずExpressionObjectを登録するDialectを実装していきます。
ここではEL式内で使用する際のユーティリティの名前・実際に処理を行うクラスをインタンス生成を行っていきます。

```java
public class SampleDialect extends AbstractDialect implements IExpressionObjectDialect {

	// EL式内で使用する際の名前の定数
	private static final String SAMPLE_DIALECT_NAME = "sample";
	
	private static final Set<String> EXPRESSION_OBJECT_NAMES = Collections.singleton(SAMPLE_DIALECT_NAME);


	public SampleDialect() {
		super(SAMPLE_DIALECT_NAME);
	}
	
	@Override
	public IExpressionObjectFactory getExpressionObjectFactory() {
		return new IExpressionObjectFactory() {

			@Override
			public Set<String> getAllExpressionObjectNames() {
				return EXPRESSION_OBJECT_NAMES;
			}

			@Override
			public Object buildObject(IExpressionContext context, String expressionObjectName) {
				if (SAMPLE_DIALECT_NAME.equals(expressionObjectName)) {
					// ユーティリティとして処理を行うクラスのインスタンス生成
					return new Sample();
				}
				return null;
			}

			@Override
			public boolean isCacheable(String expressionObjectName) {
				return true;
			}

		};
	}

}
```

次に実際にテンプレート側から処理を移譲するクラスを作成していきます。
今回は文字列を受け渡し、整形して画面に表示するシンプルな処理で実装します。

```java
public class Sample {
	
	public String getSampleString(final String str) {
		return "サンプルの文字列「" + str + "」";
	}

}
```

実際にHTML上で以下のようにEL式内で使用することができるようになります。

■テンプレート上の記述

```html
<span th:text="${#sample.getSampleString('sampleです')}" ></span>
```

■実際に出力される要素

```html
<span>サンプルの文字列「sampleです」</span>
```

## Spring MVC上の設定実装

MVC上で動作できるよう設定クラスにViewResolverの設定を行っていきます。
(Java Configの実装方法については[こちら](https://qiita.com/HiroyaEnd/items/17175e947911d84c1b3b#webmvcconfig))
ここで独自のDialectを登録することができるので、併せて登録してテンプレート上で動作するようにすることが可能です。
(IProcessorDialect・IExpressionObjectDialectどちらも設定方法は基本的に同じです)

```java
// エンコード設定に使用する定数
public static final String CHARACTER_ENCODING = "UTF-8";

// テンプレートが配置されているベースディレクトリパスの定数
private static final String VIEW_LOCATION = "/WEB-INF/views/";


// ThymeleafViewResolverをBean定義する
@Bean
public ThymeleafViewResolver viewResolver() {
	final ThymeleafViewResolver thymeleafViewResolver = new ThymeleafViewResolver();
	thymeleafViewResolver.setTemplateEngine(templateEngine());
	thymeleafViewResolver.setCharacterEncoding(CHARACTER_ENCODING); // レスポンスのエンコーディングを設定する
	
	return thymeleafViewResolver;
}

// SpringTemplateEngineをBean定義する
@Bean
public SpringTemplateEngine templateEngine() {
	final SpringTemplateEngine templateEngine = new SpringTemplateEngine();
	templateEngine.setTemplateResolver(templateResolver());
	templateEngine.addDialect(new SampleDialect()); // 独自Dialectを登録する

	return templateEngine;
}

// TemplateResolverをBean定義する
@Bean
public AbstractConfigurableTemplateResolver templateResolver() {
	final ClassLoaderTemplateResolver templateResolver = new ClassLoaderTemplateResolver();
	templateResolver.setPrefix(VIEW_LOCATION); // Thymeleafテンプレートが格納されているベースディレクトリを指定する
	templateResolver.setSuffix(".html"); // Thymeleafテンプレートの拡張子を設定する
	templateResolver.setTemplateMode(TemplateMode.HTML); // 解釈するテンプレートモードを設定する(デフォルト値はHTMLモードだが明示的に設定している)
	templateResolver.setCharacterEncoding(CHARACTER_ENCODING); // テンプレートファイルのエンコーディングを設定する
	
	return templateResolver;
}
```

設定実装が完了したら、あとはテンプレートファイルを作成して遷移させることで動作できるようになります。
上記の設定内容はあくまでサンプルなので、適宜環境に合わせて変更する必要があると思います。

## まとめ

実際に使ってみるとJSPに比べて使いやすいと思う点がたくさんありました。
独自のDialectを簡単に追加することができるので拡張性が高く、スタンダードダイアレクトと組み合わせることで柔軟に実装することができるのが魅力だと感じました。
個人的にはJSPの独自タグより拡張しやすかったです。

SpringがThymeleafを推奨しているのもあって今後も触れる機会はたくさんありそうなので、さらに知見を増やしていきたいと思っています。(できればfreemakerも…)

## 参考

https://qiita.com/yoshikawaa/items/aba090f291f69185e6a5

https://macchinetta.github.io/server-guideline-thymeleaf/1.5.1.RELEASE/ja/ArchitectureInDetail/WebApplicationDetail/Thymeleaf.html#id21