CLOVER🍀

That was when it all began.

Apache Velocity 2.0がリリースされたよという話

ちょっとした小ネタです。

ひっそりと、Apache Velocity 2.0がリリースされていました。

Velocity Engine 2.0 released

リリース日は、2017年8月6日。4ヶ月近く、気付いてませんでした(笑)。

Apache Velocityの更新の様子は時々見ているのですが、2.0タグが振られたと思ったらRCに戻ったり、2.0タグがまた更新
されたりしてだいぶ落ち着きがない感じがしていたのですが、いつの間にかリリースされていたようです…。

Apache Velocity 1.7が、2010年11月29日のリリースであることを考えると、実に7年ぶりくらいのリリースとなります。

で、せっかくなのでちょっと見てみましょうと。

お断り

世間的には、Apache Velocityはすでに役目を終えた印象かと思います。Spring Frameworkのサポートも外れていますし、
一般的に使うのであればVelocity Toolsも合わせて使うことになるとは思いますが、最新のVelocity Toolsはまだリリース
されていません。

[SPR-13795] Remove Velocity support - Spring JIRA

今回リリースされているのは、Velocity Engineのみとなります。

Note for Velocity Tools users: Velocity Tools 3.0 shall soon be released. Meanwhile, you are encouraged to use the Velocity Tools 3.x last snapshot (see Velocity Tools 3.x Upgrading notes).

http://velocity.apache.org/news.html#engine20

また、Apache Velocity 1.7から2.0になってものすごく劇的に変わったというわけでもないので、2.0のリリースで
なにか状況が変わるようなこともないと思います。

Apache Velocity自体の更新頻度も、とても低いですしね(Velocity Toolsはいつ出ることやら…)。

そんなわけで、ここから先はそれでもApache Velocityに興味のある方だけお読みいただければよいと思います。

Apache Velocity 2.0

正確には、Velocity Engine 2.0です。

リリース内容はこちら。
Velocity Engine 2.0 released

詳細なRelease Notesは、こちら。
Apache Velocity Engine - Changes Report

ざっくりとリリース内容を書くと

  • LoggingがSLF4Jへ
  • 空白の設定ができるようになったよ
  • メソッド引数と配列の添字に、式が使えるよ
  • 型変換ハンドラが設定可能だよ
  • メモリ消費量を削減したよ
  • JSR-223のサポート

という感じです。JSR-223のサポートってなにさ?という感じですけどね…。

あと、ビルドや依存関係の視点でいくと…

  • ビルドシステムがApache AntからApache Mavenへ
  • 必要な依存関係がSLF4JとCommons Lang 3のみに(前はCommons Collectionも必要だったり、オプションでOROとかJDOMとかいろいろあった)
  • アーティファクトのIDが「velocity」から「velocity-engine-core」へ

というような変化もあります。

Commons CollectionsのExtendedPropertiesというクラスに依存していたのですが、これをApache Velocity側に取り込んだようです…。
https://github.com/apache/velocity-engine/blob/2.0/velocity-engine-core/src/main/java/org/apache/velocity/util/ExtProperties.java

* This class is a clone of org.apache.commons.collections.ExtendedProperties
* (which has been removed from commons-collections-4.x)

https://github.com/apache/velocity-engine/blob/2.0/velocity-engine-core/src/main/java/org/apache/velocity/util/ExtProperties.java#L131-L132

まあ、依存関係がスッキリしたのは良いですよね。

地味な改善では、入出力のデフォルトがUTF-8になったり、JSR-223のサポート(!)とかどういう動機なのか不明なものもありますが、
ざっくり使ってみましょうか。

Getting Started

では、使ってみましょうというところですが、まともにドキュメントが揃っていないのでUser Guideで構文を、あとはサンプルを見て書くことに
なるでしょう。
https://github.com/apache/velocity-engine/tree/2.0/velocity-engine-examples

Velocity Engineを利用するのに、必要な依存関係はこちら。

        <dependency>
            <groupId>org.apache.velocity</groupId>
            <artifactId>velocity-engine-core</artifactId>
            <version>2.0</version>
        </dependency>

ロギングについてはSLF4Jですが、今回はslf4j-simpleを入れておくことにします。

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>1.7.25</version>
        </dependency>

実行はテストで。

    <dependencies>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.0.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.8.0</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.19</version>
                <dependencies>
                    <dependency>
                        <groupId>org.junit.platform</groupId>
                        <artifactId>junit-platform-surefire-provider</artifactId>
                        <version>1.0.2</version>
                    </dependency>
                    <dependency>
                        <groupId>org.junit.jupiter</groupId>
                        <artifactId>junit-jupiter-engine</artifactId>
                        <version>5.0.2</version>
                    </dependency>
                </dependencies>
            </plugin>
        </plugins>
    </build>

