FIELD NOTES: 書を持って街へ出よう

合同会社フィールドワークス プログラマ兼代表のブログ

LL言語向けPDF帳票ツールField ReportsがJavaに対応しました

弊社製品LL言語向けPDF帳票ツール Field ReportsJavaに対応させました。

当初の構想としては,Java対応の手段としてOCaml-Javaを使ってみようかと思っていたのですが,今回お手軽にJNIを使ってJava用I/F (Java Bridge) を作成してみました。

作成したJava Bridgeは,Field Reports 1.2.1に同梱して,本日(2011.07.12)よりリリースしています。
ただ,Java対応の手段としては,OCaml-Java を使った方法ももう少し検討してみたいと考えています。
したがって,今回のJava Bridgeのリリースは「暫定版」の扱いといたします(将来的には,サポート対象外の扱いにする可能性があります)。

Javaプログラムの作成

nativeメソッド宣言を含むJavaプログラムを以下のように作成しました。
帳票の構成やデータなどの構造を持った情報をJSONで与えているので,非常にシンプルなAPIとなっています。

package jp.co.field_works;

import sun.misc.HexDumpEncoder;

/**
 * Reports.java
 *
 * Java bridge for Field Reports
 *
 * @author <a href="http://www.field-works.co.jp/">Field Works, LLC.</a>
 */
public class Reports {
    /**
     * 共有ライブラリのロードとモジュールの初期化
     */
    static {
        System.loadLibrary("Reports");
        init();
    }

    /**
     * Reportsモジュールの初期化を行う。
     */
    private native static void init();
   
    /**
     * バージョン番号を取得します。
     */
    public native static String version()
        throws ReportsException;
   
    /**
     * ログ出力のレベルを設定します。
     * 有効な値の範囲は0〜4です:
     *  0: ログを出力しない
     *  1: ERRORログを出力する
     *  2: WARNログを出力する
     *  3: INFOログを出力する
     *  4: DEBUGログを出力する
     * 1以上の値を設定した場合,標準エラー出力にログを出力します(初期値:0)。
     */
    public native static void setLogLevel(int n)
        throws ReportsException;

    /**
     * レンダリング・パラメータのデフォルト値を設定します。
     * レンダリング・パラメータは,JSON形式の文字列で与えます。
     */
    public native static void setDefaults(String param)
        throws ReportsException;

    /**
     * レンダリング・パラメータparamを元にレンダリングを実行し,
     * 結果をバイト文字列として返します。
     * レンダリング・パラメータは,JSON形式の文字列で与えます。
     */
    public native static byte[] renders(String param)
        throws ReportsException;

    /**
     * レンダリング・パラメータparamを元にレンダリングを実行します。
     * 処理結果は,filenameで指定したファイルに出力されます。
     * レンダリング・パラメータは,JSON形式の文字列で与えます。
     */
    public native static void render(String param, String filename)
        throws ReportsException;

    public static void main(String[] args) throws ReportsException {
        // test
        Reports.setLogLevel(3);
        System.out.println("version: " + Reports.version());
     HexDumpEncoder hexdumpencoder = new HexDumpEncoder();
     System.out.println(hexdumpencoder.encodeBuffer(Reports.renders("{}")));
    }
}

Cプログラムの作成

javahプログラムで生成したヘッダファイルでの宣言にしたがって,Cプログラムを記述しました。

今までのPython, Ruby, Perl, PHPの言語Bridgeでは,LL言語ネイティブの辞書/ハッシュ等のデータ構造をCプログラム内でJSON相当のデータ構造に変換していたのですが,以下のような理由からJava BridgeではJSON文字列形式のみの対応としました。

  • JSONICなどのJava用JSONライブラリを見てみると,Map, Array/Listだけではく,Object, Iterable, Iteratorなど多彩なオブジェクトからの変換に対応しており,これと同レベルの変換プログラムをCで記述するのは非常に困難。
  • Clojure, Scala, GroovyなどのJVM上で動作するJava以外の言語では,独自のMap, Listのデータ構造を採用していることが多く,それらすべてに対応することは事実上不可能と考えた。
  • JSON変換とPDF生成の処理時間を比べると,PDF生成にかかる時間が圧倒的に大きく,JSON変換をCで記述してもパフォーマンス向上にあまり貢献しないことがわかった。

ビルドには,OCamlのインクルードファイルが必要です。

2011.11.28 追記

Field Reports 1.3.0からは,OCamlのインクルードファイルが不要になりました。

#include <jni.h>
#include <stdio.h>
#include <string.h>
#include "jni_reports.h"

extern "C" {
#include <caml/mlvalues.h>
#include <caml/alloc.h>
#include <caml/callback.h>
#include <caml/fail.h>
#include <caml/memory.h>
#include <caml/threads.h>
#include "reports.h"
}

