CLOVER🍀

That was when it all began.

Javaのテンプレートエンジン、Pebble、Rythm、Jtwig、HTTLをちょっと試してみる

Javaのテンプレートエンジンを使う時はVelocityやFreeMarkerを選択することが多いのですが、他にいい選択肢がないのかなと思ってちょっと調べてみました。

自分が求めているのはHTMLに特化していない、汎用のテンプレートエンジンですね。なので、この文脈だとThymeleafは外れるかなと思っています。

でまあ、このあたりのエントリを見たりして

A Java Template Engine
http://vicox.net/2014/09/02/a-java-template-engine/

今回は、以下のテンプレートエンジンを試してみました。

  • Pebble
  • Rythm
  • Jtwig
  • HTTL

詳しく比較したりはしません。まずは試してみて、合うかな?興味が持てるかな?という程度の動機です。

では、順に試してみます。

あ、各テンプレートエンジン中で一部使っている、JavaBeans的なものは以下の定義としています。パッケージは、各Mainクラスと同じパッケージに属しているものとします。

public class Person {
    private String lastName;

    private String firstName;

    public Person(String lastName, String firstName) {
        this.lastName = lastName;
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public String getFirstName() {
        return firstName;
    }
}

また、各例において、テンプレートはクラスパス上にファイルとして置くものとします。

Pebble

Djangoにインスパイアされたシンタックスを持つテンプレートエンジンらしいですけど、自分はDjangoを使ったことがないのでよくわかりませんが…。

Pebble
http://www.mitchellbosecke.com/pebble/home

エスケープも自動的にかかるらしいです。

では、以下のページを見つつ試してみます。

Installation & Configuration
http://www.mitchellbosecke.com/pebble/documentation/guide/installation

Basic Usage
http://www.mitchellbosecke.com/pebble/documentation/guide/basic-usage

Maven依存関係。

        <dependency>
            <groupId>com.mitchellbosecke</groupId>
            <artifactId>pebble</artifactId>
            <version>1.5.1</version>
        </dependency>

サンプルコード。
src/main/java/org/littlewings/template/pebble/PebbleExample.java

package org.littlewings.template.pebble;

import java.io.IOException;
import java.io.StringWriter;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

import com.mitchellbosecke.pebble.PebbleEngine;
import com.mitchellbosecke.pebble.error.PebbleException;
import com.mitchellbosecke.pebble.template.PebbleTemplate;

public class PebbleExample {
    public static void main(String... args) throws PebbleException, IOException {
        PebbleEngine engine = new PebbleEngine();

        StringWriter writer = new StringWriter();

        Map<String, Object> context = new HashMap<>();
        context.put("word", "世界");
        context.put("persons",
                Arrays.asList(new Person("磯野", "カツオ"), new Person("磯野", "ワカメ")));
        context.put("containTag", "<script>Hello!!</script>");

        PebbleTemplate template = engine.getTemplate("templates/sample.peb");
        template.evaluate(writer, context);

        System.out.println(writer);
    }
}

テンプレート。
src/main/resources/templates/sample.peb

こんにちは、{{ word }}

{% for person in persons %}
  姓: {{ person.lastName }} 名: {{ person.firstName }}

{% endfor %}

escape?: {{ containTag }}

raw: {{ containTag | raw }}

変数は{{ }}で囲み、forなどは{% %}で囲うみたいです。

実行結果。

こんにちは、世界
  姓: 磯野 名: カツオ
  姓: 磯野 名: ワカメ

escape?: &lt;script&gt;Hello!!&lt;/script&gt;
raw: <script>Hello!!</script>

HTMLタグは、デフォルトでエスケープされています。

使ってちょっと違和感があったのが、改行の扱いですね…。テンプレートに改行がそこそこ入っている割には、出力結果には含まれていませんが、これを削ると行がくっついたりします。

どういうことでしょう…。

Rythm

オフィシャルサイトが国際化されていますが、なんか日本語が怪しい感じ…。

Rythm
http://rythmengine.org/

また、唯一日本語エントリがありました。

Java の template engine の Rythm を試す
http://tc.hatenablog.com/entry/2015/04/07/213444

では、チュートリアルやテンプレートエンジンガイドにそって試してみます。

Tutorial
http://rythmengine.org/doc/tutorial.md

Template Author's Guide
http://rythmengine.org/doc/template_guide.md

Maven依存関係。

