SpringBootからThymeleafのテキストテンプレートモードを使う

# はじめに
Thymeleafは本来Webページ(HTML)用のテンプレートエンジンで、HTMLの出力に特化したものでしたが、Thymeleaf3系からは、プレーンテキストも出力できます。これによりメールやチャットへの投稿用テンプレートにも使えます。

# 前準備
本記事ではmavenを使った最小限のSpringBootで、テキストを標準出力するものを用意します。SpringBoot2系と1系とで、依存関係にあるライブラリが異なります。プロジェクト作成の際には、SpringToolsSuiteないしはmavenが利用できるIDEをお使いください。pom.xmlは記事の最後に紹介しています。

# テキストモードの設定
テキストモードでThymeleafを動かすための設定を、Spring経由で行います。
Thymeleafのテンプレートは TemplateEngine インタフェースを継承したクラスから扱います。今回はSpringBootから扱うので、Spring用の SpringTemplateEngine を用います。

```java:ThymeleafConfig.java
package com.github.apz.springsample.config;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.core.env.Environment;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.spring5.SpringTemplateEngine;    // SpringBoot1系は、import org.thymeleaf.spring4.SpringTemplateEngine;
import org.thymeleaf.templatemode.TemplateMode;
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver;
import org.thymeleaf.templateresolver.ITemplateResolver;

@Configuration
public class ThymeleafConfig implements ApplicationContextAware, EnvironmentAware {
	@Bean
	public TemplateEngine textTemplateEngine() {
		final SpringTemplateEngine templateEngine = new SpringTemplateEngine();
		templateEngine.addTemplateResolver(textTemplateResolver());
		templateEngine.setTemplateEngineMessageSource(textMessageSource());
		return templateEngine;
	}

	private ITemplateResolver textTemplateResolver() {
		final ClassLoaderTemplateResolver templateResolver = new ClassLoaderTemplateResolver();
		templateResolver.setOrder(Integer.valueOf(1));
		templateResolver.setPrefix("/mail/");
		templateResolver.setSuffix(".txt");
		templateResolver.setTemplateMode(TemplateMode.TEXT);
		templateResolver.setCharacterEncoding("utf-8");
		templateResolver.setCacheable(false);
		return templateResolver;
	}

	@Bean
	public ResourceBundleMessageSource textMessageSource() {
		final ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
		messageSource.setBasename("mail/Messages");
		return messageSource;
	}

	@Override
	public void setEnvironment(Environment environment) {
		this.environment = environment;
	}

	@Override
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		this.applicationContext = applicationContext;
	}

	private ApplicationContext applicationContext;
	private Environment environment;
}

```

TemplateEngineを動かすには、テンプレートファイルの読み込み設定を司る TemplateResolver が必須です。さらにメッセージ定義プロパティからメッセージを出力できるよう、Springで扱っているMessageSource も渡せます。

このクラスの記述により、Springは textTemplateEngine の名前で参照でき、今回定義したTemplateResolverの内容でテキストテンプレートを使います。TemplateResolverでは特に以下の設定に注視してください。

```java
// テンプレートファイルの配置ディレクトリ（src/main/resources/templates/ 以下のディレクトリ)
templateResolver.setPrefix("/mail/");

// テンプレートファイルの拡張子。ここに設定された値以外は読み込まなくなる
templateResolver.setSuffix(".txt");

// Thymeleafのテンプレートモード。テキスト出力なので、TEXTを指定
templateResolver.setTemplateMode(TemplateMode.TEXT);

// 読み込むテンプレートファイルの文字エンコード
templateResolver.setCharacterEncoding("utf-8");
```

# 実際に出力する

設定が完了したので、次は出力するクラスです。

```java
package com.github.apz.springsample.component;

import java.util.Arrays;
import java.util.Date;
import java.util.Locale;

import org.springframework.stereotype.Component;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;

import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;

@Component
@RequiredArgsConstructor  // ※
@Log4j2                   // ※
public class ThymeleafText {
	private final TemplateEngine templateEngine;

	public void process() {
		final Context ctx = new Context(Locale.getDefault());
		ctx.setVariable("subscriptionDate", new Date());		
		String text = this.templateEngine.process("/mail/sample.txt", ctx);

		log.info(text);
	}
}
```
※RequiredArgsConstructorとLog4j2はproject Lombokのアノテーションです。