では、コードの雛形を…。
src/test/java/org/littlewings/velocity/VelocityCoreTest.java

package org.littlewings.velocity;

import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;

import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import org.apache.velocity.app.VelocityEngine;
import org.junit.jupiter.api.Test;

public class VelocityCoreTest {
    static {
        System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "debug");
    }

    // ここに、テストを書く!
}

なんとなく、slf4j-simpleをデバッグモードに。

velocity.propertiesは、こんな感じで用意。
src/test/resources/velocity.properties

resource.loader=classpath
classpath.resource.loader.class=org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader

設定ファイルを、クラスパスからロードできてもいいんではないかと…。

テンプレートは、こんな感じで用意。
src/test/resources/simple.vm

こんにちは $!{name}!!

Fruits:
#foreach ($fruit in $!fruits)
  #if ($fruit.price > 500)
  $fruit.name
  #end
#end

使い方は、これまでのApache Velocityの使い方と変わりません。

    @Test
    public void simpleUsage() throws IOException {
        Properties properties = new Properties();
        try (InputStream is =
                     getClass().getClassLoader().getResourceAsStream("velocity.properties")) {
            properties.load(is);
        }

        Velocity.init(properties);

        VelocityContext context = new VelocityContext();
        context.put("name", "Velocity");

        List<Fruit> fruits = Arrays.asList(
                Fruit.create("apple", 600),
                Fruit.create("orange", 300),
                Fruit.create("banana", 800)
        );

        context.put("fruits", fruits);

        Template template = Velocity.getTemplate("simple.vm");

        StringWriter writer = new StringWriter();
        template.merge(context, writer);
        System.out.println(writer.toString());
    }

Fruitクラスの定義は、こんな感じです。

    public static class Fruit {
        String name;
        int price;

        static Fruit create(String name, int price) {
            Fruit fruit = new Fruit();
            fruit.setName(name);
            fruit.setPrice(price);
            return fruit;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public int getPrice() {
            return price;
        }

        public void setPrice(int price) {
            this.price = price;
        }
    }

結果。

こんにちは Velocity!!

Fruits:
  apple
  banana

VelocityEngineのインスタンスを個別に作成する場合。

    @Test
    public void instance() throws IOException {
        Properties properties = new Properties();
        try (InputStream is =
                     getClass().getClassLoader().getResourceAsStream("velocity.properties")) {
            properties.load(is);
        }

        VelocityEngine velocity = new VelocityEngine();
        velocity.init(properties);

        VelocityContext context = new VelocityContext();
        context.put("name", "Velocity");

        List<Fruit> fruits = Arrays.asList(
                Fruit.create("apple", 600),
                Fruit.create("orange", 300),
                Fruit.create("banana", 800)
        );

        context.put("fruits", fruits);

        Template template = velocity.getTemplate("simple.vm");

        StringWriter writer = new StringWriter();
        template.merge(context, writer);
        System.out.println(writer.toString());
    }

まあ、ふつうに使えそうですね?

JSR-223サポート

最初、これがなにを言っているのかわかりませんでしたが、Apache VelocityのテンプレートをスクリプトとしてJSR-223のAPIで扱うことができます。

要するに、Apache VelocityがJSR-223のScriptEngineの実装を提供します。

これを使うには、「velocity-engine-scripting」を依存関係に追加します。

        <dependency>
            <groupId>org.apache.velocity</groupId>
            <artifactId>velocity-engine-scripting</artifactId>
            <version>2.0</version>
        </dependency>

「velocity-engine-core」は、scripting側の依存関係に含まれます。

コードの雛形。
src/test/java/org/littlewings/velocity/VelocityScriptingTest.java

package org.littlewings.velocity;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import javax.script.Bindings;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import javax.script.SimpleScriptContext;

import org.apache.velocity.script.VelocityScriptEngine;
import org.junit.jupiter.api.Test;

public class VelocityScriptingTest {
    static {
        System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "debug");
    }

    // ここに、テストを書く!
}

単純に使う場合には、こんな使い方になります。

    @Test
    public void simpleScripting() throws ScriptException {
        ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
        ScriptEngine scriptEngine = scriptEngineManager.getEngineByName("velocity");

        Bindings bindings = scriptEngine.createBindings();

        bindings.put("name", "Velocity");

        PrintWriter writer = (PrintWriter) scriptEngine.eval("Hello ${name}!!", bindings);
        writer.println();
    }

なんと、ScriptEngineManagerから、「velocity」でScriptEngineを取得できます。

        ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
        ScriptEngine scriptEngine = scriptEngineManager.getEngineByName("velocity");

テンプレートに渡す値は、Bindingsで設定します。

        Bindings bindings = scriptEngine.createBindings();

        bindings.put("name", "Velocity");