        <dependency>
            <groupId>org.rythmengine</groupId>
            <artifactId>rythm-engine</artifactId>
            <version>1.0.1</version>
        </dependency>

サンプルコード。
src/main/java/org/littlewings/template/rythm/RythmExample.java

package org.littlewings.template.rythm;

import java.io.IOException;
import java.io.StringWriter;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;

import org.rythmengine.RythmEngine;

public class RythmExample {
    public static void main(String... args) throws URISyntaxException, IOException {
        RythmEngine rythm = new RythmEngine();

        StringWriter writer = new StringWriter();

        Path templatePath = Paths.get(Thread.currentThread().getContextClassLoader().getResource("templates/sample.txt").toURI());

        Map<String, Object> context = new LinkedHashMap<>();
        context.put("word", "世界");
        context.put("persons",
                Arrays.asList(new Person("磯野", "カツオ"), new Person("磯野", "ワカメ")));
        context.put("containTag", "<script>Hello!!</script>");

        rythm.render(writer, templatePath.toFile(), context);

        System.out.println(writer);
    }
}

テンプレート。
src/main/resources/templates/sample.txt

@args String word, List persons, String containTag
@import org.littlewings.template.rythm.Person

こんにちは、@word

@for(Person person : persons) {
  姓: @person.getLastName() 名: @person.getFirstName()
}

escape?: @containTag

最初に、使う変数とその型の宣言が必要です。

なお、テンプレートにバインドする変数を単一のMapとして渡した場合は、展開されてテンプレートに渡されるようです。
https://github.com/greenlaw110/Rythm/blob/rythm-engine-1.0.1/src/main/java/org/rythmengine/RythmEngine.java#L872

これがわからず、けっこうハマりました…。

実行結果。

こんにちは、世界
 姓: 磯野 名: カツオ
 姓: 磯野 名: ワカメ

escape?: <script>Hello!!</script>

こちらは、エスケープされずに出力されます。

なのですが、テンプレートファイルの拡張子を「.html」にすると、なんとエスケープされるようになります。
src/main/resources/templates/sample.html

@args String word, List persons, String containTag
@import org.littlewings.template.rythm.Person

こんにちは、@word

@for(Person person : persons) {
  姓: @person.getLastName() 名: @person.getFirstName()
}

escape?: @containTag

テンプレートを読むパスも変更。

        Path templatePath = Paths.get(Thread.currentThread().getContextClassLoader().getResource("templates/sample.html").toURI());

出力結果。

こんにちは、世界
 姓: 磯野 名: カツオ
 姓: 磯野 名: ワカメ

escape?: &lt;script&gt;Hello!!&lt;/script&gt;

あと、ちょっと変わった挙動として、メソッドなどがテンプレート上で宣言された型に対して見つけることができないと、実行時にエラーになります。

例えば、こんな感じにgetLastNameのつづりを間違えると