Thymeleafテンプレートエンジンから出力するには、以下のステップで完了です。

1. ThymeleafのContextインスタンスを作り
2. Thymeleafのテンプレート内で使う変数をContextに入れ
3. テンプレートエンジンに、読み込むテンプレートとコンテキストを渡して processを実行するだけです

## 起動クラス

SpringBootの起動クラスを作って、動作をしてみます。

```java
package com.github.apz.springsample;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;

import com.github.apz.springsample.component.ThymeleafText;

@SpringBootApplication
public class SpringThymeleaf3TextApplication implements CommandLineRunner {

	@Autowired
	private ThymeleafText thymeleafText;

	public static void main(String[] args) {
		new SpringApplicationBuilder(SpringThymeleaf3TextApplication.class).web(false).run(args);
	}

	@Override
	public void run(String... args) throws Exception {
		thymeleafText.process();
	}
}
```

テンプレートファイル

```
これはThymeleafテキストサンプルです。
[# th:if=${display}]======
  displayがtrueのときは[# th:utext="${#dates.format(subscriptionDate, 'dd/MMM/yyyy HH:mm')}" /]表示されます。
======
[/]末尾です。
```

テキストモードのThymeleafは、[# th:属性名] をタグとして扱います。それ以外はThymeleafの標準機能がごく一部を除きそのまま使えます。

実行結果は次のようになるでしょう。(Log4J2の出力は省略しています）

```
これはThymeleafテキストサンプルです。
======
  displayがtrueのときは11/7/2018 07:32表示されます。
======
末尾です。
```

以上です(・ω・)

# Appendix

## 参考文献
[Tutorial: Using Thymeleaf (ja)](https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf_ja.html)
[Tutorial: Thymeleaf + Spring](https://www.thymeleaf.org/doc/tutorials/3.0/thymeleafspring.html)

## サンプルコード
[https://github.com/A-pZ/spring-thymelaf3-text](https://github.com/A-pZ/spring-thymelaf3-text)

## pom.xml (SpringBoot2系)
```
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>com.github.a-pz.sample</groupId>
	<artifactId>spring-thymeleaf3-text</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>

	<name>spring-thymeleaf3-text</name>
	<description>spring-thymeleaf-text-sample</description>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.0.3.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>

		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.apache.logging.log4j</groupId>
			<artifactId>log4j-core</artifactId>
			<version>2.11.0</version>
		</dependency>
		<dependency>
			<groupId>org.apache.logging.log4j</groupId>
			<artifactId>log4j-api</artifactId>
			<version>2.11.0</version>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>
```

## pom.xml (SpringBoot1系)
SpringBoot1系は、デフォルトはThymeleaf2系になってしまうので、3系を指定し、さらに必要なプラグインを追加します。

```
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>com.github.a-pz.sample</groupId>
	<artifactId>spring-thymeleaf3-text</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>

	<name>spring-thymeleaf3-text</name>
	<description>spring-thymeleaf-text-sample</description>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>1.5.14.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>

		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.apache.logging.log4j</groupId>
			<artifactId>log4j-core</artifactId>
			<version>2.11.0</version>
		</dependency>
		<dependency>
			<groupId>org.apache.logging.log4j</groupId>
			<artifactId>log4j-api</artifactId>
			<version>2.11.0</version>
		</dependency>
		<dependency>
			<groupId>org.thymeleaf</groupId>
			<artifactId>thymeleaf</artifactId>
			<version>3.0.9.RELEASE</version>
		</dependency>
		<dependency>
			<groupId>org.thymeleaf</groupId>
			<artifactId>thymeleaf-spring4</artifactId>
			<version>3.0.9.RELEASE</version>
		</dependency>
		<dependency>
			<groupId>nz.net.ultraq.thymeleaf</groupId>
			<artifactId>thymeleaf-layout-dialect</artifactId>
			<version>2.3.0</version>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>
```