ScriptEngine#evalに、テンプレートをStringとして渡したり、Readerとして渡したりするとテンプレートを評価することができます。

        PrintWriter writer = (PrintWriter) scriptEngine.eval("Hello ${name}!!", bindings);
        writer.println();

ScriptEngine#evalの結果は、デフォルトではPrintWriterとして戻ってきます。

        PrintWriter writer = (PrintWriter) scriptEngine.eval("Hello ${name}!!", bindings);
        writer.println();

Writerが戻ってくるという動作自体は、変更できないようです。

結果。

Hello Velocity!!

動いてますね…。

ちなみに、この時のApache Velocityの設定はデフォルトのもので動作します。

[main] DEBUG org.apache.velocity - Default Properties resource: org/apache/velocity/runtime/defaults/velocity.properties
[main] DEBUG org.apache.velocity - ResourceLoader instantiated: org.apache.velocity.runtime.resource.loader.FileResourceLoader

このあたりをカスタマイズしたい場合は、こんな感じになります。

    @Test
    public void complex() throws IOException, ScriptException {
        ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
        ScriptEngine scriptEngine = scriptEngineManager.getEngineByName("velocity");

        Properties properties = new Properties();
        try (InputStream is =
                     getClass().getClassLoader().getResourceAsStream("velocity.properties")) {
            properties.load(is);
        }

        ScriptContext context = new SimpleScriptContext();
        Bindings bindings = scriptEngine.createBindings();
        context.setBindings(bindings, ScriptContext.ENGINE_SCOPE);
        context.setWriter(new StringWriter());

        bindings.put(VelocityScriptEngine.VELOCITY_PROPERTIES_KEY, properties);
        bindings.put(VelocityScriptEngine.FILENAME, "simple.vm");

        bindings.put("name", "Velocity");

        List<Fruit> fruits = Arrays.asList(
                Fruit.create("apple", 600),
                Fruit.create("orange", 300),
                Fruit.create("banana", 800)
        );

        bindings.put("fruits", fruits);

        try (BufferedReader reader =
                     new BufferedReader(new InputStreamReader(
                             getClass().getClassLoader().getResourceAsStream("simple.vm"),
                             StandardCharsets.UTF_8)
                     )) {
            StringWriter writer = (StringWriter) scriptEngine.eval(reader, context);

            System.out.println(writer.toString());
        }
    }

velocity.propertiesをPropertiesとして取り込んでおいて

        Properties properties = new Properties();
        try (InputStream is =
                     getClass().getClassLoader().getResourceAsStream("velocity.properties")) {
            properties.load(is);
        }

設定自体は、Bindingsにキー「VelocityScriptEngine.VELOCITY_PROPERTIES_KEY」で登録します。

        ScriptContext context = new SimpleScriptContext();
        Bindings bindings = scriptEngine.createBindings();
        context.setBindings(bindings, ScriptContext.ENGINE_SCOPE);
        context.setWriter(new StringWriter());

        bindings.put(VelocityScriptEngine.VELOCITY_PROPERTIES_KEY, properties);
        bindings.put(VelocityScriptEngine.FILENAME, "simple.vm");

        bindings.put("name", "Velocity");

        List<Fruit> fruits = Arrays.asList(
                Fruit.create("apple", 600),
                Fruit.create("orange", 300),
                Fruit.create("banana", 800)
        );

また、ScriptContext#setWriterでWriterは差し換えることが可能です。

        context.setWriter(new StringWriter());

ファイル名も指定できるのですが、これは実はロギングのタグとして扱われるだけです。

        bindings.put(VelocityScriptEngine.FILENAME, "simple.vm");

ScriptEngine#evalには、この場合はScriptContextを渡します。

        try (BufferedReader reader =
                     new BufferedReader(new InputStreamReader(
                             getClass().getClassLoader().getResourceAsStream("simple.vm"),
                             StandardCharsets.UTF_8)
                     )) {
            StringWriter writer = (StringWriter) scriptEngine.eval(reader, context);

            System.out.println(writer.toString());
        }

結果。

こんにちは Velocity!!

Fruits:
  apple
  banana

動きましたね?

ここまでの動作についてのドキュメント?申し訳程度に、このくらいが…。
Velocity Scripting

結局、スクリプト(というかテンプレート)はStringかReaderとして渡す必要があり、ResourceLoaderを設定しようが
関係なかったりします。どうにも中途半端感が…。Propertiesを指定できることはできますが、それ以外を設定するのには
有効、という程度です。

まとめ

2017年夏にひっそりとリリースされていた、Apache Velocity 2.0を試してみました。

Velocity Toolsはまだ出ていませんし、今更Velocity感もあるとは思いますが、興味があればどうぞ的な。

個人的には、汎用のテンプレートエンジンとしては扱いやすくて割と好きなんですけどね…。