  姓: @person.getLastNam() 名: @person.getFirstName()

こんな感じのエラーになります。

Exception in thread "main" org.rythmengine.exception.CompileException: The method getLastNam() is undefined for the type Person

Template: /xxxxx/rythm-example/target/classes/templates/sample.txt

Relevant template source lines:
-------------------------------------------------
   2: @import org.littlewings.template.rythm.Person
   3: 
   4: こんにちは、@word
   5: 
   6: @for(Person person : persons) {
>> 7:   姓: @person.getLastNam() 名: @person.getFirstName()
   8: }
   9: 
   10: escape?: @containTag


Relevant Java source lines:
-------------------------------------------------
   123: org.rythmengine.internal.LoopUtil person_utils = new org.rythmengine.internal.LoopUtil(person_isFirst, person_isLast); //line: 6
   124: org.rythmengine.internal.LoopUtil person__utils = new org.rythmengine.internal.LoopUtil(person_isFirst, person_isLast, person); //line: 6
   125: __pushItrVar("person", person); //line: 6
   126: p(__v6361); //line: 7
   127: 
>> 128: try{pe(person.getLastNam());} catch (RuntimeException e) {__handleTemplateExecutionException(e);}  //line: 7
   129: p(__v14126); //line: 7
   130: 
   131: try{pe(person.getFirstName());} catch (RuntimeException e) {__handleTemplateExecutionException(e);}  //line: 7
   132: p(__v73678); //line: 7
   133: 
   134: 	__popItrVar();

これは、リフレクションで解決できればOKみたいな挙動ではなく、例えテンプレートにバインドされたオブジェクトが該当のメソッドやフィールドを持っていても、Object型として扱われていたりすると対象のメンバーが解決できずにエラーになります。

へぇ〜って感じでした。

Jtwig

PHPにTwigというテンプレートエンジンがあるみたいなのですが、それの移植版だったりするんでしょうか…?

Jtwig
http://jtwig.org/

シンタックスも似ている気がするので、そんな感じが。

チュートリアルがSpring MVCで書かれているのですが、

Integrating Jtwig
http://jtwig.org/documentation/get-started/

とりあえず普通に使います。

サンプルを参考にしました。

https://github.com/jtwig/jtwig-examples/tree/master/simple-app

Maven依存関係。

        <dependency>
            <groupId>com.lyncode</groupId>
            <artifactId>jtwig-core</artifactId>
            <version>3.1.1</version>
        </dependency>

サンプルコード。
src/main/java/org/littlewings/template/jtwig/JtwigExample.java

package org.littlewings.template.jtwig;

import java.net.URISyntaxException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;

import com.lyncode.jtwig.JtwigModelMap;
import com.lyncode.jtwig.JtwigTemplate;
import com.lyncode.jtwig.configuration.JtwigConfiguration;
import com.lyncode.jtwig.exception.CompileException;
import com.lyncode.jtwig.exception.ParseException;
import com.lyncode.jtwig.exception.RenderException;

public class JtwigExample {
    public static void main(String... args) throws ParseException, CompileException, RenderException, URISyntaxException {
        Path templatePath = Paths.get(Thread.currentThread().getContextClassLoader().getResource("templates/sample.twig").toURI());

        JtwigConfiguration config = new JtwigConfiguration();
        JtwigTemplate template = new JtwigTemplate(templatePath.toFile(), config);

        JtwigModelMap context = new JtwigModelMap();
        context.put("word", "世界");
        context.put("persons",
                Arrays.asList(new Person("磯野", "カツオ"), new Person("磯野", "ワカメ")));
        context.put("containTag", "<script>Hello!!</script>");

        System.out.println(template.output(context));
    }
}

テンプレートファイル。
src/main/resources/templates/sample.twig

こんにちは、{{ word }}
{% for person in persons %}
  姓: {{ person.lastName }} 名: {{ person.firstName }}
{% endfor %}
escape?: {{ containTag }}
escape?: {{ containTag | escape }}

Pebbleと同じような構文に見える…。

実行結果。

こんにちは、世界

  姓: 磯野 名: カツオ

  姓: 磯野 名: ワカメ

escape?: <script>Hello!!</script>
escape?: &lt;script&gt;Hello!!&lt;/script&gt;

これも改行の調整が??
あと、デフォルトでHTMLエスケープされるみたいですね。

HTTL

最後は、HTTL。Hyper-Text Template Languageの略らしく、パフォーマンスが高いと謳っています。あと、構文がVelocityに似ていると。

HTTL
http://httl.github.io/en/

サンプルはこちら。

Example
http://httl.github.io/en/example.html

Syntax
http://httl.github.io/en/syntax.html

Maven依存関係。

