かんがるーさんの日記

最近自分が興味をもったものを調べた時の手順等を書いています。今は Spring Boot をいじっています。

Spring Boot でメール送信する Web アプリケーションを作る ( その12 )( メール送信画面の作成6 )

概要

Spring Boot でメール送信する Web アプリケーションを作る ( その11 )( メール送信画面の作成5 ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • メール送信画面の作成
    • 前回から引き続きテストクラスを書きます。ただし今回は web/mailsend/MailsendService のテストクラスのみです ( いろいろ試していたら時間がかかりりました )。web/mailsend/MailsendController のテストクラスは次回にします。

ソフトウェア一覧

参考にしたサイト

  1. Java言語で固定要素のListを初期化する際のイディオム
    http://d.hatena.ne.jp/ryoasai/20101226/1293350856

    • TestDataResource クラスで List の定数を初期化する処理を実装する時に参考にしました。
  2. What is simplest way to read a file into String?
    http://stackoverflow.com/questions/3402735/what-is-simplest-way-to-read-a-file-into-string

    • ファイルの中身を String 型の変数に一気に読み込む方法を調査した時に参考にしました。
  3. guava-libraries
    https://code.google.com/p/guava-libraries/

  4. Java7 では Charset の指定がプチ改善されてます
    http://etc9.hatenablog.com/entry/20120218/1329587059

    • Charset クラスの文字コードの指定方法を調査した時に参考にしました。
  5. Java resource as file
    http://stackoverflow.com/questions/676097/java-resource-as-file

    • resources ディレクトリの下にあるファイルから File クラスのインスタンスを生成する方法を調査した時に参考にしました。
  6. データベースのテスト支援ツール DbUnit その3 DB参照編
    http://genesis-tdsg.blogspot.jp/2013/08/dbunit-db.html

    • DbUnit を使用してテーブルのデータをチェックする方法を調査した時に参考にしました。
  7. IntelliJ IDEAで空行インデントを保持する
    http://takashabe.hatenablog.com/entry/2013/05/17/125150

    • IntelliJ IDEA の空白を取り除く設定を解除する方法を調査した時に参照しました。

手順

web/mailsend/MailsendService のテストクラスの作成

  1. ksbysample-webapp-basic から以下のファイルをコピーします。

    ※src/test/java/ksbysample/webapp/basic の下のファイルは src/test/java/ksbysample/webapp/email の下へコピーします。また package も ksbysample.webapp.email へ変更します。

    • src/test/java/ksbysample/webapp/basic/test/TestDataResource.java
  2. src/test/java/ksbysample/webapp/email/test の下の TestDataResource.javaリンク先の内容 に変更します。

  3. テスト時の初期データを作成します。src/test/resources の下に testdata ディレクトリを作成します。

  4. src/test/resources/testdata の下に email.csv, email_item.csv, table-ordering.txt を新規作成します。作成後、リンク先の内容 に変更します。

    • テスト開始時はテーブルの中に何もデータが入っていないようにしますので、CSV ファイルにはデータは何も記述しません。
  5. テスト実行後の CSV ファイルとテーブルのデータを比較するクラスを作成します。src/test/java/ksbysample/webapp/email/test の下に TableDataAssert.java を新規作成します。作成後、リンク先の内容 に変更します。

  6. mailsendForm_simple.yml でテストした時にテーブルデータと比較する CSV ファイルを作成します。src/test/java/ksbysample/webapp/email/web/mailsend/testdata の下に simple ディレクトリ、minimum ディレクトリを作成します。

  7. src/test/java/ksbysample/webapp/email/web/mailsend/testdata/simple の下に email.csv, email_item.csv, table-ordering.txt を新規作成します。作成後、リンク先の内容 に変更します。

  8. src/test/java/ksbysample/webapp/email/web/mailsend/testdata/minimum の下に email.csv, email_item.csv, table-ordering.txt を新規作成します。作成後、リンク先の内容 に変更します。

  9. 生成されるメール本文をテキストファイルに記述しておいて、ファイルから String 型の変数に読み込んだ後 assertThat でチェックします。ファイルから String 型の変数に読み込む処理で Guava の com.google.common.io.Files の toString メソッドを使用します。

  10. build.gradle を リンク先の内容 に変更します。

  11. Gradle projects View の左上にある「Refresh all Gradle projects」アイコンをクリックして、変更した build.gradle の内容を反映します。

  12. テストで生成されるメール本文を記述したテキストファイルを作成します ( テストで assertThat で比較するために使用します ) 。src/test/resources/ksbysample/webapp/email/web/mailsend の下に mailsendForm_simple_mail.txt, mailsendForm_minimum_mail.txt を新規作成します。作成後、リンク先の内容 に変更します。

  13. src/main/java/ksbysample/webapp/email/web/mailsend の MailsendService.java から MailsendServiceTest.java を生成します。

  14. src/test/java/ksbysample/webapp/email/web/mailsend の下の MailsendServiceTest.javaリンク先の内容 に変更します。

  15. テストを実行します。MailsendServiceTest.java のクラス名にカーソルを移動した後、コンテキストメニューを表示して「Run 'MailsendServiceTest' with Coverage」メニューを選択します。

    テストが実行されますが、「MailsendFormの必須項目のみ値がセットされている場合」のテストが失敗しました。

    f:id:ksby:20150519232603p:plain

    原因は MailsendService クラスの saveEmail メソッドで getItem() で取得したリストの Null チェックをせずに処理していたため、NullPointerException が発生していたためでした。

  16. src/main/java/ksbysample/webapp/email/web/mailsend の下の MailsendService.javaリンク先の内容 に変更します。

  17. 再度テストを実行しますが、また「MailsendFormの必須項目のみ値がセットされている場合」のテストが失敗しました。

    f:id:ksby:20150519233702p:plain

    今回は以下の原因でした。

    • MAIL001MailHelper クラスの generateTextUsingVelocity メソッドで mailsendForm.getItem() で取得した値が null の場合に model.put("item", ...); を呼び出して "item" のデータをセットしていないため、Velocity のテンプレートの $item が置換されていませんでした。
    • MAIL001MailHelper クラスの generateTextUsingVelocity メソッドで Velocity のテンプレートに渡す変数 model に null のデータがセットされると、テンプレートの変数が置換されないためでした。Velocity では値が null のデータは置換しないようです ( テンプレートファイルの $xxx の記述がそのまま残ります )。
    • src/test/resources/ksbysample/webapp/email/web/mailsend の下の mailsendForm_minimum_mail.txt を作成した時に ":" の後に空白を1つ入れていたのですが、IntelliJ IDEA は行末の空白を自動で取り除くため Velocity で生成したメール本文と差異が発生していました。
    • 同じく src/main/resources/templates/mail/MAIL001 の下の MAIL001-body.vm の "内容 :" の後に入れていた空白も削除されていました。
  18. src/main/java/ksbysample/webapp/email/helper/mail の下の MAIL001MailHelper.javaリンク先の内容 に変更します。

  19. Velocity に渡す変数 model にセットされた値が null の場合には空文字列に置換する処理を追加します。src/main/java/ksbysample/webapp/email/util の下の VelocityUtils.javaリンク先の内容 に変更します。

  20. IntelliJ IDEA が行末の空白を自動で取り除かないよう設定を変更します。メインメニューから「File」-「Settings...」を選択します。

  21. 「Settings」ダイアログが表示されます。画面左側のツリーから「Editor」-「General」を選択後、画面右側の下部にある「Strip trailing spaces on Save」で「None」を選択し「OK」ボタンをクリックします。

    f:id:ksby:20150520045609p:plain

    ※あとで Spring Boot でメール送信する Web アプリケーションを作る ( その3 )( Project の作成 ) にこの手順を追加しておきます。

  22. src/test/resources/ksbysample/webapp/email/web/mailsend の下の mailsendForm_minimum_mail.txt で、各行の ":" の後に空白を1つ追加します。また "内容 : " の次に空行を1行追加します。

  23. src/test/resources/ksbysample/webapp/email/web/mailsend の下の mailsendForm_simple_mail.txt も "内容 :" の後に空白を1つ追加します。

  24. src/main/resources/templates/mail/MAIL001 の下の MAIL001-body.vm も "内容 :" の後に空白を1つ追加します。

  25. 再度テストを実行します。今度はテストが正常に終了して「All Test Passed」の文字が表示されました。

    f:id:ksby:20150520051227p:plain

次回は。。。

web/mailsend/MailsendController のテストクラスを書きます。commit 等も次回です。

ソースコード

build.gradle

dependencies {
    def jdbcDriver = "org.postgresql:postgresql:9.4-1201-jdbc41"

    // spring-boot-gradle-plugin によりバージョン番号が自動で設定されるもの
    // Appendix E. Dependency versions ( http://docs.spring.io/spring-boot/docs/current/reference/html/appendix-dependency-versions.html ) 参照
    compile("org.springframework.boot:spring-boot-starter-web")
    compile("org.springframework.boot:spring-boot-starter-thymeleaf")
    compile("org.springframework.boot:spring-boot-starter-data-jpa")
    compile("org.springframework.boot:spring-boot-starter-velocity")
    compile("org.springframework.boot:spring-boot-starter-mail")
    compile("org.codehaus.janino:janino")
    testCompile("org.springframework.boot:spring-boot-starter-test")
    testCompile("org.yaml:snakeyaml")

    // spring-boot-gradle-plugin によりバージョン番号が自動で設定されないもの
    compile("${jdbcDriver}")
    compile("org.seasar.doma:doma:2.2.0")
    compile("org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16")
    compile("org.apache.commons:commons-lang3:3.4")
    compile("org.projectlombok:lombok:1.16.2")
    compile("com.google.guava:guava:18.0")
    testCompile("org.dbunit:dbunit:2.5.0")
    testCompile("org.subethamail:subethasmtp:3.1.7")
    testCompile("com.icegreen:greenmail:1.4.1")

    // for Doma-Gen
    domaGenRuntime("org.seasar.doma:doma-gen:2.2.0")
    domaGenRuntime("${jdbcDriver}")
}
  • compile("com.google.guava:guava:18.0") を追加します。

TestDataResource.java

package ksbysample.webapp.email.test;

import org.dbunit.database.DatabaseConnection;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.database.QueryDataSet;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.ReplacementDataSet;
import org.dbunit.dataset.csv.CsvDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSetBuilder;
import org.dbunit.operation.DatabaseOperation;
import org.junit.rules.ExternalResource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;
import java.io.File;
import java.io.FileOutputStream;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.List;

@Component
public class TestDataResource extends ExternalResource {

    private final List<String> BACKUP_TABLES = Arrays.asList(
            "email"
            , "email_item"
    );

    private final String BACKUP_FILE_NAME = "ksbyemail_backup";

    @Autowired
    private DataSource dataSource;

    private File backupFile;

    @Override
    protected void before() throws Exception {
        IDatabaseConnection conn = null;
        try {
            conn = new DatabaseConnection(dataSource.getConnection());

            // バックアップを取得する
            QueryDataSet partialDataSet = new QueryDataSet(conn);
            for (String backupTable: BACKUP_TABLES) {
                partialDataSet.addTable(backupTable);
            }
            ReplacementDataSet replacementDatasetBackup = new ReplacementDataSet(partialDataSet);
            replacementDatasetBackup.addReplacementObject("", "[null]");
            backupFile = File.createTempFile(BACKUP_FILE_NAME, "xml");
            try (FileOutputStream fos = new FileOutputStream(backupFile)) {
                FlatXmlDataSet.write(replacementDatasetBackup, fos);
            }

            // テストデータに入れ替える
            IDataSet dataSet = new CsvDataSet(new File("src/test/resources/testdata"));
            ReplacementDataSet replacementDataset = new ReplacementDataSet(dataSet);
            replacementDataset.addReplacementObject("[null]", null);
            DatabaseOperation.CLEAN_INSERT.execute(conn, replacementDataset);
        }
        finally {
            if (conn != null) conn.close();
        }
    }

    @Override
    protected void after() {
        try {
            IDatabaseConnection conn = null;
            try {
                conn = new DatabaseConnection(dataSource.getConnection());

                // バックアップからリストアする
                if (backupFile != null) {
                    IDataSet dataSet = new FlatXmlDataSetBuilder().build(backupFile);
                    ReplacementDataSet replacementDatasetRestore = new ReplacementDataSet(dataSet);
                    replacementDatasetRestore.addReplacementObject("[null]", null);
                    DatabaseOperation.CLEAN_INSERT.execute(conn, replacementDatasetRestore);
                }
            } finally {
                if (backupFile != null) {
                    Files.delete(backupFile.toPath());
                    backupFile = null;
                }
                try {
                    if (conn != null) conn.close();
                } catch (Exception ignored) {}
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}
  • before メソッドの以下の点を変更します。
    • partialDataSet.addTable(...) でバックアップするテーブルを email, email_item に変更します。private final List<String> BACKUP_TABLES を追加し、before メソッド内で for 文で処理するようにします。
    • for 文ではなく Stream APIBACKUP_TABLES.stream().forEach(backupTable -> partialDataSet.addTable(backupTable)); と処理するよう実装したらなぜか AmbiguousTableNameException を catch しないとダメというエラーが出ました。catch 文をわざわざ追加したくなかったので for 文で処理するようにしたのですが、なぜ Stream API を使用するとこうなるのか原因が分かりませんでした。。。
    • File.createTempFile(...) で指定するバックアップファイル名を "ksbyemail_backup" へ変更します。private final String BACKUP_FILE_NAME = "ksbyemail_backup"; を追加し、File.createTempFile(...) の引数には BACKUP_FILE_NAME を渡します。

testdata/email.csv, email_item.csv, table-ordering.txt

■email.csv

"email_id","from_addr","to_addr","subject","name","sex","type","naiyo"

■email_item.csv

"email_item_id","email_id","item"

■table-ordering.txt

email
email_item

TableDataAssert.java

package ksbysample.webapp.email.test;

import org.dbunit.Assertion;
import org.dbunit.DatabaseUnitException;
import org.dbunit.database.DatabaseConnection;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.dataset.DataSetException;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.ITable;
import org.dbunit.dataset.ReplacementDataSet;
import org.dbunit.dataset.filter.DefaultColumnFilter;

import javax.sql.DataSource;
import java.sql.SQLException;

public class TableDataAssert {

    private final IDataSet dataSet;

    private final DataSource dataSource;

    public TableDataAssert(IDataSet dataSet, DataSource dataSource) {
        ReplacementDataSet replacementDataset = new ReplacementDataSet(dataSet);
        replacementDataset.addReplacementObject("[null]", null);
        this.dataSet = replacementDataset;
        this.dataSource = dataSource;
    }

    public void assertEquals(String tableName, String[] columnNames) throws DatabaseUnitException, SQLException {
        ITable expected = expectedTable(tableName, columnNames);
        ITable actual = actualTable(tableName, columnNames);
        Assertion.assertEquals(expected, actual);
    }

    private ITable expectedTable(String tableName, String[] columnNames) throws DataSetException {
        ITable table = dataSet.getTable(tableName);
        if (columnNames != null) {
            table = DefaultColumnFilter.excludedColumnsTable(table, columnNames);
        }
        return table;
    }

    private ITable actualTable(String tableName, String[] columnNames) throws DatabaseUnitException, SQLException {
        IDatabaseConnection conn = new DatabaseConnection(this.dataSource.getConnection());
        ITable table = conn.createDataSet().getTable(tableName);
        if (columnNames != null) {
            table = DefaultColumnFilter.excludedColumnsTable(table, columnNames);
        }
        return table;
    }

}

mailsend/test/data/simple/email.csv, email_item.csv, table-ordering.txt

■email.csv

"email_id","from_addr","to_addr","subject","name","sex","type","naiyo"
"","test@sample.com","xxx@yyy.zzz","テスト","田中 太郎","1","1","これはテストです。"

■email_item.csv

"email_item_id","email_id","item"
"","","101"
"","","102"
"","","103"

■table-ordering.txt

email
email_item

mailsend/test/data/minimum/email.csv, email_item.csv, table-ordering.txt

■email.csv

"email_id","from_addr","to_addr","subject","name","sex","type","naiyo"
"","test@sample.com","xxx@yyy.zzz","テスト","[null]","[null]","[null]","[null]"
  • null の場合には "" ではなく "[null]" と書くこと。[null]→null への変換は ksbysample.webapp.email.test.TableDataAssert クラスのコンストラクタで行っています。

■email_item.csv

"email_item_id","email_id","item"

■table-ordering.txt

email
email_item

mailsendForm_simple_mail.txt, mailsendForm_minimum_mail.txt

■mailsendForm_simple_mail.txt

氏名 : 田中 太郎
性別 : 男性
項目 : 資料請求
商品 : 商品1, 商品2, 商品3
内容 :
これはテストです。

■mailsendForm_minimum_mail.txt

氏名 :
性別 :
項目 :
商品 :
内容 :

MailsendServiceTest.java

package ksbysample.webapp.email.web.mailsend;

import com.google.common.io.Files;
import ksbysample.webapp.email.Application;
import ksbysample.webapp.email.test.MailServerResource;
import ksbysample.webapp.email.test.TableDataAssert;
import ksbysample.webapp.email.test.TestDataResource;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.csv.CsvDataSet;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.yaml.snakeyaml.Yaml;

import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import javax.sql.DataSource;
import java.io.File;
import java.nio.charset.StandardCharsets;

import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebAppConfiguration
public class MailsendServiceTest {

    private final MailsendForm mailsendFormSimple
            = (MailsendForm) new Yaml().load(getClass().getResourceAsStream("mailsendForm_simple.yml"));
    private final MailsendForm mailsendFormMinimum
            = (MailsendForm) new Yaml().load(getClass().getResourceAsStream("mailsendForm_minimum.yml"));

    @Rule
    @Autowired
    public TestDataResource testDataResource;

    @Rule
    @Autowired
    public MailServerResource mailServer;

    @Autowired
    private DataSource dataSource;

    @Autowired
    private MailsendService mailsendService;

    @Test
    public void MailsendFormの全てに値がセットされている場合() throws Exception {
        mailsendService.saveAndSendEmail(mailsendFormSimple);

        // email, email_item テーブルに保存されているか確認する
        IDataSet dataSet = new CsvDataSet(new File("src/test/resources/ksbysample/webapp/email/web/mailsend/testdata/simple"));
        TableDataAssert tableDataAssert = new TableDataAssert(dataSet, dataSource);
        tableDataAssert.assertEquals("email", new String[]{"email_id"});
        tableDataAssert.assertEquals("email_item", new String[]{"email_item_id", "email_id"});

        // メールが送信されているか確認する
        assertThat(mailServer.getMessagesCount(), is(1));
        MimeMessage receiveMessage = mailServer.getFirstMessage();
        assertThat(receiveMessage.getFrom()[0], is(new InternetAddress("test@sample.com")));
        assertThat(receiveMessage.getAllRecipients()[0], is(new InternetAddress("xxx@yyy.zzz")));
        assertThat(receiveMessage.getSubject(), is("テスト"));
        String mailsendFormSimpleMail
                = Files.toString(new File(getClass().getResource("mailsendForm_simple_mail.txt").toURI()), StandardCharsets.UTF_8);
        assertThat(receiveMessage.getContent(), is(mailsendFormSimpleMail));
    }

    @Test
    public void MailsendFormの必須項目のみ値がセットされている場合() throws Exception {
        mailsendService.saveAndSendEmail(mailsendFormMinimum);

        // email, email_item テーブルに保存されているか確認する
        IDataSet dataSet = new CsvDataSet(new File("src/test/resources/ksbysample/webapp/email/web/mailsend/testdata/minimum"));
        TableDataAssert tableDataAssert = new TableDataAssert(dataSet, dataSource);
        tableDataAssert.assertEquals("email", new String[]{"email_id"});
        tableDataAssert.assertEquals("email_item", new String[]{"email_item_id", "email_id"});

        // メールが送信されているか確認する
        assertThat(mailServer.getMessagesCount(), is(1));
        MimeMessage receiveMessage = mailServer.getFirstMessage();
        assertThat(receiveMessage.getFrom()[0], is(new InternetAddress("test@sample.com")));
        assertThat(receiveMessage.getAllRecipients()[0], is(new InternetAddress("xxx@yyy.zzz")));
        assertThat(receiveMessage.getSubject(), is("テスト"));
        String mailsendFormMinimumMail
                = Files.toString(new File(getClass().getResource("mailsendForm_minimum_mail.txt").toURI()), StandardCharsets.UTF_8);
        assertThat(receiveMessage.getContent(), is(mailsendFormMinimumMail));
    }

}

MailsendService.java

    public void saveEmail(MailsendForm mailsendForm) {
        // email テーブルに保存する
        Email email = new Email();
        BeanUtils.copyProperties(mailsendForm, email);
        emailDao.insert(email);

        // email_item テーブルに保存する
        EmailItem emailItem = new EmailItem();
        if (mailsendForm.getItem() != null) {
            for (String item : mailsendForm.getItem()) {
                emailItem.setEmailItemId(null);
                emailItem.setEmailId(email.getEmailId());
                emailItem.setItem(item);
                emailItemDao.insert(emailItem);
            }
        }
    }
  • null チェックの if 文 if (mailsendForm.getItem() != null) { ... } を追加します。

MAIL001MailHelper.java

    private String generateTextUsingVelocity(MailsendForm mailsendForm) {
        Constant constant = Constant.getInstance();
        Map<String, Object> model = new HashMap<>();
        model.put("name", mailsendForm.getName());
        model.put("sex", constant.SEX_MAP.get(mailsendForm.getSex()));
        model.put("type", constant.TYPE_MAP.get(mailsendForm.getType()));

        String itemList = null;
        if (mailsendForm.getItem() != null) {
            itemList = mailsendForm.getItem().stream()
                    .map(constant.ITEM_MAP::get)
                    .collect(Collectors.joining(", "));
        }
        model.put("item", itemList);

        model.put("naiyo", mailsendForm.getNaiyo());
        return velocityUtils.merge(this.templateLocation, model);
    }
  • model.put("item", itemList); が必ず呼び出されるよう if (mailsendForm.getItem() != null) { ... } の外に出します。
  • ただし mailsendForm.getItem() の結果が null の場合には null がセットされるようにしたいので、if 文の外に String itemList = null; を追加します。

VelocityUtils.java

package ksbysample.webapp.email.util;

import org.apache.velocity.app.VelocityEngine;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.ui.velocity.VelocityEngineUtils;

import java.util.HashMap;
import java.util.Map;

@Component
public class VelocityUtils {

    @Autowired
    private VelocityEngine velocityEngine;

    @Value("${spring.velocity.charset}")
    private String charset;

    public String merge(String templateLocation, Map<String, Object> model) {
        // model 内の値が null のデータは空文字列に入れ替える
        Map<String, Object> modelForVelocity = new HashMap<>();
        for (Map.Entry<String, Object> e : model.entrySet()) {
            modelForVelocity.put(e.getKey(), (e.getValue() == null ? "" : e.getValue()));
        }

        return VelocityEngineUtils.mergeTemplateIntoString(this.velocityEngine, templateLocation, charset, modelForVelocity);
    }

}
  • model 内の値が null の場合に空文字列に入れ替える処理を追加します。model のデータは変更したくなかったので、置換結果は別の変数に保存するようにしました。
  • VelocityEngineUtils.mergeTemplateIntoString(...) の第4引数に渡す変数を model → modelForVelocity に変更します。

履歴

2015/05/20
初版発行。