はじめに
一口にテンプレートエンジンと言っても、用途はさまざまです。例えば、動的にHTMLページを生成するためにテンプレートエンジンを利用することが一般的でしょう。また、テンプレートエンジンにも数多くのツールが存在します。Java言語において特に有名なものはJakarta Velocityですが、後発のテンプレートエンジンとしてEclipse Modeling Framework プロジェクトが作成したJET(Java Emitter Templates)というものがあります。本記事では、JETの簡単な使い方と、コード生成のサンプルとしてExcel-Javaバインディングツールを作成します。
対象読者
Javaプログラミングを行ったことがある方。
必要な環境
本記事は以下の環境で動作を確認しています。
また、サンプルを実行するためには以下のライブラリが必要です。
- Jakarta POI 2.5.1 Final
- Jakarta Commons BeanUtils 1.7.0
- Jakarta Commons Logging 1.0.4(BeanUtilsのために必要)
JETの概要
テンプレートエンジンとは
テンプレートエンジンとは、テキストで記述されたテンプレートとデータを統合(マージ)して新しいテキストを生成する仕組みです。記述するテンプレートの書式はツール、または標準によって決められています。例えば、Jakarta Velocityであれば、テンプレートの書式はVTL(Velocity Template Language)という仕様が決められており、マージするデータは任意のJavaオブジェクトとなっています。
JETは何が違うのか
では、本記事の主題であるJETと、Velocityなどの他のテンプレートエンジンとの違いを見てみましょう。
エンジンの使い方
Velocityでは、テキストで記述されたテンプレートファイルをJava VM上にロードし、任意のオブジェクトとマージします。マージした結果をテキストファイルなどに出力します。
VelocityContext context = new VelocityContext(); /* * マージするデータの生成. */ context.put( "name", new String("Velocity") ); /* * テンプレートの読み込み. */ Template template = Velocity.getTemplate("mytemplate.vm"); /* * 出力先の作成. */ StringWriter sw = new StringWriter(); /* * データとテンプレートのマージ. */ template.merge( context, sw );
これに対してJETは、テキストで記述されたテンプレートファイルから、それに指定されているフォーマットでテキストを出力する「Javaソースファイル」を生成します。これをコンパイルして作成したクラスのgenerate
を実行することで、テンプレートとデータをマージすることが出来ます。
/* * マージするデータの生成. */ String data = "JET"; /* * テンプレート出力オブジェクトの生成. */ HelloJETTemplate template = new HelloJETTemplate(); /* * データとテンプレートのマージ. */ template.generate(data);
これは、JSPの構造に酷似しています。JSPの仕様では、アプリケーションサーバによってJSPファイルがJavaクラスにコンパイルされ、実行されます。同様にJETでは、JETエンジンによってテンプレートファイルがJavaソースに変換され、実行されます。両者の違いは、自動的に生成されるJavaソースが隠蔽されるか、公開されるかという点です。
テンプレートの記法
Velocityでは、テンプレートの記述をVTLで記述します。
Hello! $name
JETでは、JSPのサブセットであるタグベースの記法を採用しています。
<%@ jet package="hello" class="HelloJETTemplate"%> Hello! <%=argument%>
JETのテンプレートファイルの書式はJSPのサブセットであるため、Java技術者にとって学習しやすいといえます。しかし、Velocity愛好者にとっては貧弱な文法に見えるかもしれません。
JETの利点
Javaソースが生成されるため、テンプレートファイルの置き場所やパスの問題に困ることはありません。生成したJavaソースをコンパイルすれば、テンプレートエンジンが出来上がります。また、生成されるJavaソースは特定のライブラリに依存せず、インタフェースや継承を行わないPOJO(Plain Old Java Object)になります。よって、テンプレートエンジンの利用者は、マージのために特別な処理や依存ライブラリを用意する必要はありません。
JETチュートリアル
下準備
JETはEclipseで動かすことを前提にしています。まずはEclipseを起動して、適当なJavaプロジェクトを作成してください。ここでは「helloJET」というプロジェクト名にしました。その後、[File]→[New]→[Others]→[Java Emitter Templates]から[Convert Projects to JET Projects]を選択してください。
これは既存のプロジェクトを、JETファイルを解釈できるプロジェクトに変更するものです。ここでは、目的のプロジェクトとして先ほど作成した「helloJET」プロジェクトを選択し、[Finish]を押します。すると、プロジェクトのルート以下に「templates」ディレクトリが生成されます。次に、「helloJET」プロジェクトを右クリックし、[properties]を選択します。ダイアログボックスが表示されるので、プロパティツリーの中から[JET Settings]を選択し、[Source Container]にプロジェクトのソースフォルダを入力して、[OK]を押します。
これで、下準備は完了になります。
JET Hello World!
JETを使う準備は整ったので、早速JETファイルを記述してみましょう。まずは、最も簡単なHello Worldを出力するテンプレートを書いてみます。「templates」フォルダの下に「hello.jet」という名前で以下の内容のファイルを作成してください。
<%@ jet package="hello" class="HelloJETTemplate"%> Hello! World
すると、ソースフォルダの中に自動的にhello.HelloJETTemplate
クラスが生成されます。「templates」フォルダ以下に配置された拡張子jetのファイルはJET Builderによって自動的に解釈実行され、その結果はJET Settingで指定したフォルダ以下に出力されます。
jet
である必要はありません。拡張子の末尾がjet
にさえなっていればjavajet
でもxmljet
でもhogehogejet
でもコンパイル可能です。テンプレートの実行と生成されたソース
適当なクラスを作り、生成したHelloJETTemplate
のgenerate
メソッドを呼び出してください。メソッドの返却値として「Hello! World」というStringが返却されます。
生成されたHelloJETTemplate
クラスの中身はどうなっているでしょうか。中身を見ると、非常に単純な仕掛けになっていることがわかります。
package hello; public class HelloJETTemplate { protected static String nl; public static synchronized HelloJETTemplate create( String lineSeparator) { nl = lineSeparator; HelloJETTemplate result = new HelloJETTemplate(); nl = null; return result; } protected final String NL = nl == null ? (System.getProperties().getProperty("line.separator")) : nl; protected final String TEXT_1 = "Hello! World"; public String generate(Object argument) { StringBuffer stringBuffer = new StringBuffer(); stringBuffer.append(TEXT_1); return stringBuffer.toString(); } }
テンプレート内に記述されたテキストはそのまま定数として保持され、generate
メソッドでは定数をStringBuffer
に格納して返却するだけです。
データをマージしてみる
次に、「hello.jet」テンプレートを変更して、データの入力を受け取るようにします。
<%@ jet package="hello" class="HelloJETTemplate"%> Hello! <%=argument%>
変更して保存すると、そのままHelloJETTemplate
クラスに変更が反映されます。
<%=XXX%>
とすることで、変数の値を出力することが出来ます。これもJSPと同様です。
argument
とstringBuffer
の二種類があります。argument
はテンプレートに渡されるデータを格納した変数で、stringBuffer
はテンプレート内部からマージした文字列を生成するStringBuffer
を参照するために用意されている変数です。テンプレートの制御構造
テンプレートの内部で、繰り返しや条件分岐と言った制御構造を利用することができます。やり方はJSPと一緒で、<%
と%>
の間に任意のJavaの式を記述します。
<%@ jet package="hello" class="HelloJETTemplate" imports="java.util.Collection java.util.Iterator"%> <% Collection data = (Collection) argument; %> <% if (data.size() != 0) { %> <% for (Iterator iter = data.iterator(); iter.hasNext(); ) { %> Hello! <%= iter.next() %> <% } %> <% } else { %> Empty! <% } %>
imports
属性に利用するパッケージを記述します。複数利用する場合はスペース区切りでパッケージを記述していきます。JSPの仕様とは微妙に異なる点に注意してください。JETのまとめ
JETの使い方や利点を列挙します。
- 「templates」フォルダに拡張子の末尾がjetのファイルを置くだけで自動的にクラス生成。
- テンプレートを使う人に依存ライブラリは不要。
- テンプレートを使う人にJETファイルは不要。
- テンプレートはインスタンス化され、
generate
メソッド実行だけで利用できる。 - テンプレートの書式はほとんどJSPと一緒。
以上、テンプレートを使う側は何の配慮も要りません。ただのPOJOのメソッドを呼び出すだけです。テンプレートを作る側も、記述したファイルを所定のフォルダに置くだけです。一言で表すならば、JETは簡単だということです。
コード生成のサンプルの概要
それでは、本稿でサンプルとして作成するExcel-Javaバインディングツールの簡単な説明を行います。
バインディングツールは、決められたスキーマ定義に従って、ExcelファイルからJavaソースを出力するツールです。出力したJavaソースを使うことで、簡単にExcelファイルへの読み込み、書き込みが可能になります。
サンプルファイルのインポート
サンプルファイルはZIP形式で圧縮してあります。Eclipseのプロジェクトを固めたものですので、そのままプロジェクトとしてインポートしてください。
このプロジェクトには、Excelで記述されたスキーマ定義を読み込んでJavaソースを出力するサンプルと、生成済みのJavaソースを使って実際にExcelファイルを読み書きするサンプルが含まれています。
スキーマ定義の読み込み
jet.excel.example.SchemaLoader
クラスを実行してください。コマンドライン引数として
- Excelスキーマファイルのパス
- Javaソースの出力先
- 生成するクラスのパッケージ名
- 生成するクラスのクラス名
を指定してください。
実行に成功すると、指定した出力先にJavaソースと「template.xls」が生成されます。
Excelファイルの読み書き
すでに生成済みの物として、試験項目風のExcelファイルを読み書きするクラスがあります。
jet.excel.sample.generated.Example
クラスを実行してください。
public static void main(String[] args) throws Exception { /* * dataフォルダ以下のsampleData.xlsを読み込む. */ TestCaseBeansDocument document = new TestCaseBeansDocument(); List<TestCaseBeans> data = document.read(new File("data/sampleData.xls")); /* * 読み込んだデータを標準出力に表示. */ for (TestCaseBeans d : data) { System.out.print(d.getId()); System.out.print(", " + d.getTarget()); System.out.print(", " + d.getMethod()); System.out.println(", " + d.getDescription()); } /* * 新しいデータを追加. */ TestCaseBeans newOne = new TestCaseBeans(); newOne.setId("NEW-0001"); newOne.setTarget("Example"); newOne.setMethod("main"); newOne.setDescription("新しく追加されたテスト項目"); newOne.setOption("Excelシートに追加が反映されること"); data.add(newOne); /* * Excelファイルに書き出す */ FileOutputStream stream = new FileOutputStream("data/outputData.xls"); document.write(data, stream); stream.flush(); stream.close(); }
上記のExcelを読み込んで次の結果を表示します。
UNIT-0001, SchemaLoader, generate, すべての引数を正常に入力する UNIT-0002, SchemaLoader, generate, 出力先の指定をnullにする UNIT-0003, SchemaLoader, generate, 生成するクラスのパッケージ名を nullにする
そして、以下のようなExcelファイルを出力します。
サンプルのファイル構成
「jet.excel.example」パッケージ
Excelファイルを読み込んでJavaソースを生成するためのクラスが格納されています。
BindingData
クラスColumnBindingData
クラスSchemaLoader
クラス
「jet.excel.example.generated」パッケージ
JETによって自動生成されたクラスを格納するパッケージです。
TestCaseBeans
クラスTestCaseBeansDocument
クラス
TestCaseBeans
の生成、TestCaseBeans
からExcelファイルの出力を行う入出力クラスです。ツールによって自動生成されます。「jet.excel.example.template」パッケージ
JETファイルから自動生成されたJETを格納するパッケージです。
BeansBindingTemplate
クラスExcelBindingTemplate
クラス
使用したExcelスキーマ
今回の例は非常に貧弱なスキーマしか記述することが出来ません。
- シートの構造
- マッピングの定義
列名1 | 列名2 | 列名3 | ... |
データ1 | データ2 | データ3 | ... |
データ1 | データ2 | データ3 | ... |
... | ... | ... | ... |
ID | 名前 | 値 | 補足 |
#id | #name | #value | #option |
作成したテンプレート
作成したテンプレートファイルを提示します。
JavaBeans生成テンプレート
ColumnBindingData
オブジェクトの内容に従って、JavaBeansを生成するテンプレートです。BindingData
からColumnBindingData
のListを取り出し、Listの数だけフィールド、Getter/Setterを生成しています。
<%@ jet package="jet.excel.sample.template" class="BeansBindingTemplate" imports="java.util.* jet.excel.sample.*" %> <% BindingData data = (BindingData) argument; %> package <%=data.getPackageName()%>; public class <%=data.getClassName()%> { <% for (ColumnBindingData column : data.getColumnBindings()) { %> private String <%=column.getName()%>; public String get<%=column.getName(). substring(0,1).toUpperCase() + column.getName().substring(1)%>() { return <%=column.getName()%>; } public void set<%=column.getName(). substring(0,1).toUpperCase() + column.getName(). substring(1)%>(String <%=column.getName()%>) { this.<%=column.getName()%> = <%=column.getName()%>; } <% }%> }
Excel入出力クラステンプレート
Excelファイルの入出力と、Excelの行とJavaBeansのマッピングを行うクラスを出力します。マッピングはセルの番号とセルのスタイルへの参照とをJavaBeansのフィールド名と関連付けてHashMap
に保存することで実現しています。
<%@ jet package="jet.excel.sample.template" class="ExcelBindingTemplate" imports="java.util.* jet.excel.sample.*" %> <% BindingData data = (BindingData) argument; %> package <%=data.getPackageName()%>; ・・・中略・・・ public class <%=data.getClassName()%>Document { private static final short SHEET_NUMBER = 0; /* * マッピングを開始する行番号を設定する. */ private static final int ROW_OFFSET = <%=data.getRowOffset()%>; /* * マッピングを開始するセル番号を設定する. */ private static final short COLUMN_OFFSET = <%=data.getCellOffset()%>; private Map<Short, String> nameMapping = new HashMap<Short, String>(); private Map<Short, Short> styleMapping = new HashMap<Short, Short>(); public <%=data.getClassName()%>Document() { init(); } private void init() { /* * セル番号と対応するフィールド名、スタイル番号を * HashMapに登録. */ <% for (ColumnBindingData column : data.getColumnBindings()) { %> nameMapping.put((short) <%=column.getIndex()%>, "<%=column.getName()%>"); styleMapping.put((short) <%=column.getIndex()%>, (short)<%=column.getStyleIndex()%>); <% }%> } ・・・中略・・・ public void write( List<<%=data.getClassName()%>> list, FileOutputStream stream) throws IOException, IllegalAccessException, InvocationTargetException, NoSuchMethodException { HSSFWorkbook workbook = loadTemplate(); HSSFSheet sheet = workbook.getSheetAt(SHEET_NUMBER); int i = 0; for (<%=data.getClassName()%> binding : list) { sheet.shiftRows(ROW_OFFSET + i, sheet.getLastRowNum(), 1); HSSFRow row = sheet.createRow(ROW_OFFSET + i); /* * HashMapの中身を見てセル番号と対応する * フィールドの値とスタイルを設定. */ for (short num : nameMapping.keySet()) { HSSFCell cell = row.createCell((short) (COLUMN_OFFSET + num)); String name = nameMapping.get(num); Object value = BeanUtils.getProperty(binding, name); cell.setEncoding(HSSFCell.ENCODING_UTF_16); setCellValue(cell, value); HSSFCellStyle style = workbook.getCellStyleAt(styleMapping.get(num)); cell.setCellStyle(style); } i++; } workbook.write(stream); } }
まとめ
今回のExcelファイルの読み書きのように、定型的な処理が含まれ、かつ状況に応じて多少の違いがある場合に、コード生成は大きな力を発揮します。今回のExcelバインディングツールでは試験項目表を例にしましたが、読み込んだ情報からさらに実際のJUnitのテストコードを出力することもできるかもしれません。どのようなコード生成ツールを選択するかはケースバイケースですが、Eclipseプラグインのように、Eclipseと密接に絡んだプログラムを作成する場合にJETは妥当な選択だと思います。
参考資料
以下の資料を参考にしました。筆者はPOIが初めてだったので勝手がわからなかったのですが、CodeZineに投稿されている他の記事ではPOIやVelocityについて詳しく解説されています。