static void throw_exn(JNIEnv* env, value exn)
{
    jclass clazz = env->FindClass("jp/co/field_works/Reports/ReportsException");
    env->ThrowNew(clazz, String_val(string_of_exn(Extract_exception(exn))));
}

static value encode_string(JNIEnv* env, jstring jstr)
{
    CAMLparam0();
    CAMLlocal1(result);

    const char* s = env->GetStringUTFChars(jstr, NULL);
    result = caml_copy_string(s);
    env->ReleaseStringUTFChars(jstr, s);

    CAMLreturn(result);
}

/*
 * Class:     jp_co_field_works_Reports
 * Method:    init
 * Signature: ()V
 */
JNIEXPORT void JNICALL
Java_jp_co_field_1works_Reports_init(JNIEnv* env, jclass clazz)
{
    init_reports();
}

/*
 * Class:     jp_co_field_works_Reports
 * Method:    version
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL
Java_jp_co_field_1works_Reports_version(JNIEnv* env, jclass clazz)
{
    jstring jresult = NULL;
    CAMLparam0();
    CAMLlocal1(caml_result);
    caml_leave_blocking_section();

    caml_result = reports_version();
    if (Is_exception_result(caml_result)) {
        // Error
        throw_exn(env, caml_result);
        goto exit;
    }
    jresult = env->NewStringUTF(String_val(caml_result));

exit:
    caml_enter_blocking_section();
    CAMLreturnT(jstring, jresult);
}

/*
 * Class:     jp_co_field_works_Reports
 * Method:    setLogLevel
 * Signature: (I)V
 */
JNIEXPORT void JNICALL
Java_jp_co_field_1works_Reports_setLogLevel(JNIEnv* env, jclass clazz, jint n)
{
    CAMLparam0();
    CAMLlocal2(caml_level, caml_result);
    caml_leave_blocking_section();

    caml_level = Val_int(n);
    caml_result = set_log_level(caml_level);
    if (Is_exception_result(caml_result)) {
        // Error
        throw_exn(env, caml_result);
        goto exit;
    }

exit:
    caml_enter_blocking_section();
    CAMLreturn0;
}

/*
 * Class:     jp_co_field_works_Reports
 * Method:    setDefaults
 * Signature: (Ljava/lang/String;)V
 */
JNIEXPORT void JNICALL
Java_jp_co_field_1works_Reports_setDefaults(JNIEnv* env, jclass clazz, jstring param)
{
    CAMLparam0();
    CAMLlocal2(caml_param, caml_result);
    caml_leave_blocking_section();

    caml_param = json_of_string(encode_string(env, param));
    if (Is_exception_result(caml_param)) {
        // Error
        throw_exn(env, caml_param);
        goto exit;
    }
    caml_result = set_defaults(caml_param);
    if (Is_exception_result(caml_result)) {
        // Error
        throw_exn(env, caml_result);
        goto exit;
    }

exit:
    caml_enter_blocking_section();
    CAMLreturn0;
}

/*
 * Class:     jp_co_field_works_Reports
 * Method:    renders
 * Signature: (Ljava/lang/String;)[B
 */
JNIEXPORT jbyteArray JNICALL
Java_jp_co_field_1works_Reports_renders(JNIEnv* env, jclass clazz, jstring param)
{
    jbyteArray jresult = NULL;
    int len = 0;
    CAMLparam0();
    CAMLlocal2(caml_param, caml_result);
    caml_leave_blocking_section();

    // convert parameter
    caml_param = json_of_string(encode_string(env, param));
    if (Is_exception_result(caml_param)) {
        // Error
        throw_exn(env, caml_param);
        goto exit;
    }
    // call OCaml function
    caml_result = pdf_of_json(caml_param);
    if (Is_exception_result(caml_result)) {
        // Error
        throw_exn(env, caml_result);
        goto exit;
    }
    // set return value
    len = string_length(caml_result);
    jresult = env->NewByteArray(len);
    env->SetByteArrayRegion(jresult, 0, len, (jbyte*)String_val(caml_result));

exit:
    caml_enter_blocking_section();
    CAMLreturnT(jbyteArray, jresult);
}

/*
 * Class:     jp_co_field_works_Reports
 * Method:    render
 * Signature: (Ljava/lang/String;Ljava/lang/String;)V
 */
JNIEXPORT void JNICALL
Java_jp_co_field_1works_Reports_render(JNIEnv* env, jclass clazz, jstring param, jstring filename)
{
    CAMLparam0();
    CAMLlocal3(caml_fname, caml_param, caml_result);
    caml_leave_blocking_section();

    // convert parameter
    caml_param = json_of_string(encode_string(env, param));
    if (Is_exception_result(caml_param)) {
        // Error
        throw_exn(env, caml_param);
        goto exit;
    }
    caml_fname = encode_string(env, filename);
    // call OCaml function
    caml_result = write_pdf_from_json(caml_param, caml_fname);
    if (Is_exception_result(caml_result)) {
        // Error
        throw_exn(env, caml_result);
        goto exit;
    }

exit:
    caml_enter_blocking_section();
    CAMLreturn0;
}

