Shoeisha Technology Media

CodeZine(コードジン)

特集ページ一覧

Eclipse JETを使用した簡単なコード生成の実例

テンプレートエンジンのJETを利用してExcelファイルの読み書きを行う方法

  • LINEで送る
  • このエントリーをはてなブックマークに追加

多くの方がJavaでのテンプレートエンジンとしてJakarta Velocityを利用されていることと思います。本稿では、Velocityに比べてマイナーなJET(Java Emitter Template)を紹介し、コード生成のサンプルとして簡単なExcel-Javaバインディングツールを作成します。

はじめに

 一口にテンプレートエンジンと言っても、用途はさまざまです。例えば、動的にHTMLページを生成するためにテンプレートエンジンを利用することが一般的でしょう。また、テンプレートエンジンにも数多くのツールが存在します。Java言語において特に有名なものはJakarta Velocityですが、後発のテンプレートエンジンとしてEclipse Modeling Framework プロジェクトが作成したJET(Java Emitter Templates)というものがあります。本記事では、JETの簡単な使い方と、コード生成のサンプルとしてExcel-Javaバインディングツールを作成します。

対象読者

 Javaプログラミングを行ったことがある方。

必要な環境

 本記事は以下の環境で動作を確認しています。

 また、サンプルを実行するためには以下のライブラリが必要です。

JETの概要

テンプレートエンジンとは

 テンプレートエンジンとは、テキストで記述されたテンプレートとデータを統合(マージ)して新しいテキストを生成する仕組みです。記述するテンプレートの書式はツール、または標準によって決められています。例えば、Jakarta Velocityであれば、テンプレートの書式はVTL(Velocity Template Language)という仕様が決められており、マージするデータは任意のJavaオブジェクトとなっています。

JETは何が違うのか

 では、本記事の主題であるJETと、Velocityなどの他のテンプレートエンジンとの違いを見てみましょう。

エンジンの使い方

 Velocityでは、テキストで記述されたテンプレートファイルをJava VM上にロードし、任意のオブジェクトとマージします。マージした結果をテキストファイルなどに出力します。

Velocityの簡単な使い方
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を実行することで、テンプレートとデータをマージすることが出来ます。

JETの簡単な使い方
/*
 * マージするデータの生成.
 */
String data = "JET";
/*
 * テンプレート出力オブジェクトの生成.
 */
HelloJETTemplate template = new HelloJETTemplate();
/*
 * データとテンプレートのマージ.
 */

template.generate(data);

 これは、JSPの構造に酷似しています。JSPの仕様では、アプリケーションサーバによってJSPファイルがJavaクラスにコンパイルされ、実行されます。同様にJETでは、JETエンジンによってテンプレートファイルがJavaソースに変換され、実行されます。両者の違いは、自動的に生成されるJavaソースが隠蔽されるか、公開されるかという点です。

テンプレートの記法

 Velocityでは、テンプレートの記述をVTLで記述します。

Velocityのテンプレート
Hello! $name

 JETでは、JSPのサブセットであるタグベースの記法を採用しています。

JETのテンプレート
<%@ 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プロジェクトウィザード
JETプロジェクトウィザード

 これは既存のプロジェクトを、JETファイルを解釈できるプロジェクトに変更するものです。ここでは、目的のプロジェクトとして先ほど作成した「helloJET」プロジェクトを選択し、[Finish]を押します。すると、プロジェクトのルート以下に「templates」ディレクトリが生成されます。次に、「helloJET」プロジェクトを右クリックし、[properties]を選択します。ダイアログボックスが表示されるので、プロパティツリーの中から[JET Settings]を選択し、[Source Container]にプロジェクトのソースフォルダを入力して、[OK]を押します。

ソース出力先の設定
ソース出力先の設定

 これで、下準備は完了になります。

下準備完了後のプロジェクト
下準備完了後のプロジェクト

JET Hello World!

 JETを使う準備は整ったので、早速JETファイルを記述してみましょう。まずは、最も簡単なHello Worldを出力するテンプレートを書いてみます。「templates」フォルダの下に「hello.jet」という名前で以下の内容のファイルを作成してください。

JETのファイル
<%@ jet package="hello" class="HelloJETTemplate"%>
Hello! World
JETファイルの作成と配置
JETファイルの作成と配置

 すると、ソースフォルダの中に自動的にhello.HelloJETTemplateクラスが生成されます。「templates」フォルダ以下に配置された拡張子jetのファイルはJET Builderによって自動的に解釈実行され、その結果はJET Settingで指定したフォルダ以下に出力されます。

JETディレクティブ
 JETファイルの先頭のタグはJETディレクティブと呼ばれるものです。このタグ内でテンプレートエンジンとして出力するクラスのパッケージやクラス名、テンプレート内で使用できるクラスのインポート宣言などを行います。JETディレクティブはJETファイルに必ず記述する必要があります。また、JETファイルは拡張子は必ずしもjetである必要はありません。拡張子の末尾がjetにさえなっていればjavajetでもxmljetでもhogehogejetでもコンパイル可能です。