        <dependency>
            <groupId>com.github.httl</groupId>
            <artifactId>httl</artifactId>
            <version>1.0.11</version>
        </dependency>

サンプルコード。
src/main/java/org/littlewings/template/httl/HttlExample.java

package org.littlewings.template.httl;

import java.io.IOException;
import java.io.StringWriter;
import java.text.ParseException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

import httl.Engine;
import httl.Template;

public class HttlExample {
    public static void main(String... args) throws IOException, ParseException {
        Engine engine = Engine.getEngine();

        Map<String, Object> context = new HashMap<>();
        context.put("word", "世界");
        context.put("persons",
                Arrays.asList(new Person("磯野", "カツオ"), new Person("磯野", "ワカメ")));
        context.put("containTag", "<script>Hello!!</script>");

        Template template = engine.getTemplate("templates/sample.httl", "UTF-8");

        StringWriter writer = new StringWriter();
        template.render(context, writer);

        System.out.println(writer);
    }
}

テンプレートファイル。
src/main/resources/templates/sample.httl

#set(String word, List<org.littlewings.template.httl.Person> persons, String containTag)
こんにちは、${word}

#for(org.littlewings.template.httl.Person person : persons)
  姓: ${person.lastName} 名: ${person.firstName}
#end

escape?: ${containTag}
raw?: $!{containTag}

確かにちょっとVelocityっぽいですが、型の宣言が必要です。

出力結果。

こんにちは、世界

  姓: 磯野 名: カツオ
  姓: 磯野 名: ワカメ

escape?: &lt;script&gt;Hello!!&lt;/script&gt;
raw?: <script>Hello!!</script>

デフォルトでHTMLエスケープがかかり、$!でエスケープ解除です。

型宣言していることから、前述のRythmの時と同じように、プロパティ名など間違えると

  姓: ${person.lastName} 名: ${person.firstName}

エラーになってコケます。

重大: No such property lastNam in class org.littlewings.template.httl.Person, because no such method getLastNam() or method isLastNam() or method lastNam() or filed lastNam.
Occur to offset: 177, line: 5, column: 14, char: ., in: 
/templates/sample.httl
========================================
...  姓: ${person.lastNam} 名: ${person...
                ^-here
========================================
, stack: java.text.ParseException: No such property lastNam in class org.littlewings.template.httl.Person, because no such method getLastNam() or method isLastNam() or method lastNam() or filed lastNam.

また、以下のようなプロパティファイルを用意して
src/main/resources/httl-html.properties

comment.left=<!--
comment.right=-->

コードをこのように変更して、設定ファイルを読むようにすると

    public static void main(String... args) throws IOException, ParseException {
        Engine engine = Engine.getEngine("httl-html.properties");

        Map<String, Object> context = new HashMap<>();
        context.put("word", "世界");
        context.put("persons",
                Arrays.asList(new Person("磯野", "カツオ"), new Person("磯野", "ワカメ")));
        context.put("containTag", "<script>Hello!!</script>");

        Template template = engine.getTemplate("templates/sample-html.httl", "UTF-8");

        StringWriter writer = new StringWriter();
        template.render(context, writer);

        System.out.println(writer);
    }

各ディレクティブを、HTMLコメントの中に埋め込むことができるようになります。

<!-- #set(String word, List<org.littlewings.template.httl.Person> persons, String containTag) -->
こんにちは、${word}

<!-- #for(org.littlewings.template.httl.Person person : persons) -->
  姓: ${person.lastName} 名: ${person.firstName}
<!-- #end -->

escape?: ${containTag}
raw?: $!{containTag}

「jericho-html」を使うことで、HTML属性中にディレクティブを書くことも可能みたいです。

で、どうだった?

とりあえず、さらっと「Hello World」的に試してみましたが、個人的にはこの段階では「これは!」みたいな印象は持てなかった気がします。

どれも、そこそこ癖がありますね。VelocityとかFreeMarkerでもいいんじゃないかな、と思ったり…。
※Velocityは開発がほぼ止まっている以外に、Toolsとログまわりがちょっと微妙なんですけど

Pebbleの改行に癖がなかったら、もうちょっと考えたかも。

なお、いずれのテンプレートエンジンも、現在はそこまで活発に開発が行われているわけではなさそうです。Pebbleはぼちぼち続いているみたいですが。

このエントリを読まれた方で、興味を引くところがあれば。