Javaからの利用

完成したJava Bridgeを使って,JavaからField Reportsを利用するサンプルプログラムを以下に示します。
JSONライブラリにはJSONICを使用しました。

Map型のリテラルがないため煩雑なプログラムになってしまいましたが,実際のプログラムではデータベースなどから取得したデータを元に帳票を生成することになると思うので,問題ないでしょう。

import java.util.ArrayList;
import java.util.HashMap;   
import net.arnx.jsonic.JSON; 
import jp.co.field_works.Reports;
import jp.co.field_works.ReportsException;

public class mitumori { 
    public static void main(String[] args) throws ReportsException {
        // レンダリング・パラメータの作成
        HashMap param = new HashMap(); 
        param.put("template", "./mitumori.pdf");
        HashMap context = new HashMap();
        context.put("date", "平成23年7月11日");
        context.put("number", "10R0001");
        context.put("to", "△△△惣菜株式会社");
        context.put("title", "肉じゃがの材料");
        context.put("delivery_date", "平成23年7月31日");
        context.put("delivery_place", "貴社指定場所");
        context.put("payment_terms", "銀行振込");
        context.put("expiration_date", "発行から3ヶ月以内");
        HashMap icon = new HashMap();
        icon.put("icon", "./stamp.png");
        context.put("stamp1", icon);
        ArrayList table = new ArrayList();
        table.add(new String[]{"1", "N001", "牛肉(切り落とし)", "200g", "250円", "500円"});
        table.add(new String[]{"2", "Y001", "じゃがいも(乱切り)", "3個", "30円", "90円"});
        table.add(new String[]{"3", "Y002", "にんじん(乱切り)", "1本", "40円", "40円"});
        table.add(new String[]{"4", "Y003", "たまねぎ(くし切り)", "1個", "50円", "50円"});
        table.add(new String[]{"5", "Y004", "しらたき", "1袋", "80円", "80円"});
        table.add(new String[]{"6", "Y005", "いんげん", "1袋", "40円", "40円"});
        context.put("table", table);
        context.put("sub_total", "800円");
        context.put("tax", "40円");
        context.put("total", "840円");
        param.put("context", context);
        final String json = JSON.encode(param);
        System.out.println(json);
        // レンダリング実行
        Reports.setLogLevel(3);
        Reports.render(json, "out.pdf");

    } 
} 

Clojureからの利用

試しにClojureでもサンプルプログラムを作ってみました。
非常にすっきりと書けますね。

(use 'clojure.contrib.json)
(import '(jp.co.field_works Reports))

(def param {
    :template "./mitumori.pdf",
    :context {
        :date "平成23年7月11日",
        :number "10R0001",
        :to "△△△惣菜株式会社",
        :title "肉じゃがの材料",
        :delivery_date "平成23年7月31日",
        :delivery_place "貴社指定場所",
        :payment_terms "銀行振込",
        :expiration_date "発行から3ヶ月以内",
        :stamp1 {:icon "./stamp.png"},
        :table [
            ["1" "N001" "牛肉(切り落とし)" "200g" "250円" "500円"]
            ["2" "Y001" "じゃがいも(乱切り)" "3個" "30円" "90円"]
            ["3" "Y002" "にんじん(乱切り)" "1本" "40円" "40円"]
            ["4" "Y003" "たまねぎ(くし切り)" "1個" "50円" "50円"]
            ["5" "Y004" "しらたき" "1袋" "80円" "80円"]
            ["6" "Y005" "いんげん" "1袋" "40円" "40円"]
        ]
        :sub_total "800円",
        :tax "40円",
        :total "840円"}})

(Reports/setLogLevel 3)
(Reports/render (json-str param) "./out.pdf")

まとめ

Field ReportsのJava用I/F (Java Bridge) を作成しました。

JNIを使えば,LL言語用の拡張モジュールを作成するのと同程度の手間で,Cモジュールを呼び出すI/Fが作成できることがわかりました。
Groovy, ScalaなどJVM上で動作しJSON出力が可能なプログラミング言語であれば,Field Reportsをご利用頂けると思われます。

Java用に限らず,Python, Ruby, Perl, PHP用の言語Bridge部分のソースは,BSDライセンスオープンソースとしています。
よろしければ,さまざまなプログラミング言語からOCamlで作成したプログラムを呼び出すサンプルとしてご参照ください。
また,もし弊社で対応しきれないプログラミング言語用の拡張モジュールを作って頂ける方がいらっしゃれば歓迎します。