テンプレートの実行と生成されたソース

 適当なクラスを作り、生成したHelloJETTemplategenerateメソッドを呼び出してください。メソッドの返却値として「Hello! World」というStringが返却されます。

 生成されたHelloJETTemplateクラスの中身はどうなっているでしょうか。中身を見ると、非常に単純な仕掛けになっていることがわかります。

「HelloJETTemplate.java」
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と同様です。

JET予約語
 JETの予約語として、argumentstringBufferの二種類があります。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!
<% } %>
微妙に違うインポート宣言
 テンプレート内でjava.langパッケージ以外のクラスを利用する場合は、JETディレクティブ内のimports属性に利用するパッケージを記述します。複数利用する場合はスペース区切りでパッケージを記述していきます。JSPの仕様とは微妙に異なる点に注意してください。

JETのまとめ

 JETの使い方や利点を列挙します。

  1. 「templates」フォルダに拡張子の末尾がjetのファイルを置くだけで自動的にクラス生成。
  2. テンプレートを使う人に依存ライブラリは不要。
  3. テンプレートを使う人にJETファイルは不要。
  4. テンプレートはインスタンス化され、generateメソッド実行だけで利用できる。
  5. テンプレートの書式はほとんど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クラスを実行してください。

「Example.java」
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ファイル(sampleData.xls)
読み込まれるExcelファイル(sampleData.xls)

 上記のExcelを読み込んで次の結果を表示します。

出力されるデータ
UNIT-0001, SchemaLoader, generate, すべての引数を正常に入力する
UNIT-0002, SchemaLoader, generate, 出力先の指定をnullにする
UNIT-0003, SchemaLoader, generate, 生成するクラスのパッケージ名を
nullにする

 そして、以下のようなExcelファイルを出力します。

書き込まれるExcelファイル(outputData.xls)
書き込まれるExcelファイル(outputData.xls)

サンプルのファイル構成

「jet.excel.example」パッケージ

 Excelファイルを読み込んでJavaソースを生成するためのクラスが格納されています。

  • BindingDataクラス
  • Excelファイルの内容と、コマンドラインからの入力(生成するパッケージ、クラス名)を保持するデータオブジェクトです。
  • ColumnBindingDataクラス
  • Excelファイルの各セルの内容を保持するデータオブジェクトです。
  • SchemaLoaderクラス
  • Excelファイルの内容を読み込んで、データオブジェクトを生成するクラスです。生成したデータオブジェクトをテンプレートに渡し、Javaソースも生成します。

「jet.excel.example.generated」パッケージ

 JETによって自動生成されたクラスを格納するパッケージです。

  • TestCaseBeansクラス
  • Excelシートの各列に対応したJavaBeansです。ツールによって自動生成されます。
  • TestCaseBeansDocumentクラス
  • ExcelシートからTestCaseBeansの生成、TestCaseBeansからExcelファイルの出力を行う入出力クラスです。ツールによって自動生成されます。

「jet.excel.example.template」パッケージ

 JETファイルから自動生成されたJETを格納するパッケージです。

  • BeansBindingTemplateクラス
  • 入力されたExcelシートから、各セルにフィールドがマッピングされたJavaBeansを生成するテンプレートです。
  • ExcelBindingTemplateクラス
  • 入力されたExcelシート通りにExcelファイルの入出力を行うクラスを生成するテンプレートです。

使用したExcelスキーマ

 今回の例は非常に貧弱なスキーマしか記述することが出来ません。

  • シートの構造
  • スキーマとなるExcelファイルは、1シート、かつ、以下の形式に従ったExcelファイルに限定します。
    シートの構造
    列名1列名2列名3...
    データ1データ2データ3...
    データ1データ2データ3...
    ............
  • マッピングの定義
  • 先頭が#で始まるセルがJavaオブジェクトの持つフィールドとマッピングされます。また、セルは文字列形式のみのサポートとします。次のような形式になります。
    スキーマの例
    ID名前補足
    #id#name#value#option

作成したテンプレート

 作成したテンプレートファイルを提示します。

JavaBeans生成テンプレート

 ColumnBindingDataオブジェクトの内容に従って、JavaBeansを生成するテンプレートです。BindingDataからColumnBindingDataのListを取り出し、Listの数だけフィールド、Getter/Setterを生成しています。

JavaBeans生成テンプレート
<%@ 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に保存することで実現しています。

Excel入出力テンプレート(一部抜粋)
<%@ 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について詳しく解説されています。

  • LINEで送る
  • このエントリーをはてなブックマークに追加

修正履歴

  • 2005/08/30 14:15 ソース中のdatasをdataに変更。ご指摘ありがとうございます。

著者プロフィール

  • NTTデータ先端技術株式会社 志田 隆弘(シダ タカヒロ)

    &lt;NTTデータ先端技術株式会社について&gt; データベース、ネットワーク、OS、ミドルウェアの基盤技術を武器にシステムの技術面でのコンサルテーションや最新製品の調査を行う専門家集団。 &lt;筆者について&gt; Ja-Jakartaプロジェクトの末席にこっそりと参加しています。...

All contents copyright © 2005-2019 Shoeisha Co., Ltd. All rights reserved. ver.1.5